Source Code

Every automation โ€” Flavors Driving School
โ† Mission Control
17
Scripts
658KB
Total Code
15,776
Lines of Code
$0
Monthly Cost
๐Ÿ“ฑ Auto Social Media Posts 28KB ยท 698 linesGSโ–ถ
/**
 * =========================================================
 *  AUTO SOCIAL MEDIA POSTS โ€” Automation #16
 *  Flavors Driving School
 * =========================================================
 *  Auto-generates congrats social posts when students complete
 *  all lessons. Queue for Instagram, Facebook, Google Business.
 *
 *  Features:
 *  - Demo mode with fake graduates (no real data during demos)
 *  - Email-first + fuzzy name matching
 *  - Attendance counting skips cancelled/no-show/pending/scheduled/upcoming/rescheduled
 *  - Mission Control themed admin emails with unsubscribe
 *  - Graduate ordinal tracking ("our 1st graduate!")
 *  - Configurable post days (Tue/Wed/Thu default)
 *  - Weekly digest email
 *  - New graduate notification email
 *  - Manual: generatePostForStudent(), previewPosts()
 *
 *  SHEETS (in Schedule Board spreadsheet):
 *    - Social Post Queue
 *    - Posted Students
 *    - Social Settings
 *
 *  TRIGGERS: dailySocialCheck @ 11 AM daily, weeklyPostDigest Monday 9 AM
 * =========================================================
 */

// โ”€โ”€ CONFIG โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const CONFIG = {
  SCHEDULE_BOARD_ID: '1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE',
  REGISTRATION_ID: '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',
  PAYMENT_TRACKER_ID: '1XWNU7PygQX9jV6mFUUrgHYU_vrhS7NP1OHwNUNWRoOk',
  SCHOOL_NAME: 'Flavors Driving School',
  SCHOOL_EMAIL: '[email protected]',
  SCHOOL_HANDLE: '@flavorsdrivingschool',
  ADMIN_EMAIL: '[email protected]',
  BOOKING_URL: '',

  // Sheet tab names
  BOOKINGS_SHEET_TAB: 'Bookings',
  REGISTRATION_SHEET_TAB: '',

  // DEMO MODE โ€” set to true for presentations
  DEMO_MODE: true,
};

const PACKAGES = {
  '3 Lessons': 3,
  '5 Lessons': 5,
  '10 Lessons': 10,
  '15 Lessons': 15,
  '25 Lessons': 25,
  '5-Hour Class': 1,
};

const HASHTAGS = {
  primary: '#FlavorsDrivingSchool',
  extra: ['#DrivingSchool', '#LearnToDrive', '#NewDriver', '#SafeDriving', '#QueensNY'],
};

// โ”€โ”€ DEMO DATA โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const DEMO = {
  graduates: [
    { name: 'Sarah Johnson', ordinal: '47th' },
    { name: 'Marcus Williams', ordinal: '48th' },
  ],
  existingCount: 46,
};

// โ”€โ”€ FORCE AUTH โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function forceAuth() {
  SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  MailApp.getRemainingDailyQuota();
  Logger.log('โœ… All permissions authorized โ€” 3 sheets + email.');
}

// โ”€โ”€ SETUP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function fullSetup() {
  setupSocialSheets_();
  setupTriggers_();
  Logger.log('โœ… Auto Social Media Posts fully set up.');
}

function setupSocialSheets_() {
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);

  let queue = ss.getSheetByName('Social Post Queue');
  if (!queue) {
    queue = ss.insertSheet('Social Post Queue');
    queue.getRange('A1:E1').setValues([['Platform', 'Suggested Date', 'Content', 'Student Name', 'Status']]);
    formatHeader_(queue, 5);
  }

  let posted = ss.getSheetByName('Posted Students');
  if (!posted) {
    posted = ss.insertSheet('Posted Students');
    posted.getRange('A1:C1').setValues([['Student Name', 'Graduate Date', 'Posted']]);
    formatHeader_(posted, 3);
  }

  let settings = ss.getSheetByName('Social Settings');
  if (!settings) {
    settings = ss.insertSheet('Social Settings');
    settings.getRange('A1:B1').setValues([['Setting', 'Value']]);
    formatHeader_(settings, 2);
    const defaults = [
      ['Demo Mode', CONFIG.DEMO_MODE ? 'Yes' : 'No'],
      ['School Name', CONFIG.SCHOOL_NAME],
      ['School Email', CONFIG.SCHOOL_EMAIL],
      ['School Handle', CONFIG.SCHOOL_HANDLE],
      ['Admin Email', CONFIG.ADMIN_EMAIL],
      ['Send New Posts Email', 'Yes'],
      ['Post Day 1', 'Tuesday'],
      ['Post Day 2', 'Wednesday'],
      ['Post Day 3', 'Thursday'],
      ['Default Post Time', '11:00 AM'],
      ['Total Graduates', '0'],
    ];
    settings.getRange(2, 1, defaults.length, 2).setValues(defaults);
    settings.getRange(2, 2).setFontColor('#f59e0b').setFontWeight('bold').setFontSize(12);
    settings.getRange(2, 1, defaults.length, 2).setBackground('#0a0a0a');
    settings.getRange(2, 1, defaults.length, 1).setFontColor('#888');
    settings.getRange(2, 2, defaults.length, 1).setFontColor('#fff').setFontWeight('bold');
    settings.setColumnWidth(1, 260);
    settings.setColumnWidth(2, 200);
  }

  Logger.log('โœ… Social sheets created/verified.');
}

function setupTriggers_() {
  const clean = ['dailySocialCheck', 'weeklyPostDigest'];
  ScriptApp.getProjectTriggers().forEach(t => {
    if (clean.includes(t.getHandlerFunction())) ScriptApp.deleteTrigger(t);
  });

  ScriptApp.newTrigger('dailySocialCheck').timeBased().everyDays(1).atHour(11).nearMinute(0).create();
  ScriptApp.newTrigger('weeklyPostDigest').timeBased().onWeekDay(ScriptApp.WeekDay.MONDAY).atHour(9).nearMinute(0).create();

  Logger.log('โœ… Triggers: dailySocialCheck 11 AM daily, weeklyPostDigest Monday 9 AM');
}

function formatHeader_(sheet, cols) {
  sheet.getRange(1, 1, 1, cols).setFontWeight('bold').setBackground('#1a1a1a').setFontColor('#ff2d2d').setFontSize(10);
  sheet.setFrozenRows(1);
}


// ================================================================
//  DAILY CHECK (trigger)
// ================================================================

function dailySocialCheck() {
  try {
    const settings = getSettings_();
    const isDemo = settings.demoMode;

    if (isDemo) {
      Logger.log('โš ๏ธ DEMO MODE โ€” using fake graduate data.');
    }

    const graduates = isDemo ? DEMO.graduates : findNewGraduates_();

    if (graduates.length === 0) {
      Logger.log('No new graduates today.');
      return;
    }

    const board = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
    const queueSheet = board.getSheetByName('Social Post Queue');
    const postedSheet = board.getSheetByName('Posted Students');
    const platforms = ['Instagram', 'Facebook', 'Google Business'];
    const postDate = suggestPostDate_(settings);

    for (const grad of graduates) {
      const posts = generatePosts_(grad.name, grad.ordinal, settings);
      for (const platform of platforms) {
        const content = getTemplateForPlatform_(posts, platform);
        if (content) {
          if (!isDemo) {
            queueSheet.appendRow([platform, postDate, content, grad.name, 'Queued']);
          }
        }
      }
      if (!isDemo) {
        postedSheet.appendRow([grad.name, new Date(), 'No']);
      }

      Logger.log((isDemo ? 'DEMO: Would queue' : 'Queued') + ' posts for ' + grad.name + ' (' + grad.ordinal + ' graduate)');
    }

    if (!isDemo) {
      updateTotalGraduates_(board, graduates.length);
    }

    if (settings.sendNewPostsEmail && settings.adminEmail) {
      sendNewPostsEmail_(graduates, settings, isDemo);
    }

    Logger.log((isDemo ? 'DEMO: Would queue' : 'Queued') + ' posts for ' + graduates.length + ' new graduate(s).');

  } catch (e) {
    Logger.log('โŒ dailySocialCheck error: ' + e.message + '\n' + e.stack);
    try {
      if (CONFIG.ADMIN_EMAIL) {
        MailApp.sendEmail({
          to: CONFIG.ADMIN_EMAIL,
          subject: 'โŒ Auto Social Posts โ€” Daily check failed',
          body: 'Error: ' + (e.message || e.toString()),
        });
      }
    } catch (mailErr) { Logger.log('Could not send error email: ' + mailErr.message); }
  }
}


// ================================================================
//  WEEKLY DIGEST (trigger)
// ================================================================

function weeklyPostDigest() {
  try {
    const settings = getSettings_();
    if (settings.demoMode) {
      Logger.log('โš ๏ธ DEMO MODE โ€” skipping weekly digest email.');
      return;
    }

    const board = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
    const queueSheet = board.getSheetByName('Social Post Queue');
    if (!queueSheet) return;

    const data = queueSheet.getDataRange().getValues();
    if (data.length < 2) { Logger.log('No posts in queue.'); return; }

    if (!settings.adminEmail) return;

    let rows = '';
    for (let i = 1; i < data.length; i++) {
      const platform = escHtml_(String(data[i][0] || '').trim());
      const rawDate = data[i][1];
      const dateStr = rawDate instanceof Date
        ? Utilities.formatDate(rawDate, Session.getScriptTimeZone(), 'MMM d, yyyy')
        : escHtml_(String(rawDate || ''));
      const content = escHtml_(String(data[i][2] || '').substring(0, 200));
      const student = escHtml_(String(data[i][3] || '').trim());
      const status = escHtml_(String(data[i][4] || '').trim());

      rows +=
        '<tr>' +
        '<td style="color:#ccc;padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.04);font-size:13px;">' + platform + '</td>' +
        '<td style="color:#ccc;padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.04);font-size:13px;">' + dateStr + '</td>' +
        '<td style="color:#fff;padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.04);font-size:13px;font-weight:500;">' + student + '</td>' +
        '<td style="color:#888;padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.04);font-size:12px;">' + content + '</td>' +
        '<td style="color:' + (status.toLowerCase() === 'posted' ? '#22c55e' : '#f59e0b') + ';padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.04);font-size:12px;font-weight:600;">' + status + '</td>' +
        '</tr>';
    }

    const tableContent =
      '<table width="100%" cellpadding="0" cellspacing="0" style="margin:20px 0;">' +
      '<tr>' +
      '<td style="color:#ff2d2d;padding:8px 12px;font-size:10px;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid rgba(255,255,255,0.06);">Platform</td>' +
      '<td style="color:#ff2d2d;padding:8px 12px;font-size:10px;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid rgba(255,255,255,0.06);">Date</td>' +
      '<td style="color:#ff2d2d;padding:8px 12px;font-size:10px;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid rgba(255,255,255,0.06);">Student</td>' +
      '<td style="color:#ff2d2d;padding:8px 12px;font-size:10px;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid rgba(255,255,255,0.06);">Content</td>' +
      '<td style="color:#ff2d2d;padding:8px 12px;font-size:10px;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid rgba(255,255,255,0.06);">Status</td>' +
      '</tr>' + rows + '</table>' +
      '<p style="color:#555;font-size:13px;margin-top:16px;">' + (data.length - 1) + ' total posts in queue</p>';

    const html = missionControlWrapper_('Weekly Post Digest', tableContent);

    MailApp.sendEmail({
      to: settings.adminEmail,
      subject: '๐Ÿ“ฑ Social Post Queue โ€” Weekly Digest โ€” ' + CONFIG.SCHOOL_NAME,
      htmlBody: html,
    });

    Logger.log('โœ… Weekly digest sent.');

  } catch (e) {
    Logger.log('โŒ weeklyPostDigest error: ' + e.message + '\n' + e.stack);
  }
}


// ================================================================
//  FIND NEW GRADUATES
// ================================================================

function findNewGraduates_() {
  const regSS = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  const regSheet = CONFIG.REGISTRATION_SHEET_TAB
    ? regSS.getSheetByName(CONFIG.REGISTRATION_SHEET_TAB)
    : regSS.getSheets()[0];
  if (!regSheet) return [];

  const regData = regSheet.getDataRange().getValues();
  if (regData.length < 2) return [];

  const headers = regData[0].map(h => String(h).toLowerCase().trim());
  const nameCol = findCol_(headers, ['full name', 'name', 'student name']);
  const emailCol = findCol_(headers, ['email', 'email address', 'student email']);
  const packageCol = findCol_(headers, ['lesson package', 'package', 'selected package', 'class type']);

  const students = [];
  for (let i = 1; i < regData.length; i++) {
    const name = String(regData[i][nameCol] || '').trim();
    const email = emailCol !== -1 ? String(regData[i][emailCol] || '').trim().toLowerCase() : '';
    const pkgRaw = packageCol !== -1 ? String(regData[i][packageCol] || '').trim() : '';
    if (!name) continue;

    const lessonCount = extractLessonCount_(pkgRaw);
    students.push({ name, email, lessonCount, nameKey: name.toLowerCase() });
  }

  // Count completed lessons per student
  const board = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const bookingsSheet = CONFIG.BOOKINGS_SHEET_TAB
    ? board.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB)
    : board.getSheets()[0];
  if (!bookingsSheet) return [];

  const bookData = bookingsSheet.getDataRange().getValues();
  if (bookData.length < 2) return [];

  const bookHeaders = bookData[0].map(h => String(h).toLowerCase().trim());
  const bookNameCol = findCol_(bookHeaders, ['student', 'student name', 'name']);
  const bookEmailCol = findCol_(bookHeaders, ['email', 'student email']);
  const bookStatusCol = findCol_(bookHeaders, ['status', 'booking status']);
  const bookDateCol = findCol_(bookHeaders, ['date', 'lesson date', 'booking date']);

  // Only count confirmed past lessons
  const skipStatuses = ['cancelled', 'canceled', 'no-show', 'no show', 'noshow', 'pending', 'scheduled', 'upcoming', 'rescheduled'];
  const today = new Date();

  // Build completed count by name and email
  const completedByName = {};
  const completedByEmail = {};

  for (let i = 1; i < bookData.length; i++) {
    const status = String(bookData[i][bookStatusCol] || '').toLowerCase().trim();
    if (skipStatuses.includes(status)) continue;

    const rawDate = bookData[i][bookDateCol];
    const date = rawDate instanceof Date ? rawDate : new Date(rawDate);
    if (isNaN(date.getTime()) || date > today) continue;

    const sName = String(bookData[i][bookNameCol] || '').trim().toLowerCase();
    const sEmail = bookEmailCol !== -1 ? String(bookData[i][bookEmailCol] || '').trim().toLowerCase() : '';

    if (sName) completedByName[sName] = (completedByName[sName] || 0) + 1;
    if (sEmail && sEmail.includes('@')) completedByEmail[sEmail] = (completedByEmail[sEmail] || 0) + 1;
  }

  // Get already-posted students
  const postedSheet = board.getSheetByName('Posted Students');
  const postedSet = new Set();
  if (postedSheet && postedSheet.getLastRow() > 1) {
    const postedData = postedSheet.getDataRange().getValues();
    for (let i = 1; i < postedData.length; i++) {
      const postedName = String(postedData[i][0] || '').trim().toLowerCase();
      if (postedName) postedSet.add(postedName);
    }
  }

  const existingCount = postedSheet ? Math.max(0, postedSheet.getLastRow() - 1) : 0;
  const graduates = [];

  for (const st of students) {
    // Skip if already posted (exact or fuzzy)
    if (postedSet.has(st.nameKey)) continue;
    let alreadyPosted = false;
    for (const posted of postedSet) {
      if (fuzzyNameMatch_(st.nameKey, posted)) { alreadyPosted = true; break; }
    }
    if (alreadyPosted) continue;

    // Count completed: email-first, then name, then fuzzy
    let completed = 0;
    if (st.email && completedByEmail[st.email]) {
      completed = completedByEmail[st.email];
    } else if (completedByName[st.nameKey]) {
      completed = completedByName[st.nameKey];
    } else {
      // Fuzzy name match
      for (const [key, count] of Object.entries(completedByName)) {
        if (fuzzyNameMatch_(st.nameKey, key)) {
          completed = count;
          break;
        }
      }
    }

    if (st.lessonCount > 0 && completed >= st.lessonCount) {
      graduates.push({
        name: st.name,
        ordinal: getOrdinal_(existingCount + graduates.length + 1),
      });
    }
  }

  return graduates;
}


// ================================================================
//  POST GENERATION
// ================================================================

function generatePosts_(studentName, ordinal, settings) {
  const school = settings.schoolName || CONFIG.SCHOOL_NAME;
  const handle = settings.schoolHandle || CONFIG.SCHOOL_HANDLE;
  const tags = HASHTAGS.primary + ' ' + HASHTAGS.extra.join(' ');
  const safeName = escHtml_(studentName);

  return {
    instagram: '๐ŸŽ‰๐Ÿš— Congrats to our ' + ordinal + ' graduate, ' + studentName + '! Another one ready for the road! ' + handle + '\n\n' + tags,
    facebook: '๐ŸŽ“ Congratulations to ' + studentName + ' โ€” our ' + ordinal + ' graduate! We\'re so proud of your hard work and dedication. Ready for the open road! ๐Ÿš—๐Ÿ’จ\n\n' + school + '\n' + tags,
    google: 'Congratulations ' + studentName + '! You\'re our ' + ordinal + ' graduate. Thank you for choosing ' + school + '! ๐ŸŽ‰ ' + tags,
  };
}

function getTemplateForPlatform_(posts, platform) {
  const p = String(platform || '').trim().toLowerCase();
  if (p === 'instagram') return posts.instagram;
  if (p === 'facebook') return posts.facebook;
  if (p.includes('google')) return posts.google;
  return posts.instagram;
}


// ================================================================
//  EMAIL โ€” Mission Control Theme
// ================================================================

function missionControlWrapper_(title, content) {
  const unsubLink = '<p style="margin:8px 0 0;color:#333;font-size:10px;"><a href="mailto:' + escHtml_(CONFIG.SCHOOL_EMAIL) + '?subject=Unsubscribe%20Social%20Post%20Notifications" style="color:#555;text-decoration:underline;">Unsubscribe</a></p>';

  return '<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"></head>' +
    '<body style="margin:0;padding:0;background:#000;font-family:-apple-system,BlinkMacSystemFont,\'SF Pro Display\',sans-serif;">' +
    '<table width="100%" cellpadding="0" cellspacing="0" style="background:#000;padding:20px;">' +
    '<tr><td align="center">' +
    '<table width="600" cellpadding="0" cellspacing="0" style="background:#0d0d0d;border-radius:20px;border:1px solid rgba(255,255,255,0.06);overflow:hidden;box-shadow:0 8px 30px rgba(0,0,0,0.5);">' +
    '<tr><td style="padding:30px 40px;border-bottom:1px solid rgba(255,255,255,0.06);">' +
    '<table width="100%"><tr>' +
    '<td style="color:#ff2d2d;font-size:22px;font-weight:700;">๐Ÿ“ฑ ' + escHtml_(title) + '</td>' +
    '<td align="right" style="color:#555;font-size:11px;">' + escHtml_(CONFIG.SCHOOL_NAME) + '</td>' +
    '</tr></table></td></tr>' +
    '<tr><td style="padding:30px 40px;">' + content + '</td></tr>' +
    '<tr><td style="padding:16px 40px;border-top:1px solid rgba(255,255,255,0.06);text-align:center;">' +
    '<p style="color:#333;font-size:11px;margin:0;">Powered by Auto Social Posts โ€ข <span style="color:#ff2d2d;">' + escHtml_(CONFIG.SCHOOL_NAME) + '</span></p>' +
    unsubLink +
    '</td></tr></table></td></tr></table></body></html>';
}

function sendNewPostsEmail_(graduates, settings, isDemo) {
  let gradList = '';
  for (const grad of graduates) {
    gradList +=
      '<div style="display:flex;align-items:center;padding:12px 14px;background:rgba(255,255,255,0.03);border-radius:12px;margin-bottom:8px;border:1px solid rgba(255,255,255,0.06);">' +
      '<div style="width:40px;height:40px;background:linear-gradient(135deg,#ff2d2d,#cc0000);border-radius:12px;display:flex;align-items:center;justify-content:center;margin-right:14px;">' +
      '<span style="color:#fff;font-size:18px;font-weight:700;">' + escHtml_(grad.name.charAt(0)) + '</span></div>' +
      '<div><p style="color:#fff;font-size:15px;font-weight:600;margin:0;">' + escHtml_(grad.name) + '</p>' +
      '<p style="color:#888;font-size:12px;margin:2px 0 0;">' + escHtml_(grad.ordinal) + ' graduate</p></div></div>';
  }

  const demoTag = isDemo ? '<div style="background:rgba(255,45,45,0.08);border:1px solid rgba(255,45,45,0.2);border-radius:12px;padding:10px 16px;margin-bottom:20px;text-align:center;"><span style="color:#ff6b6b;font-size:12px;font-weight:700;letter-spacing:1px;text-transform:uppercase;">โœฆ Demo Mode</span></div>' : '';

  const content = demoTag +
    '<p style="color:#ccc;font-size:16px;line-height:1.6;">New graduate posts have been ' + (isDemo ? 'simulated' : 'queued') + '!</p>' +
    gradList +
    '<p style="color:#888;font-size:13px;margin-top:20px;">Posts are queued for Instagram, Facebook, and Google Business. Review and post from the <strong style="color:#fff;">Social Post Queue</strong> sheet.</p>';

  const html = missionControlWrapper_('New Graduate Posts ' + (isDemo ? '[DEMO]' : 'Queued'), content);

  try {
    MailApp.sendEmail({
      to: settings.adminEmail,
      subject: (isDemo ? '[DEMO] ' : '') + '๐ŸŽ“ New graduate posts queued โ€” ' + CONFIG.SCHOOL_NAME,
      htmlBody: html,
    });
  } catch (e) { Logger.log('Error sending new posts email: ' + e.message); }
}


// ================================================================
//  UTILITIES
// ================================================================

function getSettings_() {
  const d = {
    demoMode: CONFIG.DEMO_MODE,
    schoolName: CONFIG.SCHOOL_NAME,
    schoolEmail: CONFIG.SCHOOL_EMAIL,
    schoolHandle: CONFIG.SCHOOL_HANDLE,
    adminEmail: CONFIG.ADMIN_EMAIL,
    sendNewPostsEmail: true,
    postDay1: 'Tuesday',
    postDay2: 'Wednesday',
    postDay3: 'Thursday',
    defaultPostTime: '11:00 AM',
  };

  try {
    const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
    const sheet = ss.getSheetByName('Social Settings');
    if (!sheet) return d;

    const data = sheet.getDataRange().getValues();
    for (let i = 1; i < data.length; i++) {
      const key = String(data[i][0] || '').toLowerCase().trim();
      const val = String(data[i][1] || '').trim();

      if (key.includes('demo mode')) d.demoMode = val.toLowerCase() === 'yes';
      else if (key.includes('school name')) d.schoolName = val;
      else if (key.includes('school email')) d.schoolEmail = val;
      else if (key.includes('school handle')) d.schoolHandle = val;
      else if (key.includes('admin email')) d.adminEmail = val;
      else if (key.includes('send new posts')) d.sendNewPostsEmail = val.toLowerCase() === 'yes';
      else if (key.includes('post day 1')) d.postDay1 = val || 'Tuesday';
      else if (key.includes('post day 2')) d.postDay2 = val || 'Wednesday';
      else if (key.includes('post day 3')) d.postDay3 = val || 'Thursday';
      else if (key.includes('default post time')) d.defaultPostTime = val || '11:00 AM';
    }
  } catch (e) { Logger.log('Settings error: ' + e.message); }
  return d;
}

function suggestPostDate_(settings) {
  const now = new Date();
  const dayNames = { sunday: 0, monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6 };
  const dayNum = str => dayNames[String(str || '').toLowerCase().trim()] ?? -1;

  let targetDays = [dayNum(settings.postDay1), dayNum(settings.postDay2), dayNum(settings.postDay3)].filter(d => d >= 0);
  if (targetDays.length === 0) targetDays = [2, 3, 4];

  const timeStr = (settings.defaultPostTime || '11:00 AM').trim();
  let hour = 11, minute = 0;
  const match = timeStr.match(/(\d+)\s*:\s*(\d+)\s*(AM|PM)?/i);
  if (match) {
    hour = parseInt(match[1]);
    minute = parseInt(match[2]) || 0;
    if (match[3] && match[3].toUpperCase() === 'PM' && hour < 12) hour += 12;
    if (match[3] && match[3].toUpperCase() === 'AM' && hour === 12) hour = 0;
  }

  const d = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, 0, 0);
  while (d <= now || !targetDays.includes(d.getDay())) {
    d.setDate(d.getDate() + 1);
    d.setHours(hour, minute, 0, 0);
  }
  return d;
}

function updateTotalGraduates_(board, addCount) {
  const settingsSheet = board.getSheetByName('Social Settings');
  if (!settingsSheet) return;

  const data = settingsSheet.getDataRange().getValues();
  let rowIndex = -1;
  for (let i = 0; i < data.length; i++) {
    if (String(data[i][0] || '').toLowerCase().includes('total graduate')) {
      rowIndex = i + 1;
      break;
    }
  }

  let current = 0;
  if (rowIndex > 0) {
    current = parseInt(String(settingsSheet.getRange(rowIndex, 2).getValue()).trim()) || 0;
  } else {
    settingsSheet.appendRow(['Total Graduates', '0']);
    rowIndex = settingsSheet.getLastRow();
  }
  settingsSheet.getRange(rowIndex, 2).setValue(current + addCount);
}

function getOrdinal_(n) {
  const num = parseInt(n) || 0;
  if (num >= 11 && num <= 13) return num + 'th';
  const last = num % 10;
  if (last === 1) return num + 'st';
  if (last === 2) return num + 'nd';
  if (last === 3) return num + 'rd';
  return num + 'th';
}

function findCol_(headers, candidates) {
  for (const c of candidates) {
    const idx = headers.findIndex(h => h.includes(c.toLowerCase()));
    if (idx !== -1) return idx;
  }
  return -1;
}

function escHtml_(str) {
  if (!str) return '';
  return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function extractLessonCount_(pkg) {
  if (!pkg) return 0;
  const raw = pkg.toString().toLowerCase().trim();
  if (raw.includes('5-hour') || raw.includes('5 hour')) return 1;
  const match = raw.match(/(\d+)\s*lesson/i);
  if (match) return parseInt(match[1]);
  const match2 = raw.match(/^(\d+)$/);
  if (match2) return parseInt(match2[1]);
  if (raw.includes('beginner')) return 10;
  if (raw.includes('standard')) return 10;
  if (raw.includes('premium')) return 15;
  if (raw.includes('intensive')) return 20;
  return 0;
}

function formatDate_(date) {
  if (!date) return '';
  if (!(date instanceof Date)) date = new Date(date);
  if (isNaN(date.getTime())) return '';
  return (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear();
}


// ================================================================
//  FUZZY NAME MATCHING
// ================================================================

function fuzzyNameMatch_(name1, name2) {
  if (!name1 || !name2) return false;
  const n1 = name1.toLowerCase().replace(/\s+/g, ' ').trim();
  const n2 = name2.toLowerCase().replace(/\s+/g, ' ').trim();
  if (n1 === n2) return true;
  if (n1.includes(n2) || n2.includes(n1)) return true;

  const p1 = n1.split(' ').filter(Boolean);
  const p2 = n2.split(' ').filter(Boolean);

  if (p1.length >= 2 && p2.length >= 2) {
    if (p1[p1.length-1] === p2[p2.length-1] && p1[0].substring(0,3) === p2[0].substring(0,3)) return true;
    if (p1[0] === p2[p2.length-1] && p1[p1.length-1] === p2[0]) return true;
  }
  if (levenshtein_(n1, n2) <= 2) return true;
  return false;
}

function levenshtein_(a, b) {
  if (a.length === 0) return b.length;
  if (b.length === 0) return a.length;
  const m = [];
  for (let i = 0; i <= b.length; i++) m[i] = [i];
  for (let j = 0; j <= a.length; j++) m[0][j] = j;
  for (let i = 1; i <= b.length; i++) {
    for (let j = 1; j <= a.length; j++) {
      m[i][j] = b.charAt(i-1) === a.charAt(j-1) ? m[i-1][j-1] : Math.min(m[i-1][j-1]+1, m[i][j-1]+1, m[i-1][j]+1);
    }
  }
  return m[b.length][a.length];
}


// ================================================================
//  MANUAL FUNCTIONS
// ================================================================

function generatePostForStudent(studentName) {
  const board = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const settings = getSettings_();
  const postedSheet = board.getSheetByName('Posted Students');
  const total = postedSheet ? Math.max(0, postedSheet.getLastRow() - 1) + 1 : 1;
  const posts = generatePosts_(studentName, getOrdinal_(total), settings);
  const queueSheet = board.getSheetByName('Social Post Queue');
  const postDate = suggestPostDate_(settings);

  queueSheet.appendRow(['Instagram', postDate, posts.instagram, studentName, 'Queued']);
  queueSheet.appendRow(['Facebook', postDate, posts.facebook, studentName, 'Queued']);
  queueSheet.appendRow(['Google Business', postDate, posts.google, studentName, 'Queued']);
  postedSheet.appendRow([studentName, new Date(), 'No']);
  updateTotalGraduates_(board, 1);

  Logger.log('โœ… Queued posts for ' + studentName + ' (' + getOrdinal_(total) + ' graduate)');
}

function previewPosts() {
  const settings = getSettings_();
  const posts = generatePosts_('Preview Student', getOrdinal_(1), settings);
  Logger.log('๐Ÿ“ธ Instagram:\n' + posts.instagram);
  Logger.log('๐Ÿ“˜ Facebook:\n' + posts.facebook);
  Logger.log('๐Ÿข Google Business:\n' + posts.google);
}

function runOptimizationNow() {
  dailySocialCheck();
  Logger.log('โœ… Manual social check complete.');
}
๐Ÿ“Š Business Intelligence Dashboard 37KB ยท 719 linesGSโ–ถ
/**
 * =========================================================
 *  BUSINESS INTELLIGENCE DASHBOARD โ€” Cherry on Top #2
 *  Flavors Driving School
 * =========================================================
 *  Live web app โ€” opens on Mom's phone, shows everything.
 *  Mobile-first. Real-time data. Zero effort.
 *
 *  Features:
 *  - Demo mode with realistic fake business data
 *  - PIN gate (4-digit access code)
 *  - Mission Control theme (#000/#0d0d0d/#ff2d2d)
 *  - Revenue snapshot (today / week / month / trend)
 *  - Active students & new signups
 *  - Today's schedule with instructor breakdown
 *  - Instructor utilization (visual bars)
 *  - Payment health (collected vs outstanding)
 *  - Tomorrow's preview
 *  - Alerts & action items
 *  - 90-second data cache for performance
 *  - Mobile-first with floating refresh button
 * =========================================================
 */

const CONFIG = {
  SCHEDULE_BOARD_ID: '1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE',
  REGISTRATION_ID: '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',
  PAYMENT_TRACKER_ID: '1XWNU7PygQX9jV6mFUUrgHYU_vrhS7NP1OHwNUNWRoOk',
  SIGNUP_SHEET_ID: '1lE2_amcNj3-ao3TiP_U0VpaWHqmx4R-0rkF3n3xl7fM',
  EXPENSE_TRACKER_ID: '1QyC39fjuslDXk-a_u09XACyHSq782H7NUJajV3vBp8M',
  SCHOOL_NAME: 'Flavors Driving School',
  SCHOOL_EMAIL: '[email protected]',
  INSTRUCTORS: ['Anisha', 'Carlos', 'Nick'],
  WORK_START: 9,
  WORK_END: 18,

  BOOKINGS_SHEET_TAB: 'Bookings',
  REGISTRATION_SHEET_TAB: '',
  SIGNUP_SHEET_TAB: '',

  CACHE_TTL_SECONDS: 90,

  // Access PIN โ€” change this to your own 4-digit code
  ACCESS_PIN: '7777',

  // DEMO MODE โ€” set to true for presentations
  DEMO_MODE: true,
};

// โ”€โ”€ DEMO DATA โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const DEMO = {
  revenue: { today: 650, week: 2150, month: 8400, lastMonth: 7200, trend: '17', netMonth: 6850 },
  expenses: { month: 1550 },
  students: {
    active: 38, total: 38, newWeek: 3, newMonth: 9, fiveHourMonth: 5,
    recent: [
      { name: 'Tyler Brooks', package: '10 Lessons', date: '6/25' },
      { name: 'Jessica Kim', package: '5-Hour Class', date: '6/24' },
      { name: 'Marcus Williams', package: '15 Lessons', date: '6/23' },
      { name: 'Priya Patel', package: '10 Lessons', date: '6/22' },
    ],
  },
  lessons: { completedToday: 5, noShowsToday: 1, remainingToday: 3, weekTotal: 32 },
  todaySchedule: [
    { time: '9:00 AM', student: 'Sarah Johnson', instructor: 'Carlos', status: 'completed' },
    { time: '10:00 AM', student: 'Marcus Williams', instructor: 'Nick', status: 'completed' },
    { time: '11:00 AM', student: 'Priya Patel', instructor: 'Anisha', status: 'completed' },
    { time: '1:00 PM', student: 'Alex Rivera', instructor: 'Carlos', status: 'no show' },
    { time: '2:00 PM', student: 'Sofia Rivera', instructor: 'Anisha', status: 'completed' },
    { time: '3:00 PM', student: 'Emma Garcia', instructor: 'Carlos', status: 'scheduled' },
    { time: '4:00 PM', student: 'Tyler Brooks', instructor: 'Nick', status: 'scheduled' },
    { time: '5:00 PM', student: 'David Chen', instructor: 'Anisha', status: 'scheduled' },
  ],
  utilization: {
    'Anisha': { booked: 24, total: 63, percent: 38, todayCount: 3 },
    'Carlos': { booked: 35, total: 63, percent: 56, todayCount: 3 },
    'Nick': { booked: 18, total: 63, percent: 29, todayCount: 2 },
  },
  paymentHealth: { totalPaid: 28500, totalOwed: 3200, studentsOwing: 6, studentsPaid: 32 },
  tomorrow: {
    byInstructor: {
      'Anisha': [
        { time: '9:00 AM', student: 'Priya Patel' },
        { time: '11:00 AM', student: 'Sofia Rivera' },
        { time: '2:00 PM', student: 'Emma Garcia' },
      ],
      'Carlos': [
        { time: '10:00 AM', student: 'Sarah Johnson' },
        { time: '1:00 PM', student: 'Tyler Brooks' },
      ],
      'Nick': [
        { time: '9:00 AM', student: 'Marcus Williams' },
        { time: '3:00 PM', student: 'David Chen' },
      ],
    },
    total: 7, open: 20,
  },
  overdue: [
    { name: 'Alex Rivera', balance: 250, days: 21 },
    { name: 'Jade Thompson', balance: 175, days: 16 },
  ],
  dayName: 'Wednesday',
  tomorrowDay: 'Thursday',
};

// โ”€โ”€ WEB APP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function doGet(e) {
  return HtmlService.createHtmlOutput(getDashboardHTML_())
    .setTitle('Business Intelligence โ€” ' + CONFIG.SCHOOL_NAME)
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .addMetaTag('viewport', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');
}

// โ”€โ”€ SETUP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function forceAuth() {
  SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  SpreadsheetApp.openById(CONFIG.SIGNUP_SHEET_ID);
  SpreadsheetApp.openById(CONFIG.EXPENSE_TRACKER_ID);
  MailApp.getRemainingDailyQuota();
  Logger.log('โœ… All permissions authorized โ€” 5 sheets + email.');
}

function fullSetup() {
  Logger.log('โœ… Business Intelligence Dashboard ready. Deploy as web app to use.');
}

// โ”€โ”€ PIN VERIFICATION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function verifyPin(pin) {
  pin = String(pin || '').trim();
  if (CONFIG.DEMO_MODE) {
    return { verified: pin === CONFIG.ACCESS_PIN || pin === '0000' };
  }
  return { verified: pin === CONFIG.ACCESS_PIN };
}

// โ”€โ”€ DATA API (called from client) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function getDashboardData() {
  try {
    if (CONFIG.DEMO_MODE) {
      return DEMO;
    }

    if (CONFIG.CACHE_TTL_SECONDS > 0) {
      const cache = CacheService.getScriptCache();
      const cached = cache.get('bi_dashboard');
      if (cached) return JSON.parse(cached);
    }

    const payload = buildDashboardData_();

    if (CONFIG.CACHE_TTL_SECONDS > 0) {
      try {
        CacheService.getScriptCache().put('bi_dashboard', JSON.stringify(payload), CONFIG.CACHE_TTL_SECONDS);
      } catch (cacheErr) { Logger.log('Cache put failed: ' + cacheErr.message); }
    }
    return payload;
  } catch (e) {
    Logger.log('getDashboardData error: ' + e.message);
    return { error: true, message: String(e.message || '').substring(0, 200) };
  }
}

function buildDashboardData_() {
  const now = new Date();
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const endOfToday = new Date(today); endOfToday.setHours(23, 59, 59, 999);
  const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1);
  const endOfTomorrow = new Date(tomorrow); endOfTomorrow.setHours(23, 59, 59, 999);
  const monday = new Date(today);
  const dow = monday.getDay();
  monday.setDate(monday.getDate() - (dow === 0 ? 6 : dow - 1));
  const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
  const lastMonthStart = new Date(today.getFullYear(), today.getMonth() - 1, 1);
  const lastMonthEnd = new Date(today.getFullYear(), today.getMonth(), 0, 23, 59, 59, 999);

  const allPayments = getAllPayments_();
  const revenueToday = sumInRange_(allPayments, today, endOfToday);
  const revenueWeek = sumInRange_(allPayments, monday, endOfToday);
  const revenueMonth = sumInRange_(allPayments, monthStart, endOfToday);
  const revenueLastMonth = sumInRange_(allPayments, lastMonthStart, lastMonthEnd);
  const monthTrend = revenueLastMonth > 0 ? ((revenueMonth - revenueLastMonth) / revenueLastMonth * 100).toFixed(0) : '0';

  const allExpenses = getAllExpenses_();
  const expensesMonth = sumInRange_(allExpenses, monthStart, endOfToday);

  const students = getStudentData_();
  const fiveHourMonth = get5HourSignups_(monthStart, endOfToday);

  const allBookings = getAllBookings_();
  const todaysLessons = filterBookings_(allBookings, today, endOfToday);
  const tomorrowLessons = filterBookings_(allBookings, tomorrow, endOfTomorrow);
  const weekLessons = filterBookings_(allBookings, monday, endOfToday);

  // Only count confirmed past lessons as completed
  const skipStatuses = ['cancelled', 'canceled', 'no-show', 'no show', 'noshow', 'pending', 'scheduled', 'upcoming', 'rescheduled'];
  const completedToday = todaysLessons.filter(l => !skipStatuses.includes(l.status) && isPast_(l.date, l.time)).length;
  const noShowsToday = todaysLessons.filter(l => l.status === 'no show' || l.status === 'no-show' || l.status === 'noshow').length;
  const remainingToday = todaysLessons.filter(l => !isCancelled_(l.status) && l.status !== 'no show' && l.status !== 'no-show' && !isPast_(l.date, l.time)).length;

  const weekSlots = daysBetween_(monday, today) * (CONFIG.WORK_END - CONFIG.WORK_START);
  const utilization = {};
  CONFIG.INSTRUCTORS.forEach(inst => {
    const booked = weekLessons.filter(l => l.instructor === inst && !isCancelled_(l.status)).length;
    utilization[inst] = {
      booked, total: weekSlots || 1,
      percent: weekSlots > 0 ? Math.round((booked / weekSlots) * 100) : 0,
      todayCount: todaysLessons.filter(l => l.instructor === inst && !isCancelled_(l.status)).length,
    };
  });

  const tomorrowByInstructor = {};
  CONFIG.INSTRUCTORS.forEach(i => { tomorrowByInstructor[i] = []; });
  tomorrowLessons.filter(l => !isCancelled_(l.status)).forEach(l => {
    const inst = l.instructor || 'Other';
    if (!tomorrowByInstructor[inst]) tomorrowByInstructor[inst] = [];
    tomorrowByInstructor[inst].push({ time: l.time, student: l.student });
  });
  const tomorrowTotal = tomorrowLessons.filter(l => !isCancelled_(l.status)).length;

  return {
    timestamp: now.toISOString(),
    revenue: { today: revenueToday, week: revenueWeek, month: revenueMonth, lastMonth: revenueLastMonth, trend: monthTrend, netMonth: revenueMonth - expensesMonth },
    expenses: { month: expensesMonth },
    students: { active: students.total, total: students.total, newWeek: students.newThisWeek, newMonth: students.newThisMonth, fiveHourMonth: fiveHourMonth, recent: students.recent },
    lessons: { completedToday, noShowsToday, remainingToday, weekTotal: weekLessons.filter(l => !isCancelled_(l.status)).length },
    todaySchedule: todaysLessons.filter(l => !isCancelled_(l.status)),
    utilization,
    paymentHealth: getPaymentHealth_(),
    tomorrow: { byInstructor: tomorrowByInstructor, total: tomorrowTotal, open: (CONFIG.INSTRUCTORS.length * (CONFIG.WORK_END - CONFIG.WORK_START)) - tomorrowTotal },
    overdue: getOverdueStudents_(),
    dayName: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][now.getDay()],
    tomorrowDay: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][(now.getDay() + 1) % 7],
  };
}


// ================================================================
//  DATA HELPERS
// ================================================================

function getBookingsSheet_() {
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  if (CONFIG.BOOKINGS_SHEET_TAB) { const s = ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB); if (s) return s; }
  return ss.getSheets()[0];
}

function getRegistrationSheet_() {
  const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  if (CONFIG.REGISTRATION_SHEET_TAB) { const s = ss.getSheetByName(CONFIG.REGISTRATION_SHEET_TAB); if (s) return s; }
  return ss.getSheets()[0];
}

function getSignupSheet_() {
  const ss = SpreadsheetApp.openById(CONFIG.SIGNUP_SHEET_ID);
  if (CONFIG.SIGNUP_SHEET_TAB) { const s = ss.getSheetByName(CONFIG.SIGNUP_SHEET_TAB); if (s) return s; }
  return ss.getSheets()[0];
}

function getAllPayments_() {
  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  const sheet = ss.getSheetByName('Payments') || ss.getSheets()[0];
  if (!sheet || sheet.getLastRow() <= 1) return [];
  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const dateCol = findCol_(headers, ['date', 'payment date', 'timestamp']);
  const amountCol = findCol_(headers, ['amount', 'payment amount']);
  const studentCol = findCol_(headers, ['student', 'student name', 'name']);
  const methodCol = findCol_(headers, ['method', 'payment method']);
  if (dateCol === -1 || amountCol === -1) return [];
  const out = [];
  for (let i = 1; i < data.length; i++) {
    const date = data[i][dateCol] instanceof Date ? data[i][dateCol] : new Date(data[i][dateCol]);
    if (isNaN(date.getTime())) continue;
    out.push({ date, amount: parseFloat(data[i][amountCol]) || 0, student: String(data[i][studentCol] || '').trim(), method: methodCol !== -1 ? String(data[i][methodCol] || '').trim() : '' });
  }
  return out;
}

function getAllExpenses_() {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.EXPENSE_TRACKER_ID);
    const sheet = ss.getSheets()[0];
    if (!sheet || sheet.getLastRow() <= 1) return [];
    const data = sheet.getDataRange().getValues();
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const dateCol = findCol_(headers, ['date', 'timestamp', 'expense date']);
    const amountCol = findCol_(headers, ['amount', 'expense amount']);
    if (dateCol === -1 || amountCol === -1) return [];
    const out = [];
    for (let i = 1; i < data.length; i++) {
      const date = data[i][dateCol] instanceof Date ? data[i][dateCol] : new Date(data[i][dateCol]);
      if (isNaN(date.getTime())) continue;
      out.push({ date, amount: parseFloat(data[i][amountCol]) || 0 });
    }
    return out;
  } catch (e) { Logger.log('Expense error: ' + e.message); return []; }
}

function sumInRange_(items, start, end) {
  return items.filter(p => p.date >= start && p.date <= end).reduce((s, p) => s + p.amount, 0);
}

function getAllBookings_() {
  const sheet = getBookingsSheet_();
  if (!sheet || sheet.getLastRow() <= 1) return [];
  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const dateCol = findCol_(headers, ['date', 'lesson date', 'booking date']);
  const timeCol = findCol_(headers, ['time', 'lesson time', 'start time', 'time slot']);
  const studentCol = findCol_(headers, ['student', 'student name', 'name']);
  const instructorCol = findCol_(headers, ['instructor', 'instructor name']);
  const statusCol = findCol_(headers, ['status', 'booking status']);
  if (dateCol === -1) return [];
  const out = [];
  for (let i = 1; i < data.length; i++) {
    const date = data[i][dateCol] instanceof Date ? data[i][dateCol] : new Date(data[i][dateCol]);
    if (isNaN(date.getTime())) continue;
    out.push({
      date, time: String(data[i][timeCol] || '').trim(),
      student: String(data[i][studentCol] || '').trim(),
      instructor: String(data[i][instructorCol] || '').trim(),
      status: String(data[i][statusCol] || '').trim().toLowerCase(),
    });
  }
  return out;
}

function filterBookings_(bookings, start, end) { return bookings.filter(b => b.date >= start && b.date <= end); }

function getStudentData_() {
  const sheet = getRegistrationSheet_();
  if (!sheet || sheet.getLastRow() <= 1) return { total: 0, newThisWeek: 0, newThisMonth: 0, recent: [] };
  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const nameCol = findCol_(headers, ['full name', 'name', 'student name']);
  const tsCol = findCol_(headers, ['timestamp', 'date', 'submitted']);
  const packageCol = findCol_(headers, ['lesson package', 'package', 'class type', 'selected package']);
  if (nameCol === -1) return { total: 0, newThisWeek: 0, newThisMonth: 0, recent: [] };
  const now = new Date();
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const monday = new Date(today); const dow = monday.getDay(); monday.setDate(monday.getDate() - (dow === 0 ? 6 : dow - 1));
  const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
  const sevenDaysAgo = new Date(today); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
  let total = 0, newWeek = 0, newMonth = 0;
  const recent = [];
  for (let i = 1; i < data.length; i++) {
    const name = String(data[i][nameCol] || '').trim();
    if (!name) continue;
    total++;
    if (tsCol !== -1) {
      const date = data[i][tsCol] instanceof Date ? data[i][tsCol] : new Date(data[i][tsCol]);
      if (!isNaN(date.getTime())) {
        if (date >= monday) newWeek++;
        if (date >= monthStart) newMonth++;
        if (date >= sevenDaysAgo) {
          recent.push({ name, package: packageCol !== -1 ? String(data[i][packageCol] || '').trim() : '', date: formatDateShort_(date) });
        }
      }
    }
  }
  return { total, newThisWeek: newWeek, newThisMonth: newMonth, recent: recent.slice(-5).reverse() };
}

function get5HourSignups_(start, end) {
  const sheet = getSignupSheet_();
  if (!sheet || sheet.getLastRow() <= 1) return 0;
  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const tsCol = findCol_(headers, ['timestamp', 'date']);
  if (tsCol === -1) return 0;
  let count = 0;
  for (let i = 1; i < data.length; i++) {
    const date = data[i][tsCol] instanceof Date ? data[i][tsCol] : new Date(data[i][tsCol]);
    if (!isNaN(date.getTime()) && date >= start && date <= end) count++;
  }
  return count;
}

function getPaymentHealth_() {
  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  const sheet = ss.getSheetByName('Student Balances');
  if (!sheet || sheet.getLastRow() <= 1) return { totalOwed: 0, totalPaid: 0, studentsOwing: 0, studentsPaid: 0 };
  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const balCol = findCol_(headers, ['balance', 'remaining balance', 'amount due']);
  const paidCol = findCol_(headers, ['total paid', 'paid']);
  if (balCol === -1) return { totalOwed: 0, totalPaid: 0, studentsOwing: 0, studentsPaid: 0 };
  let totalOwed = 0, totalPaid = 0, studentsOwing = 0, studentsPaid = 0;
  for (let i = 1; i < data.length; i++) {
    const bal = parseFloat(data[i][balCol]) || 0;
    const paid = paidCol !== -1 ? (parseFloat(data[i][paidCol]) || 0) : 0;
    totalPaid += paid;
    if (bal > 0) { totalOwed += bal; studentsOwing++; } else { studentsPaid++; }
  }
  return { totalOwed, totalPaid, studentsOwing, studentsPaid };
}

function getOverdueStudents_() {
  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  const sheet = ss.getSheetByName('Student Balances');
  if (!sheet || sheet.getLastRow() <= 1) return [];
  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const nameCol = findCol_(headers, ['student', 'student name', 'name']);
  const balCol = findCol_(headers, ['balance', 'remaining balance', 'amount due']);
  const lastPayCol = findCol_(headers, ['last payment', 'last payment date']);
  if (nameCol === -1 || balCol === -1) return [];
  const overdue = [];
  const now = new Date();
  for (let i = 1; i < data.length; i++) {
    const bal = parseFloat(data[i][balCol]) || 0;
    if (bal <= 0) continue;
    if (lastPayCol === -1) continue;
    const lastPay = data[i][lastPayCol] instanceof Date ? data[i][lastPayCol] : new Date(data[i][lastPayCol]);
    if (!isNaN(lastPay.getTime()) && (now - lastPay) > 14 * 86400000) {
      overdue.push({ name: String(data[i][nameCol] || '').trim(), balance: bal, days: Math.floor((now - lastPay) / 86400000) });
    }
  }
  return overdue.sort((a, b) => b.days - a.days);
}


// ================================================================
//  UTILITIES
// ================================================================

function findCol_(headers, candidates) {
  for (const c of candidates) {
    const idx = headers.findIndex(h => h.includes(c.toLowerCase()));
    if (idx !== -1) return idx;
  }
  return -1;
}

function isCancelled_(status) {
  const s = String(status || '').toLowerCase().trim();
  return s === 'cancelled' || s === 'canceled';
}

function isPast_(date, timeStr) {
  const now = new Date();
  const t = parseTime_(timeStr);
  const lessonEnd = new Date(date);
  lessonEnd.setHours(Math.floor(t / 60) + 1, t % 60, 0);
  return now > lessonEnd;
}

function parseTime_(s) {
  if (!s) return 0;
  const m = s.toString().match(/(\d{1,2}):?(\d{2})?\s*(AM|PM)?/i);
  if (!m) return 0;
  let h = parseInt(m[1]);
  const min = parseInt(m[2] || '0');
  const p = (m[3] || '').toUpperCase();
  if (p === 'PM' && h < 12) h += 12;
  if (p === 'AM' && h === 12) h = 0;
  return h * 60 + min;
}

function daysBetween_(a, b) { return Math.max(1, Math.ceil((b - a) / 86400000) + 1); }

function formatDateShort_(d) {
  if (!(d instanceof Date)) d = new Date(d);
  if (isNaN(d.getTime())) return '';
  return (d.getMonth() + 1) + '/' + d.getDate();
}

function esc_(s) {
  if (!s) return '';
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}


// ================================================================
//  DASHBOARD HTML
// ================================================================

function getDashboardHTML_() {
  const sn = esc_(CONFIG.SCHOOL_NAME);
  const demoHint = CONFIG.DEMO_MODE ? ' (Demo PIN: 7777 or 0000)' : '';

  return '<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n' +
'<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">\n' +
'<title>Business Intelligence โ€” ' + sn + '</title>\n' +
'<style>\n' +
'*{margin:0;padding:0;box-sizing:border-box}\n' +
'body{background:#000;color:#ccc;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","Segoe UI",Roboto,sans-serif;min-height:100vh;-webkit-font-smoothing:antialiased;overflow-x:hidden}\n' +
'.app{max-width:500px;margin:0 auto;padding:16px 16px 80px}\n' +

// PIN screen
'.pin-screen{text-align:center;padding:60px 20px}\n' +
'.pin-screen .icon{font-size:48px;margin-bottom:20px}\n' +
'.pin-screen h2{color:#fff;font-size:22px;font-weight:700;margin-bottom:8px}\n' +
'.pin-screen p{color:#555;font-size:14px;margin-bottom:24px}\n' +
'.pin-input{background:#0a0a0a;border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:16px;color:#fff;font-size:24px;text-align:center;letter-spacing:8px;width:180px;outline:none}\n' +
'.pin-input:focus{border-color:#ff2d2d}\n' +
'.pin-btn{background:linear-gradient(135deg,#ff2d2d,#cc0000);color:#fff;border:none;border-radius:12px;padding:14px 32px;font-size:15px;font-weight:600;cursor:pointer;margin-top:16px;box-shadow:0 4px 16px rgba(255,45,45,0.3)}\n' +
'.pin-error{color:#ff2d2d;font-size:13px;margin-top:12px;display:none}\n' +

// Header
'.hdr{padding:20px 0 16px;text-align:center;border-bottom:1px solid rgba(255,255,255,0.06);margin-bottom:20px}\n' +
'.hdr .tag{display:inline-block;background:rgba(255,45,45,0.06);border:1px solid rgba(255,45,45,0.12);color:#ff2d2d;font-size:9px;letter-spacing:2px;text-transform:uppercase;font-weight:700;padding:4px 12px;border-radius:20px;margin-bottom:10px}\n' +
'.hdr h1{font-size:22px;font-weight:800;color:#fff;letter-spacing:-0.5px}\n' +
'.hdr .sub{font-size:12px;color:#555;margin-top:4px}\n' +
'.hdr .live{display:inline-flex;align-items:center;gap:5px;font-size:10px;color:#22c55e;margin-top:8px}\n' +
'.hdr .live .dot{width:6px;height:6px;background:#22c55e;border-radius:50%;animation:blink 2s infinite}\n' +
'@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}\n' +

// Loading
'.loading{text-align:center;padding:60px 0}\n' +
'.spin{width:32px;height:32px;border:3px solid rgba(255,255,255,0.06);border-top-color:#ff2d2d;border-radius:50%;animation:sp .7s linear infinite;margin:0 auto 12px}\n' +
'@keyframes sp{to{transform:rotate(360deg)}}\n' +

// Stats grid
'.stats{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}\n' +
'.stat{background:#0d0d0d;border:1px solid rgba(255,255,255,0.06);border-radius:16px;padding:16px;text-align:center}\n' +
'.stat .ico{font-size:22px;margin-bottom:6px}\n' +
'.stat .val{font-size:24px;font-weight:800;color:#fff}\n' +
'.stat .lbl{font-size:10px;color:#555;text-transform:uppercase;letter-spacing:1px;margin-top:3px}\n' +
'.stat .sub{font-size:11px;margin-top:6px}\n' +

// Cards
'.card{background:#0d0d0d;border:1px solid rgba(255,255,255,0.06);border-radius:16px;margin-bottom:14px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,0.3)}\n' +
'.card-h{padding:14px 18px;border-bottom:1px solid rgba(255,255,255,0.06);display:flex;align-items:center;justify-content:space-between}\n' +
'.card-h h3{font-size:13px;font-weight:600;color:#fff}\n' +
'.card-h .badge{font-size:10px;background:rgba(255,45,45,0.06);color:#ff2d2d;padding:3px 9px;border-radius:8px;font-weight:700}\n' +
'.card-b{padding:14px 18px}\n' +

// Bars
'.bar-row{display:flex;align-items:center;gap:10px;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.02)}\n' +
'.bar-row:last-child{border:none}\n' +
'.bar-name{font-size:13px;color:#ccc;width:65px;flex-shrink:0}\n' +
'.bar-track{flex:1;height:8px;background:rgba(255,255,255,0.04);border-radius:4px;overflow:hidden}\n' +
'.bar-fill{height:100%;border-radius:4px}\n' +
'.bar-val{font-size:13px;font-weight:700;color:#fff;width:40px;text-align:right}\n' +

// Rows
'.row{display:flex;align-items:center;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.02);gap:10px}\n' +
'.row:last-child{border:none}\n' +
'.row .time{font-size:12px;color:#555;width:55px;flex-shrink:0}\n' +
'.row .name{font-size:13px;color:#fff;flex:1}\n' +
'.row .inst{font-size:11px;color:#555}\n' +

// Alerts
'.alert{background:rgba(255,45,45,0.04);border:1px solid rgba(255,45,45,0.12);border-radius:12px;padding:12px 14px;margin-bottom:8px;display:flex;align-items:center;gap:10px}\n' +
'.alert .a-text{font-size:12px;color:#ccc;flex:1}\n' +
'.alert .a-val{font-size:13px;font-weight:700;color:#ff2d2d}\n' +

// Instructor blocks
'.inst-block{margin-bottom:12px}\n' +
'.inst-head{font-size:11px;color:#ff2d2d;text-transform:uppercase;letter-spacing:1px;font-weight:700;padding:8px 0 4px;border-bottom:1px solid rgba(255,255,255,0.06)}\n' +
'.inst-slot{display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid rgba(255,255,255,0.02);font-size:12px}\n' +
'.inst-slot .t{color:#555}\n' +
'.inst-slot .n{color:#fff}\n' +

// Revenue grid
'.rev-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:12px}\n' +
'.rev-item{text-align:center;padding:10px 0}\n' +
'.rev-item .rv{font-size:20px;font-weight:800;color:#22c55e}\n' +
'.rev-item .rl{font-size:9px;color:#555;text-transform:uppercase;letter-spacing:1px;margin-top:3px}\n' +

// Misc
'.trend-up{color:#22c55e}.trend-down{color:#ff2d2d}.trend-flat{color:#f59e0b}\n' +
'.refresh{position:fixed;bottom:20px;right:20px;width:48px;height:48px;background:linear-gradient(135deg,#ff2d2d,#cc0000);border:none;border-radius:50%;color:#fff;font-size:20px;cursor:pointer;box-shadow:0 4px 20px rgba(255,45,45,0.3);z-index:100;display:none}\n' +
'.empty{text-align:center;padding:16px;color:#333;font-size:12px}\n' +
'.signup-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.02)}\n' +
'.signup-row:last-child{border:none}\n' +
'.signup-name{font-size:13px;color:#fff}\n' +
'.signup-pkg{font-size:11px;color:#3b82f6;font-weight:600}\n' +
'.signup-date{font-size:10px;color:#333}\n' +

'@media(max-width:400px){.rev-item .rv{font-size:16px}.stat .val{font-size:20px}}\n' +
'</style>\n</head>\n<body>\n' +
'<div class="app">\n' +

// PIN Screen
'<div id="pinScreen" class="pin-screen">\n' +
'<span class="icon">๐Ÿ”</span>\n' +
'<h2>' + sn + '</h2>\n' +
'<p>Enter your access PIN to view the dashboard.' + esc_(demoHint) + '</p>\n' +
'<input type="password" id="pinInput" class="pin-input" maxlength="4" inputmode="numeric" pattern="[0-9]*" placeholder="โ€ขโ€ขโ€ขโ€ข" onkeypress="if(event.key===\'Enter\')checkPin()">\n' +
'<br><button class="pin-btn" onclick="checkPin()">Access Dashboard</button>\n' +
'<div class="pin-error" id="pinError"></div>\n' +
'</div>\n' +

// Dashboard (hidden until PIN verified)
'<div id="dashboard" style="display:none">\n' +
'<div class="hdr">\n' +
'<div class="tag">Business Intelligence</div>\n' +
'<h1>' + sn + '</h1>\n' +
'<div class="sub" id="dateDisplay"></div>\n' +
'<div class="live"><span class="dot"></span> Live Data</div>\n' +
'<div id="lastUpdated" style="font-size:12px;color:rgba(255,255,255,0.45);margin-top:6px"></div>\n' +
'</div>\n' +
'<div id="content"><div class="loading"><div class="spin"></div><div style="color:#555;font-size:13px">Loading dashboard...</div></div></div>\n' +
'</div>\n' +
'</div>\n' +
'<button class="refresh" id="refreshBtn" onclick="loadData()" title="Refresh">&#8635;</button>\n' +

'<script>\n' +
'function $(id){return document.getElementById(id)}\n' +
'function esc(s){if(s==null)return"";var d=document.createElement("div");d.appendChild(document.createTextNode(String(s)));return d.innerHTML}\n' +

// PIN check
'function checkPin(){\n' +
'var pin=$("pinInput").value.trim();\n' +
'if(!pin||pin.length!==4){showPinError("Enter a 4-digit PIN.");return}\n' +
'$("pinInput").disabled=true;\n' +
'google.script.run.withSuccessHandler(function(r){\n' +
'$("pinInput").disabled=false;\n' +
'if(r&&r.verified){$("pinScreen").style.display="none";$("dashboard").style.display="block";$("refreshBtn").style.display="block";initDashboard()}\n' +
'else{showPinError("Incorrect PIN. Try again.")}\n' +
'}).withFailureHandler(function(){$("pinInput").disabled=false;showPinError("Error verifying. Try again.")}).verifyPin(pin)}\n' +

'function showPinError(msg){var e=$("pinError");e.textContent=msg;e.style.display="block"}\n' +

// Dashboard init
'function initDashboard(){\n' +
'var now=new Date();var opts={weekday:"long",month:"long",day:"numeric",year:"numeric"};\n' +
'$("dateDisplay").textContent=now.toLocaleDateString("en-US",opts)+" \\u2022 "+now.toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"});\n' +
'loadData()}\n' +

'function loadData(){\n' +
'$("content").innerHTML=\'<div class="loading"><div class="spin"></div><div style="color:#555;font-size:13px">Refreshing...</div></div>\';\n' +
'google.script.run.withSuccessHandler(render).withFailureHandler(err).getDashboardData()}\n' +

'function err(e){$("content").innerHTML=\'<div class="empty" style="color:#ff2d2d">Error loading data. Tap refresh to try again.</div>\'}\n' +

// Render
'function render(d){\n' +
'if(d&&d.error){$("content").innerHTML=\'<div class="empty" style="color:#ff2d2d">\'+esc(d.message||"Error")+\'. Tap refresh.</div>\';return}\n' +
'var trend=parseFloat(d.revenue.trend)||0;\n' +
'var tClass=trend>0?"trend-up":trend<0?"trend-down":"trend-flat";\n' +
'var tIcon=trend>0?"\\u2191":trend<0?"\\u2193":"\\u2192";\n' +
'var h="";\n' +

// Revenue card
'h+=\'<div class="card"><div class="card-h"><h3>\\uD83D\\uDCB0 Revenue</h3><span class="badge \'+tClass+\'">\'+tIcon+" "+esc(Math.abs(trend))+"% vs last month</span></div><div class=\\"card-b\\">";\n' +
'h+=\'<div class="rev-grid">\';\n' +
'h+=revItem(d.revenue.today,"Today")+revItem(d.revenue.week,"This Week")+revItem(d.revenue.month,"This Month");\n' +
'h+="</div>";\n' +
'h+=\'<div style="display:flex;justify-content:space-between;padding:8px 0;border-top:1px solid rgba(255,255,255,.04)"><span style="font-size:12px;color:#555">Expenses this month</span><span style="font-size:12px;color:#ff2d2d;font-weight:600">-$\'+esc((d.expenses.month||0).toLocaleString())+"</span></div>";\n' +
'var nc=(d.revenue.netMonth||0)>=0?"#22c55e":"#ff2d2d";\n' +
'h+=\'<div style="display:flex;justify-content:space-between;padding:8px 0"><span style="font-size:12px;color:#555">Net profit</span><span style="font-size:13px;color:\'+nc+\';font-weight:700">\'+((d.revenue.netMonth||0)>=0?"+":"")+\'$\'+esc((d.revenue.netMonth||0).toLocaleString())+"</span></div></div></div>";\n' +

// Stats
'h+=\'<div class="stats">\';\n' +
'h+=statCard("\\uD83D\\uDCDA",(d.lessons.completedToday||0)+(d.lessons.remainingToday||0),"Lessons Today",esc(d.lessons.remainingToday||0)+" remaining");\n' +
'h+=statCard("\\uD83C\\uDD95",d.students.newWeek||0,"New This Week",esc(d.students.newMonth||0)+" this month");\n' +
'h+=statCard("\\uD83D\\uDC65",d.students.active||0,"Total Students",esc(d.students.fiveHourMonth||0)+" 5-hr this month");\n' +
'var nsc=(d.lessons.noShowsToday||0)>0?"color:#ff2d2d":"color:#22c55e";\n' +
'h+=statCard("\\uD83D\\uDCCA",d.lessons.weekTotal||0,"Week Lessons",(d.lessons.noShowsToday||0)>0?esc(d.lessons.noShowsToday)+" no-show today":"No no-shows today",nsc);\n' +
'h+="</div>";\n' +

// Utilization
'h+=\'<div class="card"><div class="card-h"><h3>Instructor Utilization</h3><span class="badge">THIS WEEK</span></div><div class="card-b">\';\n' +
'Object.keys(d.utilization||{}).forEach(function(n){var u=d.utilization[n];var c=u.percent>60?"#22c55e":u.percent>30?"#f59e0b":"#ff2d2d";\n' +
'h+=\'<div class="bar-row"><span class="bar-name">\'+esc(n)+\'</span><div class="bar-track"><div class="bar-fill" style="width:\'+Math.max(3,u.percent||0)+\'%;background:\'+c+\'"></div></div><span class="bar-val">\'+esc(u.percent||0)+\'%</span></div>\'});\n' +
'h+="</div></div>";\n' +

// Today
'h+=\'<div class="card"><div class="card-h"><h3>Today \\u2014 \'+esc(d.dayName||"")+"</h3></div><div class=\\"card-b\\">";\n' +
'if(d.todaySchedule&&d.todaySchedule.length>0){d.todaySchedule.forEach(function(l){var p=isPastC(l.time);\n' +
'h+=\'<div class="row"><span class="time">\'+esc(l.time)+\'</span><span class="name" style="\' +(p?"opacity:.5":"")+\'">\'+esc(l.student)+\'</span><span class="inst">\'+esc(l.instructor)+"</span></div>"})}\n' +
'else{h+=\'<div class="empty">No lessons today</div>\'}\n' +
'h+="</div></div>";\n' +

// Payment Health
'h+=\'<div class="card"><div class="card-h"><h3>Payment Health</h3></div><div class="card-b">\';\n' +
'var ph=d.paymentHealth||{};\n' +
'h+=\'<div style="display:flex;justify-content:space-around;text-align:center;margin-bottom:12px">\';\n' +
'h+=\'<div><div style="font-size:22px;font-weight:800;color:#22c55e">$\'+esc((ph.totalPaid||0).toLocaleString())+\'</div><div style="font-size:10px;color:#555;margin-top:3px">COLLECTED</div></div>\';\n' +
'h+=\'<div><div style="font-size:22px;font-weight:800;color:\'+((ph.totalOwed||0)>0?"#f59e0b":"#22c55e")+\'">$\'+esc((ph.totalOwed||0).toLocaleString())+\'</div><div style="font-size:10px;color:#555;margin-top:3px">OUTSTANDING</div></div>\';\n' +
'h+="</div>";\n' +
'h+=\'<div style="display:flex;justify-content:space-around;text-align:center;padding-top:10px;border-top:1px solid rgba(255,255,255,.04)">\';\n' +
'h+=\'<div style="font-size:12px"><span style="color:#22c55e;font-weight:700">\'+esc(ph.studentsPaid||0)+\'</span> <span style="color:#555">paid up</span></div>\';\n' +
'h+=\'<div style="font-size:12px"><span style="color:#f59e0b;font-weight:700">\'+esc(ph.studentsOwing||0)+\'</span> <span style="color:#555">with balance</span></div>\';\n' +
'h+="</div></div></div>";\n' +

// Tomorrow
'h+=\'<div class="card"><div class="card-h"><h3>Tomorrow \\u2014 \'+esc(d.tomorrowDay||"")+\'</h3><span class="badge">\'+esc(d.tomorrow.total||0)+\' booked \\u00B7 \'+esc(d.tomorrow.open||0)+\' open</span></div><div class="card-b">\';\n' +
'if((d.tomorrow.total||0)>0&&d.tomorrow.byInstructor){Object.keys(d.tomorrow.byInstructor).forEach(function(inst){\n' +
'var ls=d.tomorrow.byInstructor[inst];\n' +
'h+=\'<div class="inst-block"><div class="inst-head">\'+esc(inst)+" ("+ls.length+")</div>";\n' +
'if(ls.length>0){ls.forEach(function(l){h+=\'<div class="inst-slot"><span class="t">\'+esc(l.time)+\'</span><span class="n">\'+esc(l.student)+"</span></div>"})}\n' +
'else{h+=\'<div style="font-size:11px;color:#333;padding:6px 0">No lessons booked</div>\'}\n' +
'h+="</div>"})}else{h+=\'<div class="empty">No lessons scheduled tomorrow</div>\'}\n' +
'h+="</div></div>";\n' +

// Recent signups
'if(d.students.recent&&d.students.recent.length>0){\n' +
'h+=\'<div class="card"><div class="card-h"><h3>Recent Signups</h3><span class="badge">LAST 7 DAYS</span></div><div class="card-b">\';\n' +
'd.students.recent.forEach(function(s){h+=\'<div class="signup-row"><div><div class="signup-name">\'+esc(s.name)+\'</div><div class="signup-date">\'+esc(s.date)+\'</div></div><div class="signup-pkg">\'+esc(s.package)+"</div></div>"});\n' +
'h+="</div></div>"}\n' +

// Overdue
'if(d.overdue&&d.overdue.length>0){\n' +
'h+=\'<div class="card"><div class="card-h"><h3>\\u26A0\\uFE0F Overdue Payments</h3></div><div class="card-b">\';\n' +
'd.overdue.forEach(function(o){h+=\'<div class="alert"><span style="font-size:18px">\\uD83D\\uDD34</span><span class="a-text">\'+esc(o.name)+\' <span style="color:#333">(\'+esc(o.days)+" days)</span></span><span class=\\"a-val\\">$"+esc((o.balance||0).toFixed(0))+"</span></div>"});\n' +
'h+="</div></div>"}\n' +

// Footer
'h+=\'<div style="text-align:center;padding:20px 0;font-size:11px;color:#333">Last refreshed: \'+esc(new Date().toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit",second:"2-digit"}))+"</div>";\n' +
'$("content").innerHTML=h;\n' +
'var u=$("lastUpdated");if(u)u.textContent="Data pulled "+new Date().toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"})}\n' +

// Helpers
'function statCard(ico,val,lbl,sub,ss){return\'<div class="stat"><div class="ico">\'+ico+\'</div><div class="val">\'+esc(val)+\'</div><div class="lbl">\'+esc(lbl)+\'</div><div class="sub" style="\'+(ss||"color:#555")+\'">\'+sub+"</div></div>"}\n' +
'function revItem(val,lbl){return\'<div class="rev-item"><div class="rv">$\'+esc((val||0).toLocaleString())+\'</div><div class="rl">\'+esc(lbl)+"</div></div>"}\n' +
'function isPastC(ts){var now=new Date();var m=(ts||"").match(/(\\d{1,2}):?(\\d{2})?\\s*(AM|PM)?/i);if(!m)return false;var h=parseInt(m[1],10);var p=(m[3]||"").toUpperCase();if(p==="PM"&&h<12)h+=12;if(p==="AM"&&h===12)h=0;return now.getHours()>=h+1}\n' +
'</script>\n</body>\n</html>';
}
๐Ÿ”” Saturday Class Reminders 24KB ยท 626 linesGSโ–ถ
/**
 * =========================================================
 *  SATURDAY 5-HOUR CLASS REMINDERS
 *  Flavors Driving School
 * =========================================================
 *  Sends SMS reminders (email-to-SMS gateway) + email fallback
 *  to students whose Saturday 5-Hour Class is tomorrow.
 *
 *  - Runs Friday at 6 PM via daily trigger
 *  - Multi-carrier SMS support (Verizon, AT&T, T-Mobile, Sprint, etc.)
 *  - Email fallback if no phone/carrier on file
 *  - Admin summary after each run
 *  - Reminder history log sheet
 *  - Demo mode for safe presentations
 * =========================================================
 */

const CONFIG = {
  // โ”€โ”€ Sheet IDs โ”€โ”€
  SIGNUP_SHEET_ID:    '1lE2_amcNj3-ao3TiP_U0VpaWHqmx4R-0rkF3n3xl7fM',
  REGISTRATION_ID:    '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',

  // โ”€โ”€ Sheet tabs (empty = first sheet) โ”€โ”€
  SIGNUP_SHEET_TAB:   '',
  REGISTRATION_SHEET_TAB: '',

  // โ”€โ”€ Branding โ”€โ”€
  SCHOOL_NAME:    'Flavors Driving School',
  SCHOOL_TAGLINE: 'Your Road to Freedom Starts Here',
  ADMIN_EMAIL:    '[email protected]',

  // โ”€โ”€ Class details โ”€โ”€
  CLASS_TIME:     '11:00 AM',
  CLASS_LOCATION: 'Flavors Driving School, Queens, NY',
  WHAT_TO_BRING:  'your learner permit and a valid photo ID',

  // โ”€โ”€ Timezone โ”€โ”€
  TIMEZONE: 'America/New_York',

  // โ”€โ”€ SMS carrier gateways โ”€โ”€
  CARRIER_GATEWAYS: {
    'verizon':  '@vtext.com',
    'att':      '@txt.att.net',
    'at&t':     '@txt.att.net',
    'tmobile':  '@tmomail.net',
    't-mobile': '@tmomail.net',
    'sprint':   '@messaging.sprintpcs.com',
    'metro':    '@mymetropcs.com',
    'metropcs': '@mymetropcs.com',
    'boost':    '@sms.myboostmobile.com',
    'cricket':  '@sms.cricketwireless.net',
    'uscellular': '@email.uscc.net',
    'us cellular': '@email.uscc.net',
    'mint':     '@tmomail.net',
    'visible':  '@vtext.com',
    'xfinity':  '@vtext.com',
    'google fi': '@msg.fi.google.com',
    'fi':       '@msg.fi.google.com'
  },

  // โ”€โ”€ Default gateway if carrier unknown โ”€โ”€
  DEFAULT_SMS_GATEWAY: '@vtext.com',

  // โ”€โ”€ Demo mode (true = no real messages, fake data for presentations) โ”€โ”€
  DEMO_MODE: true,

  // โ”€โ”€ Week-ahead pre-reminder (Monday before Saturday class) โ”€โ”€
  SEND_WEEK_AHEAD_REMINDER: true
};

/* ================================================================
   DEMO DATA
   ================================================================ */
const DEMO = {
  students: [
    { name: 'Sarah Johnson',   phone: '(917) 555-0123', carrier: 'Verizon',  email: '[email protected]',   classDate: 'next Saturday' },
    { name: 'Marcus Williams', phone: '(718) 555-0456', carrier: 'T-Mobile', email: '[email protected]',  classDate: 'next Saturday' },
    { name: 'Emily Chen',      phone: '',               carrier: '',          email: '[email protected]',   classDate: 'next Saturday' },
    { name: 'David Rodriguez', phone: '(646) 555-0789', carrier: 'AT&T',     email: '[email protected]',   classDate: 'next Saturday' }
  ],
  summary: {
    smsReminders: 3,
    emailFallbacks: 1,
    skipped: 0,
    failed: 0
  }
};

/* ================================================================
   SETUP & AUTH
   ================================================================ */

/** Run once to trigger OAuth for all connected services. */
function forceAuth() {
  SpreadsheetApp.openById(CONFIG.SIGNUP_SHEET_ID).getSheetByName('test_auth_ignore');
  SpreadsheetApp.openById(CONFIG.REGISTRATION_ID).getSheetByName('test_auth_ignore');
  MailApp.getRemainingDailyQuota();
  Logger.log('โœ… All permissions authorized. You can close this now.');
}

function fullSetup() {
  const ss = SpreadsheetApp.openById(CONFIG.SIGNUP_SHEET_ID);

  // โ”€โ”€ Create "Reminder Log" sheet โ”€โ”€
  let logSheet = ss.getSheetByName('Reminder Log');
  if (!logSheet) {
    logSheet = ss.insertSheet('Reminder Log');
    logSheet.appendRow([
      'Date Sent', 'Student Name', 'Phone', 'Carrier', 'Email', 'Class Date',
      'Method', 'Status', 'Message Preview'
    ]);
    logSheet.getRange('1:1').setFontWeight('bold');
    logSheet.setFrozenRows(1);
    Logger.log('โœ… Created "Reminder Log" sheet.');
  }

  // โ”€โ”€ Friday evening trigger (delete old first) โ”€โ”€
  ScriptApp.getProjectTriggers().forEach(t => {
    if (t.getHandlerFunction() === 'sendReminders' || t.getHandlerFunction() === 'sendWeekAheadReminders') {
      ScriptApp.deleteTrigger(t);
    }
  });

  // Daily at 6 PM โ€” sendReminders checks if tomorrow is the class date
  ScriptApp.newTrigger('sendReminders')
    .timeBased()
    .everyDays(1)
    .atHour(18)
    .create();
  Logger.log('โœ… Daily trigger set: sendReminders at 6 PM.');

  // Monday at 10 AM โ€” week-ahead heads up
  if (CONFIG.SEND_WEEK_AHEAD_REMINDER) {
    ScriptApp.newTrigger('sendWeekAheadReminders')
      .timeBased()
      .everyDays(1)
      .atHour(10)
      .create();
    Logger.log('โœ… Daily trigger set: sendWeekAheadReminders at 10 AM.');
  }

  Logger.log('โœ… Class Reminders setup complete.');
}

/* ================================================================
   MAIN: SEND REMINDERS (day-before)
   ================================================================ */

function sendReminders() {
  try {
    if (CONFIG.DEMO_MODE) {
      Logger.log('๐ŸŽญ DEMO MODE โ€” no real reminders sent.');
      Logger.log('Would send to: ' + DEMO.students.map(s => s.name).join(', '));
      Logger.log('Summary: ' + JSON.stringify(DEMO.summary));
      return;
    }
    runReminders_(1, 'day-before');
  } catch (e) {
    Logger.log('sendReminders error: ' + (e.message || e));
    notifyAdmin_('Class Reminders โ€” Error', 'sendReminders failed: ' + String(e.message || e).substring(0, 500));
    throw e;
  }
}

/** Week-ahead reminder โ€” runs daily but only fires on Monday for next Saturday's class. */
function sendWeekAheadReminders() {
  try {
    if (CONFIG.DEMO_MODE) {
      Logger.log('๐ŸŽญ DEMO MODE โ€” no week-ahead reminders sent.');
      return;
    }
    if (!CONFIG.SEND_WEEK_AHEAD_REMINDER) return;

    // Only run on Mondays
    const now = new Date();
    const day = parseInt(Utilities.formatDate(now, CONFIG.TIMEZONE, 'u'), 10); // 1=Mon
    if (day !== 1) {
      Logger.log('Not Monday โ€” skipping week-ahead reminder.');
      return;
    }

    runReminders_(5, 'week-ahead');
  } catch (e) {
    Logger.log('sendWeekAheadReminders error: ' + (e.message || e));
    notifyAdmin_('Class Reminders โ€” Error', 'sendWeekAheadReminders failed: ' + String(e.message || e).substring(0, 500));
    throw e;
  }
}

/* ================================================================
   CORE LOGIC
   ================================================================ */

function runReminders_(daysAhead, reminderType) {
  const tz = CONFIG.TIMEZONE;
  const targetDate = new Date();
  targetDate.setDate(targetDate.getDate() + daysAhead);
  const targetStr = Utilities.formatDate(targetDate, tz, 'M/d/yyyy');
  const targetDay = Utilities.formatDate(targetDate, tz, 'EEEE'); // e.g. "Saturday"

  // โ”€โ”€ Get signup data โ”€โ”€
  const ss = SpreadsheetApp.openById(CONFIG.SIGNUP_SHEET_ID);
  const sheet = CONFIG.SIGNUP_SHEET_TAB
    ? (ss.getSheetByName(CONFIG.SIGNUP_SHEET_TAB) || ss.getSheets()[0])
    : ss.getSheets()[0];
  if (!sheet || sheet.getLastRow() < 2) {
    Logger.log('No data in signup sheet.');
    return;
  }

  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h || '').toLowerCase().trim());

  const nameCol    = findCol_(headers, ['full name', 'name', 'student name']);
  const phoneCol   = findCol_(headers, ['phone', 'phone number', 'mobile', 'cell']);
  const emailCol   = findCol_(headers, ['email', 'student email', 'email address']);
  const dateCol    = findCol_(headers, ['saturday', 'class date', 'saturday date', 'date', 'scheduled date']);
  const carrierCol = findCol_(headers, ['carrier', 'phone carrier', 'mobile carrier', 'network']);
  const reminderCol = findCol_(headers, ['reminder sent', 'reminder', 'sent']);
  const unsubCol   = findCol_(headers, ['unsubscribe', 'opt out', 'opted out', 'sms opt out']);

  if (nameCol < 0 || dateCol < 0) {
    Logger.log('Missing required columns: need name and class date.');
    return;
  }

  // โ”€โ”€ Also check Registration sheet for phone/email/carrier if missing in signups โ”€โ”€
  const regLookup = buildRegistrationLookup_();

  // โ”€โ”€ Get or create log sheet โ”€โ”€
  let logSheet = ss.getSheetByName('Reminder Log');
  if (!logSheet) {
    fullSetup();
    logSheet = ss.getSheetByName('Reminder Log');
  }

  // โ”€โ”€ Cooldown: check what we already sent today โ”€โ”€
  const todayStr = Utilities.formatDate(new Date(), tz, 'M/d/yyyy');
  const alreadySent = new Set();
  if (logSheet && logSheet.getLastRow() > 1) {
    const logData = logSheet.getDataRange().getValues();
    for (let r = 1; r < logData.length; r++) {
      const logDate = logData[r][0];
      const logName = String(logData[r][1] || '').toLowerCase().trim();
      const logType = String(logData[r][7] || '').toLowerCase();
      if (logDate instanceof Date) {
        const logDateStr = Utilities.formatDate(logDate, tz, 'M/d/yyyy');
        if (logDateStr === todayStr && logType.includes('sent')) {
          alreadySent.add(logName);
        }
      }
    }
  }

  const stats = { sms: 0, email: 0, skipped: 0, failed: 0, optedOut: 0 };
  const sentThisRun = new Set();

  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    const name = sanitize_(String(row[nameCol] || '').trim());
    if (!name) continue;

    // โ”€โ”€ Check class date matches target โ”€โ”€
    const classDate = parseDate_(row[dateCol]);
    if (!classDate || isNaN(classDate.getTime())) continue;
    const classDateStr = Utilities.formatDate(classDate, tz, 'M/d/yyyy');
    if (classDateStr !== targetStr) continue;

    // โ”€โ”€ Check if already reminded โ”€โ”€
    const nameKey = name.toLowerCase().trim();
    if (alreadySent.has(nameKey)) { stats.skipped++; continue; }
    if (sentThisRun.has(nameKey)) { stats.skipped++; continue; }

    // Check sheet "Reminder Sent" column
    if (reminderCol >= 0) {
      const val = String(row[reminderCol] || '').toUpperCase().trim();
      if (reminderType === 'day-before' && val === 'YES') { stats.skipped++; continue; }
      if (reminderType === 'week-ahead' && (val === 'YES' || val === 'WEEK')) { stats.skipped++; continue; }
    }

    // โ”€โ”€ Check unsubscribe โ”€โ”€
    if (unsubCol >= 0) {
      const unsub = String(row[unsubCol] || '').toLowerCase().trim();
      if (unsub === 'yes' || unsub === 'true' || unsub === '1') { stats.optedOut++; continue; }
    }

    // โ”€โ”€ Get contact info (from signup row + registration fallback) โ”€โ”€
    let phone   = phoneCol >= 0 ? String(row[phoneCol] || '').trim() : '';
    let email   = emailCol >= 0 ? String(row[emailCol] || '').trim() : '';
    let carrier = carrierCol >= 0 ? String(row[carrierCol] || '').trim() : '';

    // Registration lookup for missing fields
    if (!phone || !email || !carrier) {
      const regInfo = regLookup[nameKey] || regLookup[email.toLowerCase()] || {};
      if (!phone && regInfo.phone) phone = regInfo.phone;
      if (!email && regInfo.email) email = regInfo.email;
      if (!carrier && regInfo.carrier) carrier = regInfo.carrier;
    }

    // โ”€โ”€ Build message โ”€โ”€
    const firstName = name.split(' ')[0] || name;
    const dayLabel = reminderType === 'week-ahead' ? 'this Saturday' : 'tomorrow (Saturday)';

    const smsMsg = 'Hi ' + firstName + '! Reminder: Your 5-Hour Pre-Licensing Class is '
      + dayLabel + ' at ' + CONFIG.CLASS_TIME + ' at ' + CONFIG.CLASS_LOCATION
      + '. Please bring ' + CONFIG.WHAT_TO_BRING + '. Arrive 10 min early. See you there! โ€” '
      + CONFIG.SCHOOL_NAME
      + '\n\nReply STOP to opt out of reminders.';

    // โ”€โ”€ Try SMS first, fall back to email โ”€โ”€
    const cleanPhone = cleanPhone_(phone);
    let method = '';
    let status = '';

    if (cleanPhone) {
      const gateway = resolveGateway_(carrier);
      const smsEmail = cleanPhone + gateway;
      try {
        MailApp.sendEmail({
          to: smsEmail,
          subject: '',
          body: smsMsg,
          name: CONFIG.SCHOOL_NAME
        });
        method = 'SMS (' + (carrier || 'default') + ')';
        status = 'Sent';
        stats.sms++;
      } catch (e) {
        Logger.log('SMS failed for ' + name + ', trying email fallback: ' + e.message);
        method = 'SMS Failed';
        status = 'Failed';
      }
    }

    // Email fallback if no phone or SMS failed
    if (status !== 'Sent' && email) {
      try {
        const htmlBody = buildReminderEmail_(firstName, dayLabel, classDateStr);
        MailApp.sendEmail({
          to: email,
          subject: 'Reminder: 5-Hour Class ' + dayLabel + ' โ€” ' + CONFIG.SCHOOL_NAME,
          body: smsMsg,
          htmlBody: htmlBody,
          name: CONFIG.SCHOOL_NAME
        });
        method = method === 'SMS Failed' ? 'Email (SMS failed)' : 'Email (no phone)';
        status = 'Sent';
        stats.email++;
      } catch (e) {
        Logger.log('Email also failed for ' + name + ': ' + e.message);
        method = method || 'Email';
        status = 'Failed';
        stats.failed++;
      }
    }

    if (status !== 'Sent' && !email && !cleanPhone) {
      method = 'None';
      status = 'No contact info';
      stats.failed++;
    }

    // โ”€โ”€ Log it โ”€โ”€
    if (logSheet) {
      logSheet.appendRow([
        new Date(), name, phone, carrier, email, classDateStr,
        method, status, smsMsg.substring(0, 100) + '...'
      ]);
    }

    // โ”€โ”€ Mark reminder sent in signup sheet โ”€โ”€
    if (status === 'Sent' && reminderCol >= 0) {
      const mark = reminderType === 'week-ahead' ? 'WEEK' : 'YES';
      sheet.getRange(i + 1, reminderCol + 1).setValue(mark);
    }

    sentThisRun.add(nameKey);
  }

  // โ”€โ”€ Admin summary โ”€โ”€
  const summaryText = '๐Ÿ“‹ Class Reminder Summary (' + reminderType + ')\n'
    + 'Target date: ' + targetDay + ' ' + targetStr + '\n\n'
    + '๐Ÿ“ฑ SMS sent: ' + stats.sms + '\n'
    + '๐Ÿ“ง Email fallbacks: ' + stats.email + '\n'
    + 'โญ๏ธ Already reminded: ' + stats.skipped + '\n'
    + '๐Ÿšซ Opted out: ' + stats.optedOut + '\n'
    + 'โŒ Failed: ' + stats.failed + '\n'
    + 'โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n'
    + 'Total reached: ' + (stats.sms + stats.email);

  Logger.log(summaryText);

  if (stats.sms + stats.email + stats.failed > 0) {
    sendAdminSummary_(stats, reminderType, targetStr);
  }
}

/* ================================================================
   REGISTRATION LOOKUP (for phone/email/carrier enrichment)
   ================================================================ */

function buildRegistrationLookup_() {
  const lookup = {};
  try {
    const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
    const sheet = CONFIG.REGISTRATION_SHEET_TAB
      ? (ss.getSheetByName(CONFIG.REGISTRATION_SHEET_TAB) || ss.getSheets()[0])
      : ss.getSheets()[0];
    if (!sheet || sheet.getLastRow() <= 1) return lookup;

    const data = sheet.getDataRange().getValues();
    const h = data[0].map(c => String(c || '').toLowerCase().trim());

    const nCol = findCol_(h, ['student name', 'full name', 'name']);
    const eCol = findCol_(h, ['email', 'student email', 'email address']);
    const pCol = findCol_(h, ['phone', 'phone number', 'mobile', 'cell']);
    const cCol = findCol_(h, ['carrier', 'phone carrier', 'mobile carrier', 'network']);

    for (let i = 1; i < data.length; i++) {
      const row = data[i];
      const info = {
        name:    nCol >= 0 ? String(row[nCol] || '').trim() : '',
        email:   eCol >= 0 ? String(row[eCol] || '').trim() : '',
        phone:   pCol >= 0 ? String(row[pCol] || '').trim() : '',
        carrier: cCol >= 0 ? String(row[cCol] || '').trim() : ''
      };
      if (info.name) lookup[info.name.toLowerCase()] = info;
      if (info.email) lookup[info.email.toLowerCase()] = info;
    }
  } catch (e) {
    Logger.log('Registration lookup failed: ' + e.message);
  }
  return lookup;
}

/* ================================================================
   SMS HELPERS
   ================================================================ */

function cleanPhone_(phone) {
  let digits = String(phone || '').replace(/\D/g, '');
  if (digits.length === 11 && digits[0] === '1') digits = digits.substring(1);
  return digits.length === 10 ? digits : '';
}

function resolveGateway_(carrier) {
  if (!carrier) return CONFIG.DEFAULT_SMS_GATEWAY;
  const key = String(carrier).toLowerCase().trim();
  return CONFIG.CARRIER_GATEWAYS[key] || CONFIG.DEFAULT_SMS_GATEWAY;
}

/* ================================================================
   EMAIL FALLBACK (Mission Control theme)
   ================================================================ */

function buildReminderEmail_(firstName, dayLabel, classDate) {
  const f   = esc_(firstName);
  const sch = esc_(CONFIG.SCHOOL_NAME);
  const tag = esc_(CONFIG.SCHOOL_TAGLINE);
  const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe%20Reminders&body=Please%20remove%20me%20from%20class%20reminder%20emails.';

  return '<!DOCTYPE html><html><head><meta charset="utf-8"></head>'
    + '<body style="margin:0;padding:0;background:#000;font-family:Arial,Helvetica,sans-serif;">'
    + '<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#000;"><tr><td align="center">'
    + '<table width="600" cellpadding="0" cellspacing="0" border="0">'
    // Header
    + '<tr><td style="background:#0d0d0d;border-bottom:2px solid #ff2d2d;padding:30px 24px;text-align:center;">'
    + '<h1 style="color:#fff;font-size:20px;margin:0 0 4px;">' + sch + '</h1>'
    + '<p style="color:rgba(255,255,255,0.5);font-size:12px;margin:0;">' + tag + '</p>'
    + '</td></tr>'
    // Body
    + '<tr><td style="background:#0d0d0d;padding:32px 24px;text-align:center;">'
    + '<div style="font-size:48px;margin-bottom:16px;">๐Ÿ“š</div>'
    + '<div style="font-size:24px;font-weight:800;color:#fff;margin-bottom:8px;">Class Reminder</div>'
    + '<p style="font-size:14px;color:rgba(255,255,255,0.6);line-height:1.7;max-width:440px;margin:0 auto 24px;">'
    + 'Hi <strong style="color:#fff;">' + f + '</strong>! Your <strong style="color:#ff2d2d;">5-Hour Pre-Licensing Class</strong> is '
    + '<strong style="color:#fff;">' + esc_(dayLabel) + '</strong>.</p>'
    // Details cards
    + '<table cellpadding="0" cellspacing="8" border="0" align="center"><tr>'
    + detailBox_('๐Ÿ•', 'Time', esc_(CONFIG.CLASS_TIME))
    + detailBox_('๐Ÿ“', 'Location', esc_(CONFIG.CLASS_LOCATION))
    + '</tr><tr>'
    + detailBox_('๐Ÿ“‹', 'Bring', esc_(CONFIG.WHAT_TO_BRING))
    + detailBox_('โฐ', 'Arrive', '10 minutes early')
    + '</tr></table>'
    + '</td></tr>'
    // Footer
    + '<tr><td style="background:#000;border-top:1px solid rgba(255,255,255,0.05);padding:20px 24px;text-align:center;">'
    + '<p style="font-size:11px;color:rgba(255,255,255,0.25);margin:2px 0;">' + sch + ' โ€” See you there!</p>'
    + '<p style="font-size:10px;color:rgba(255,255,255,0.15);margin:8px 0 0;"><a href="' + unsub + '" style="color:rgba(255,255,255,0.15);text-decoration:underline;">Unsubscribe from reminders</a></p>'
    + '</td></tr>'
    + '</table></td></tr></table></body></html>';
}

function detailBox_(emoji, label, value) {
  return '<td style="background:rgba(255,45,45,0.08);border:1px solid rgba(255,45,45,0.2);border-radius:12px;padding:14px 16px;text-align:center;width:50%;">'
    + '<div style="font-size:24px;margin-bottom:4px;">' + emoji + '</div>'
    + '<div style="font-size:10px;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:1px;">' + esc_(label) + '</div>'
    + '<div style="font-size:13px;color:#fff;margin-top:4px;font-weight:600;">' + value + '</div></td>';
}

/* ================================================================
   ADMIN SUMMARY EMAIL
   ================================================================ */

function sendAdminSummary_(stats, reminderType, targetDate) {
  if (!CONFIG.ADMIN_EMAIL) return;

  const label = reminderType === 'week-ahead' ? 'Week-Ahead' : 'Day-Before';
  const subject = CONFIG.SCHOOL_NAME + ' โ€” ' + label + ' Reminder Summary';
  const total = stats.sms + stats.email;

  const html = '<!DOCTYPE html><html><head><meta charset="utf-8"></head>'
    + '<body style="margin:0;padding:0;background:#000;font-family:Arial,Helvetica,sans-serif;">'
    + '<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#000;"><tr><td align="center">'
    + '<table width="600" cellpadding="0" cellspacing="0" border="0">'
    + '<tr><td style="background:#0d0d0d;border-bottom:2px solid #ff2d2d;padding:24px;text-align:center;">'
    + '<h1 style="color:#fff;font-size:18px;margin:0;">๐Ÿ“‹ ' + label + ' Reminder Summary</h1>'
    + '<p style="color:rgba(255,255,255,0.4);font-size:12px;margin:4px 0 0;">Class date: ' + esc_(targetDate) + '</p>'
    + '</td></tr>'
    + '<tr><td style="background:#0d0d0d;padding:24px;">'
    + '<table width="100%" cellpadding="0" cellspacing="8" border="0">'
    + summaryRow_('๐Ÿ“ฑ SMS Sent', stats.sms)
    + summaryRow_('๐Ÿ“ง Email Fallbacks', stats.email)
    + summaryRow_('โญ๏ธ Already Reminded', stats.skipped)
    + summaryRow_('๐Ÿšซ Opted Out', stats.optedOut)
    + summaryRow_('โŒ Failed', stats.failed)
    + '</table>'
    + '<div style="margin-top:16px;padding:16px;background:rgba(255,45,45,0.08);border:1px solid rgba(255,45,45,0.2);border-radius:12px;text-align:center;">'
    + '<div style="font-size:28px;font-weight:800;color:#ff2d2d;">' + total + '</div>'
    + '<div style="font-size:10px;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:1px;margin-top:4px;">Students Reached</div>'
    + '</div>'
    + '</td></tr>'
    + '<tr><td style="background:#000;padding:16px 24px;text-align:center;">'
    + '<p style="font-size:10px;color:rgba(255,255,255,0.2);">' + esc_(CONFIG.SCHOOL_NAME) + ' โ€” Automated Reminder System</p>'
    + '</td></tr>'
    + '</table></td></tr></table></body></html>';

  try {
    MailApp.sendEmail({
      to: CONFIG.ADMIN_EMAIL,
      subject: subject,
      body: '5-Hour Class Reminders (' + label + ') for ' + targetDate + ': ' + total + ' students reached (' + stats.sms + ' SMS, ' + stats.email + ' email). Failed: ' + stats.failed,
      htmlBody: html,
      name: CONFIG.SCHOOL_NAME
    });
  } catch (e) {
    Logger.log('Admin summary email failed: ' + e.message);
  }
}

function summaryRow_(label, value) {
  return '<tr><td style="padding:8px 12px;background:rgba(255,255,255,0.03);border-radius:8px;">'
    + '<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr>'
    + '<td style="color:rgba(255,255,255,0.6);font-size:13px;">' + label + '</td>'
    + '<td align="right" style="color:#fff;font-size:14px;font-weight:700;">' + value + '</td>'
    + '</tr></table></td></tr>';
}

function notifyAdmin_(subject, body) {
  if (!CONFIG.ADMIN_EMAIL) return;
  try {
    MailApp.sendEmail(CONFIG.ADMIN_EMAIL, subject, body, { name: CONFIG.SCHOOL_NAME });
  } catch (_) { /* silent */ }
}

/* ================================================================
   MANUAL TOOLS
   ================================================================ */

/** Test: run a demo reminder check (logs only, no real sends). */
function testDemoReminders() {
  const wasDemoMode = CONFIG.DEMO_MODE;
  // Force demo mode for safety
  Logger.log('๐ŸŽญ Running demo test...');
  Logger.log('Students who would receive reminders:');
  DEMO.students.forEach((s, i) => {
    const method = s.phone ? 'SMS (' + (s.carrier || 'default') + ')' : 'Email fallback';
    Logger.log('  ' + (i + 1) + '. ' + s.name + ' โ€” ' + method);
  });
  Logger.log('\nSummary: ' + DEMO.summary.smsReminders + ' SMS, ' + DEMO.summary.emailFallbacks + ' email fallbacks');
}

/** Force send reminders to all students with class in the next 7 days (use with care). */
function sendAllPendingReminders() {
  if (CONFIG.DEMO_MODE) {
    Logger.log('๐ŸŽญ DEMO MODE โ€” cannot send real reminders. Set DEMO_MODE to false first.');
    return;
  }
  runReminders_(1, 'day-before');
}

/* ================================================================
   SHARED HELPERS
   ================================================================ */

/** Header contains any candidate (.includes() matching). Returns 0-based index or -1. */
function findCol_(headers, candidates) {
  for (let i = 0; i < headers.length; i++) {
    const h = String(headers[i] || '').toLowerCase().trim();
    for (const kw of candidates) {
      if (kw && h.includes(String(kw).toLowerCase().trim())) return i;
    }
  }
  return -1;
}

function esc_(str) {
  if (str == null) return '';
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

function sanitize_(str) {
  return String(str || '').replace(/[<>{}()\[\]\\\/]/g, '').substring(0, 100).trim();
}

function parseDate_(val) {
  if (val instanceof Date) return val;
  if (val == null || val === '') return new Date(NaN);
  return new Date(val);
}
๐Ÿ“ง End-of-Day Summary Email 43KB ยท 938 linesGSโ–ถ
/**
 * =========================================================
 *  END-OF-DAY SUMMARY EMAIL โ€” Cherry on Top #1
 *  Flavors Driving School
 * =========================================================
 *  Daily business briefing delivered at 7 PM
 *
 *  One email. Everything that happened today.
 *  Tomorrow's schedule. Revenue snapshot. Alerts.
 *  Mom doesn't lift a finger โ€” it just shows up.
 *
 *  Features:
 *  - Demo mode with realistic fake data
 *  - MailApp (not GmailApp)
 *  - Email-first + fuzzy name matching for completion detection
 *  - Attendance counting skips cancelled/no-show/pending/scheduled/upcoming/rescheduled
 *  - Mission Control theme (#000/#0d0d0d/#ff2d2d)
 *  - Unsubscribe link
 *  - Expense tracking + net profit
 *  - Week-to-date stats
 *  - Smart contextual greeting
 * =========================================================
 */

// โ”€โ”€ CONFIG โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const CONFIG = {
  SCHEDULE_BOARD_ID: '1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE',
  REGISTRATION_ID: '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',
  PAYMENT_TRACKER_ID: '1XWNU7PygQX9jV6mFUUrgHYU_vrhS7NP1OHwNUNWRoOk',
  SIGNUP_SHEET_ID: '1lE2_amcNj3-ao3TiP_U0VpaWHqmx4R-0rkF3n3xl7fM',
  EXPENSE_TRACKER_ID: '1QyC39fjuslDXk-a_u09XACyHSq782H7NUJajV3vBp8M',
  SCHOOL_NAME: 'Flavors Driving School',
  SCHOOL_EMAIL: '[email protected]',
  INSTRUCTORS: ['Anisha', 'Carlos', 'Nick'],
  WORK_START: 9,
  WORK_END: 18,

  BOOKINGS_SHEET_TAB: 'Bookings',
  REGISTRATION_SHEET_TAB: '',
  SIGNUP_SHEET_TAB: '',

  // DEMO MODE โ€” set to true for presentations
  DEMO_MODE: true,
};

// โ”€โ”€ DEMO DATA โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const DEMO = {
  todaysLessons: [
    { time: '9:00 AM', student: 'Sarah Johnson', instructor: 'Carlos', status: 'Completed' },
    { time: '10:00 AM', student: 'Marcus Williams', instructor: 'Nick', status: 'Completed' },
    { time: '11:00 AM', student: 'Priya Patel', instructor: 'Anisha', status: 'Completed' },
    { time: '1:00 PM', student: 'Alex Rivera', instructor: 'Carlos', status: 'No Show' },
    { time: '2:00 PM', student: 'Sofia Rivera', instructor: 'Anisha', status: 'Completed' },
    { time: '3:00 PM', student: 'David Chen', instructor: 'Nick', status: 'Cancelled' },
    { time: '4:00 PM', student: 'Emma Garcia', instructor: 'Carlos', status: 'Completed' },
  ],
  todaysPayments: [
    { student: 'Sarah Johnson', amount: 200, method: 'Zelle' },
    { student: 'Priya Patel', amount: 150, method: 'Cash' },
    { student: 'Emma Garcia', amount: 300, method: 'Card' },
  ],
  newSignups: [
    { name: 'Tyler Brooks', package: '10 Lessons', email: '[email protected]' },
  ],
  new5HourSignups: [
    { name: 'Jessica Kim' },
  ],
  tomorrowSchedule: [
    { time: '9:00 AM', student: 'Marcus Williams', instructor: 'Nick' },
    { time: '10:00 AM', student: 'Sarah Johnson', instructor: 'Carlos' },
    { time: '11:00 AM', student: 'Priya Patel', instructor: 'Anisha' },
    { time: '1:00 PM', student: 'Sofia Rivera', instructor: 'Carlos' },
    { time: '2:00 PM', student: 'Emma Garcia', instructor: 'Anisha' },
    { time: '3:00 PM', student: 'Tyler Brooks', instructor: 'Nick' },
  ],
  overdueStudents: [
    { name: 'Alex Rivera', balance: 250, daysSince: 21 },
  ],
  completedToday: [
    { name: 'Emma Garcia', package: '10 Lessons', totalLessons: 10 },
  ],
  todaysExpenses: [
    { category: 'Gas', amount: 45.50, notes: 'Instructor vehicles' },
  ],
  weekStats: { weekRevenue: 2150, weekLessons: 32, weekSignups: 4 },
};

// โ”€โ”€ FORCE AUTH โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function forceAuth() {
  SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  SpreadsheetApp.openById(CONFIG.SIGNUP_SHEET_ID);
  SpreadsheetApp.openById(CONFIG.EXPENSE_TRACKER_ID);
  MailApp.getRemainingDailyQuota();
  Logger.log('โœ… All permissions authorized โ€” 5 sheets + email.');
}

// โ”€โ”€ SETUP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function fullSetup() {
  setupTrigger_();
  Logger.log('โœ… End-of-Day Summary fully set up.');
}

function setupTrigger_() {
  ScriptApp.getProjectTriggers().forEach(t => {
    if (t.getHandlerFunction() === 'sendEndOfDaySummary') ScriptApp.deleteTrigger(t);
  });

  ScriptApp.newTrigger('sendEndOfDaySummary')
    .timeBased()
    .everyDays(1)
    .atHour(19)
    .nearMinute(0)
    .create();

  Logger.log('โœ… Trigger: sendEndOfDaySummary daily at 7 PM');
}

// โ”€โ”€ MAIN โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function sendEndOfDaySummary() {
  try {
    const isDemo = CONFIG.DEMO_MODE;
    if (isDemo) Logger.log('โš ๏ธ DEMO MODE โ€” using simulated data.');

    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const tomorrow = new Date(today);
    tomorrow.setDate(tomorrow.getDate() + 1);
    const endOfToday = new Date(today);
    endOfToday.setHours(23, 59, 59, 999);

    let data;
    if (isDemo) {
      data = {
        date: today,
        lessonsCompleted: DEMO.todaysLessons.filter(l => !isCancelled_(l.status) && l.status.toLowerCase() !== 'no show').length,
        noShows: DEMO.todaysLessons.filter(l => l.status.toLowerCase() === 'no show').length,
        cancellations: DEMO.todaysLessons.filter(l => isCancelled_(l.status)).length,
        todaysLessons: DEMO.todaysLessons,
        revenueToday: DEMO.todaysPayments.reduce((s, p) => s + p.amount, 0),
        expensesToday: DEMO.todaysExpenses.reduce((s, e) => s + e.amount, 0),
        todaysPayments: DEMO.todaysPayments,
        newSignups: DEMO.newSignups,
        new5HourSignups: DEMO.new5HourSignups,
        tomorrowSchedule: DEMO.tomorrowSchedule,
        tomorrowLessonCount: DEMO.tomorrowSchedule.length,
        tomorrowOpenSlots: (CONFIG.INSTRUCTORS.length * (CONFIG.WORK_END - CONFIG.WORK_START)) - DEMO.tomorrowSchedule.length,
        overdueStudents: DEMO.overdueStudents,
        completedToday: DEMO.completedToday,
        todaysExpenses: DEMO.todaysExpenses,
        weekStats: DEMO.weekStats,
      };
    } else {
      const todaysLessons = getTodaysLessons_(today, endOfToday);
      const todaysPayments = getTodaysPayments_(today, endOfToday);
      const newSignups = getNewSignups_(today, endOfToday);
      const new5HourSignups = getNew5HourSignups_(today, endOfToday);
      const tomorrowSchedule = getTomorrowSchedule_(tomorrow);
      const overdueStudents = getOverdueStudents_();
      const completedToday = getCompletedStudents_(today, endOfToday);
      const todaysExpenses = getTodaysExpenses_(today, endOfToday);
      const weekStats = getWeekStats_(today);
      const tomorrowLessonCount = tomorrowSchedule.length;

      data = {
        date: today,
        lessonsCompleted: todaysLessons.filter(l => !isCancelled_(l.status) && l.status.toLowerCase() !== 'no show').length,
        noShows: todaysLessons.filter(l => l.status.toLowerCase() === 'no show').length,
        cancellations: todaysLessons.filter(l => isCancelled_(l.status)).length,
        todaysLessons,
        revenueToday: todaysPayments.reduce((s, p) => s + p.amount, 0),
        expensesToday: todaysExpenses.reduce((s, e) => s + e.amount, 0),
        todaysPayments,
        newSignups,
        new5HourSignups,
        tomorrowSchedule,
        tomorrowLessonCount,
        tomorrowOpenSlots: (CONFIG.INSTRUCTORS.length * (CONFIG.WORK_END - CONFIG.WORK_START)) - tomorrowLessonCount,
        overdueStudents,
        completedToday,
        todaysExpenses,
        weekStats,
      };
    }

    const html = buildSummaryEmail_(data, isDemo);
    const dayName = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][today.getDay()];
    const dateStr = formatDate_(today);

    MailApp.sendEmail({
      to: CONFIG.SCHOOL_EMAIL,
      subject: (isDemo ? '[DEMO] ' : '') + '๐Ÿ“Š Daily Briefing โ€” ' + dayName + ', ' + dateStr + ' โ€” ' + CONFIG.SCHOOL_NAME,
      htmlBody: html,
      name: CONFIG.SCHOOL_NAME,
    });

    Logger.log((isDemo ? 'DEMO: ' : '') + 'End-of-day summary sent for ' + dateStr);

  } catch (e) {
    Logger.log('โŒ sendEndOfDaySummary error: ' + e.message + '\n' + e.stack);
    try {
      MailApp.sendEmail({
        to: CONFIG.SCHOOL_EMAIL,
        subject: 'โŒ End-of-Day Summary failed โ€” ' + CONFIG.SCHOOL_NAME,
        body: 'The daily briefing did not send.\n\nError: ' + (e.message || e.toString()),
      });
    } catch (mailErr) { Logger.log('Could not send error email: ' + mailErr.message); }
  }
}

/** Test: send summary now without waiting for 7 PM */
function testSendNow() {
  sendEndOfDaySummary();
}


// ================================================================
//  DATA GATHERING
// ================================================================

function getBookingsSheet_() {
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  if (CONFIG.BOOKINGS_SHEET_TAB) {
    const sheet = ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB);
    if (sheet) return sheet;
  }
  return ss.getSheets()[0];
}

function getRegistrationSheet_() {
  const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  if (CONFIG.REGISTRATION_SHEET_TAB) {
    const sheet = ss.getSheetByName(CONFIG.REGISTRATION_SHEET_TAB);
    if (sheet) return sheet;
  }
  return ss.getSheets()[0];
}

function getSignupSheet_() {
  const ss = SpreadsheetApp.openById(CONFIG.SIGNUP_SHEET_ID);
  if (CONFIG.SIGNUP_SHEET_TAB) {
    const sheet = ss.getSheetByName(CONFIG.SIGNUP_SHEET_TAB);
    if (sheet) return sheet;
  }
  return ss.getSheets()[0];
}

function getTodaysLessons_(today, endOfToday) {
  const sheet = getBookingsSheet_();
  if (!sheet || sheet.getLastRow() <= 1) return [];

  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const dateCol = findCol_(headers, ['date', 'lesson date', 'booking date']);
  const timeCol = findCol_(headers, ['time', 'lesson time', 'start time', 'time slot']);
  const studentCol = findCol_(headers, ['student', 'student name', 'name']);
  const instructorCol = findCol_(headers, ['instructor', 'instructor name']);
  const statusCol = findCol_(headers, ['status', 'booking status']);
  if (dateCol === -1) return [];

  const lessons = [];
  for (let i = 1; i < data.length; i++) {
    const date = data[i][dateCol] instanceof Date ? data[i][dateCol] : new Date(data[i][dateCol]);
    if (isNaN(date.getTime())) continue;
    if (date >= today && date <= endOfToday) {
      lessons.push({
        time: String(data[i][timeCol] || ''),
        student: String(data[i][studentCol] || '').trim(),
        instructor: String(data[i][instructorCol] || '').trim(),
        status: String(data[i][statusCol] || 'Scheduled').trim(),
      });
    }
  }
  lessons.sort((a, b) => parseTime_(a.time) - parseTime_(b.time));
  return lessons;
}

function getTodaysPayments_(today, endOfToday) {
  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  const sheet = ss.getSheetByName('Payments') || ss.getSheets()[0];
  if (!sheet || sheet.getLastRow() <= 1) return [];

  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const dateCol = findCol_(headers, ['date', 'payment date', 'timestamp']);
  const studentCol = findCol_(headers, ['student', 'student name', 'name']);
  const amountCol = findCol_(headers, ['amount', 'payment amount']);
  const methodCol = findCol_(headers, ['method', 'payment method']);
  if (dateCol === -1) return [];

  const payments = [];
  for (let i = 1; i < data.length; i++) {
    const date = data[i][dateCol] instanceof Date ? data[i][dateCol] : new Date(data[i][dateCol]);
    if (isNaN(date.getTime()) || date < today || date > endOfToday) continue;
    payments.push({
      student: String(data[i][studentCol] || '').trim(),
      amount: parseFloat(data[i][amountCol]) || 0,
      method: methodCol !== -1 ? String(data[i][methodCol] || '').trim() : '',
    });
  }
  return payments;
}

function getNewSignups_(today, endOfToday) {
  const sheet = getRegistrationSheet_();
  if (!sheet || sheet.getLastRow() <= 1) return [];

  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const tsCol = findCol_(headers, ['timestamp', 'date', 'submitted']);
  const nameCol = findCol_(headers, ['full name', 'name', 'student name']);
  const packageCol = findCol_(headers, ['lesson package', 'package', 'class type', 'selected package']);
  const emailCol = findCol_(headers, ['email', 'email address', 'student email']);
  if (tsCol === -1) return [];

  const signups = [];
  for (let i = 1; i < data.length; i++) {
    const date = data[i][tsCol] instanceof Date ? data[i][tsCol] : new Date(data[i][tsCol]);
    if (isNaN(date.getTime()) || date < today || date > endOfToday) continue;
    signups.push({
      name: String(data[i][nameCol] || '').trim(),
      package: packageCol !== -1 ? String(data[i][packageCol] || '').trim() : '',
      email: emailCol !== -1 ? String(data[i][emailCol] || '').trim() : '',
    });
  }
  return signups;
}

function getNew5HourSignups_(today, endOfToday) {
  const sheet = getSignupSheet_();
  if (!sheet || sheet.getLastRow() <= 1) return [];

  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const tsCol = findCol_(headers, ['timestamp', 'date']);
  const nameCol = findCol_(headers, ['full name', 'name']);
  if (tsCol === -1) return [];

  const signups = [];
  for (let i = 1; i < data.length; i++) {
    const date = data[i][tsCol] instanceof Date ? data[i][tsCol] : new Date(data[i][tsCol]);
    if (isNaN(date.getTime()) || date < today || date > endOfToday) continue;
    signups.push({ name: String(data[i][nameCol] || '').trim() });
  }
  return signups;
}

function getTomorrowSchedule_(tomorrow) {
  const endOfTomorrow = new Date(tomorrow);
  endOfTomorrow.setHours(23, 59, 59, 999);

  const sheet = getBookingsSheet_();
  if (!sheet || sheet.getLastRow() <= 1) return [];

  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const dateCol = findCol_(headers, ['date', 'lesson date', 'booking date']);
  const timeCol = findCol_(headers, ['time', 'lesson time', 'start time', 'time slot']);
  const studentCol = findCol_(headers, ['student', 'student name', 'name']);
  const instructorCol = findCol_(headers, ['instructor', 'instructor name']);
  const statusCol = findCol_(headers, ['status', 'booking status']);
  if (dateCol === -1) return [];

  const lessons = [];
  for (let i = 1; i < data.length; i++) {
    const date = data[i][dateCol] instanceof Date ? data[i][dateCol] : new Date(data[i][dateCol]);
    if (isNaN(date.getTime()) || date < tomorrow || date > endOfTomorrow) continue;
    if (isCancelled_(String(data[i][statusCol] || ''))) continue;

    lessons.push({
      time: String(data[i][timeCol] || ''),
      student: String(data[i][studentCol] || '').trim(),
      instructor: String(data[i][instructorCol] || '').trim(),
    });
  }
  lessons.sort((a, b) => parseTime_(a.time) - parseTime_(b.time));
  return lessons;
}

function getOverdueStudents_() {
  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  const balSheet = ss.getSheetByName('Student Balances');
  if (!balSheet || balSheet.getLastRow() <= 1) return [];

  const data = balSheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const nameCol = findCol_(headers, ['student', 'student name', 'name']);
  const balCol = findCol_(headers, ['balance', 'remaining balance', 'amount due']);
  const lastPayCol = findCol_(headers, ['last payment', 'last payment date']);
  if (nameCol === -1 || balCol === -1) return [];

  const overdue = [];
  const now = new Date();
  const fourteenDays = 14 * 24 * 60 * 60 * 1000;

  for (let i = 1; i < data.length; i++) {
    const balance = parseFloat(data[i][balCol]) || 0;
    if (balance <= 0) continue;

    const lastPay = lastPayCol !== -1 ? (data[i][lastPayCol] instanceof Date ? data[i][lastPayCol] : new Date(data[i][lastPayCol])) : null;
    if (lastPay && !isNaN(lastPay.getTime()) && (now - lastPay) > fourteenDays) {
      overdue.push({
        name: String(data[i][nameCol] || '').trim(),
        balance: balance,
        daysSince: Math.floor((now - lastPay) / (24 * 60 * 60 * 1000)),
      });
    }
  }
  overdue.sort((a, b) => b.daysSince - a.daysSince);
  return overdue;
}

function getCompletedStudents_(today, endOfToday) {
  const sheet = getBookingsSheet_();
  if (!sheet || sheet.getLastRow() <= 1) return [];

  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const dateCol = findCol_(headers, ['date', 'lesson date', 'booking date']);
  const studentCol = findCol_(headers, ['student', 'student name', 'name']);
  const emailCol = findCol_(headers, ['email', 'student email']);
  const statusCol = findCol_(headers, ['status', 'booking status']);
  if (dateCol === -1 || studentCol === -1) return [];

  // Only count confirmed past lessons
  const skipStatuses = ['cancelled', 'canceled', 'no-show', 'no show', 'noshow', 'pending', 'scheduled', 'upcoming', 'rescheduled'];

  const lessonsByName = {};
  const lessonsByEmail = {};
  const hadLessonToday = {};

  for (let i = 1; i < data.length; i++) {
    const status = String(data[i][statusCol] || '').toLowerCase().trim();
    if (skipStatuses.includes(status)) continue;

    const date = data[i][dateCol] instanceof Date ? data[i][dateCol] : new Date(data[i][dateCol]);
    if (isNaN(date.getTime()) || date > endOfToday) continue;

    const sName = String(data[i][studentCol] || '').trim().toLowerCase();
    const sEmail = emailCol !== -1 ? String(data[i][emailCol] || '').trim().toLowerCase() : '';

    if (sName) lessonsByName[sName] = (lessonsByName[sName] || 0) + 1;
    if (sEmail && sEmail.includes('@')) lessonsByEmail[sEmail] = (lessonsByEmail[sEmail] || 0) + 1;

    if (date >= today && date <= endOfToday) {
      if (sName) hadLessonToday[sName] = true;
      if (sEmail) hadLessonToday[sEmail] = true;
    }
  }

  const regSheet = getRegistrationSheet_();
  if (!regSheet || regSheet.getLastRow() <= 1) return [];
  const regData = regSheet.getDataRange().getValues();
  const regHeaders = regData[0].map(h => String(h).toLowerCase().trim());
  const regNameCol = findCol_(regHeaders, ['full name', 'name', 'student name']);
  const regEmailCol = findCol_(regHeaders, ['email', 'email address', 'student email']);
  const regPkgCol = findCol_(regHeaders, ['lesson package', 'package', 'selected package']);
  if (regNameCol === -1) return [];

  const completed = [];
  for (let i = 1; i < regData.length; i++) {
    const name = String(regData[i][regNameCol] || '').trim();
    const email = regEmailCol !== -1 ? String(regData[i][regEmailCol] || '').trim().toLowerCase() : '';
    const pkg = regPkgCol !== -1 ? String(regData[i][regPkgCol] || '') : '';
    const totalLessons = extractLessonCount_(pkg);
    if (totalLessons <= 0) continue;

    const nameKey = name.toLowerCase();

    // Count completed: email-first, then name, then fuzzy
    let count = 0;
    if (email && lessonsByEmail[email]) {
      count = lessonsByEmail[email];
    } else if (lessonsByName[nameKey]) {
      count = lessonsByName[nameKey];
    } else {
      for (const [key, c] of Object.entries(lessonsByName)) {
        if (fuzzyNameMatch_(nameKey, key)) { count = c; break; }
      }
    }

    // Check if had a lesson today (email or name or fuzzy)
    let todayFlag = hadLessonToday[nameKey] || hadLessonToday[email];
    if (!todayFlag) {
      for (const key of Object.keys(hadLessonToday)) {
        if (fuzzyNameMatch_(nameKey, key)) { todayFlag = true; break; }
      }
    }

    if (count >= totalLessons && todayFlag) {
      completed.push({ name, package: pkg, totalLessons });
    }
  }
  return completed;
}

function getTodaysExpenses_(today, endOfToday) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.EXPENSE_TRACKER_ID);
    const sheet = ss.getSheets()[0];
    if (!sheet || sheet.getLastRow() <= 1) return [];

    const data = sheet.getDataRange().getValues();
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const dateCol = findCol_(headers, ['date', 'timestamp', 'expense date']);
    const categoryCol = findCol_(headers, ['category', 'expense category']);
    const amountCol = findCol_(headers, ['amount', 'expense amount']);
    const notesCol = findCol_(headers, ['notes', 'description']);
    if (dateCol === -1) return [];

    const expenses = [];
    for (let i = 1; i < data.length; i++) {
      const date = data[i][dateCol] instanceof Date ? data[i][dateCol] : new Date(data[i][dateCol]);
      if (isNaN(date.getTime()) || date < today || date > endOfToday) continue;
      expenses.push({
        category: categoryCol !== -1 ? String(data[i][categoryCol] || '').trim() : '',
        amount: parseFloat(data[i][amountCol]) || 0,
        notes: notesCol !== -1 ? String(data[i][notesCol] || '').trim() : '',
      });
    }
    return expenses;
  } catch (e) { Logger.log('Expense fetch error: ' + e.message); return []; }
}

function getWeekStats_(today) {
  const monday = new Date(today);
  const dayOfWeek = monday.getDay();
  const diff = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
  monday.setDate(monday.getDate() - diff);
  monday.setHours(0, 0, 0, 0);

  const endOfToday = new Date(today);
  endOfToday.setHours(23, 59, 59, 999);

  let weekRevenue = 0;
  try {
    const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
    const paySheet = ss.getSheetByName('Payments') || ss.getSheets()[0];
    if (paySheet && paySheet.getLastRow() > 1) {
      const data = paySheet.getDataRange().getValues();
      const headers = data[0].map(h => String(h).toLowerCase().trim());
      const dateCol = findCol_(headers, ['date', 'payment date', 'timestamp']);
      const amountCol = findCol_(headers, ['amount', 'payment amount']);
      if (dateCol !== -1 && amountCol !== -1) {
        for (let i = 1; i < data.length; i++) {
          const date = data[i][dateCol] instanceof Date ? data[i][dateCol] : new Date(data[i][dateCol]);
          if (!isNaN(date.getTime()) && date >= monday && date <= endOfToday) {
            weekRevenue += parseFloat(data[i][amountCol]) || 0;
          }
        }
      }
    }
  } catch (e) { Logger.log('Week revenue error: ' + e.message); }

  let weekLessons = 0;
  try {
    const schedSheet = getBookingsSheet_();
    if (schedSheet && schedSheet.getLastRow() > 1) {
      const data = schedSheet.getDataRange().getValues();
      const headers = data[0].map(h => String(h).toLowerCase().trim());
      const dateCol = findCol_(headers, ['date', 'lesson date', 'booking date']);
      const statusCol = findCol_(headers, ['status', 'booking status']);
      if (dateCol !== -1) {
        for (let i = 1; i < data.length; i++) {
          const date = data[i][dateCol] instanceof Date ? data[i][dateCol] : new Date(data[i][dateCol]);
          if (isNaN(date.getTime()) || date < monday || date > endOfToday) continue;
          if (isCancelled_(String(data[i][statusCol] || ''))) continue;
          weekLessons++;
        }
      }
    }
  } catch (e) { Logger.log('Week lessons error: ' + e.message); }

  let weekSignups = 0;
  try {
    const regSheet = getRegistrationSheet_();
    if (regSheet && regSheet.getLastRow() > 1) {
      const data = regSheet.getDataRange().getValues();
      const headers = data[0].map(h => String(h).toLowerCase().trim());
      const tsCol = findCol_(headers, ['timestamp', 'date', 'submitted']);
      if (tsCol !== -1) {
        for (let i = 1; i < data.length; i++) {
          const date = data[i][tsCol] instanceof Date ? data[i][tsCol] : new Date(data[i][tsCol]);
          if (!isNaN(date.getTime()) && date >= monday && date <= endOfToday) weekSignups++;
        }
      }
    }
  } catch (e) { Logger.log('Week signups error: ' + e.message); }

  return { weekRevenue, weekLessons, weekSignups };
}


// ================================================================
//  EMAIL BUILDER โ€” Mission Control Theme
// ================================================================

function buildSummaryEmail_(d, isDemo) {
  const dayName = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][d.date.getDay()];
  const dateStr = formatDateLong_(d.date);
  const tomorrowDay = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][(d.date.getDay() + 1) % 7];

  let greeting = 'Here\'s your daily briefing.';
  if (d.lessonsCompleted === 0 && d.newSignups.length === 0) greeting = 'Quiet day today. Here\'s the snapshot.';
  else if (d.revenueToday > 500) greeting = 'Great day for revenue! Here\'s the full breakdown.';
  else if (d.completedToday.length > 0) greeting = 'We had a graduate today! ๐ŸŽ“ Here\'s everything.';
  else if (d.newSignups.length > 0) greeting = 'New students signed up today! Here\'s the rundown.';

  const demoTag = isDemo ? '<div style="background:rgba(255,45,45,0.08);border:1px solid rgba(255,45,45,0.2);border-radius:12px;padding:10px 16px;margin-bottom:20px;text-align:center;"><span style="color:#ff6b6b;font-size:12px;font-weight:700;letter-spacing:1px;text-transform:uppercase;">โœฆ Demo Mode โ€” Simulated Data</span></div>' : '';

  // Stats cards
  const stats =
    '<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:24px;"><tr>' +
    statCard_('Lessons', d.lessonsCompleted, d.noShows > 0 ? d.noShows + ' no-show' + (d.noShows > 1 ? 's' : '') : 'โœ“ All showed up', '#fff', d.noShows > 0 ? '#ff2d2d' : '#22c55e') +
    statCard_('Revenue', '$' + d.revenueToday.toLocaleString(), d.todaysPayments.length + ' payment' + (d.todaysPayments.length !== 1 ? 's' : ''), d.revenueToday > 0 ? '#22c55e' : '#fff', '#555') +
    statCard_('New Students', d.newSignups.length + d.new5HourSignups.length, d.newSignups.length + ' lesson + ' + d.new5HourSignups.length + ' 5-hr', d.newSignups.length > 0 ? '#3b82f6' : '#fff', '#555') +
    statCard_('Tomorrow', d.tomorrowLessonCount, d.tomorrowOpenSlots + ' slots open', '#fff', '#f59e0b') +
    '</tr></table>';

  // Week-to-date
  const week =
    '<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:14px;padding:16px 20px;margin-bottom:24px;">' +
    '<div style="font-size:11px;color:#555;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px;">Week-to-Date</div>' +
    '<table width="100%" cellpadding="0" cellspacing="0">' +
    weekRow_('Revenue this week', '$' + (d.weekStats.weekRevenue || 0).toLocaleString(), '#22c55e') +
    weekRow_('Lessons this week', d.weekStats.weekLessons || 0, '#fff') +
    weekRow_('New signups this week', d.weekStats.weekSignups || 0, '#3b82f6') +
    '</table></div>';

  // Graduates
  let graduatesBlock = '';
  if (d.completedToday.length > 0) {
    let gl = '';
    d.completedToday.forEach(g => {
      gl += '<div style="padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.04);">' +
        '<span style="color:#fff;font-size:14px;font-weight:600;">' + esc_(g.name) + '</span>' +
        '<span style="color:#22c55e;font-size:12px;margin-left:10px;">Completed ' + g.totalLessons + ' lessons!</span></div>';
    });
    graduatesBlock =
      '<div style="background:rgba(34,197,94,0.04);border:1px solid rgba(34,197,94,0.15);border-radius:14px;padding:16px 20px;margin-bottom:24px;">' +
      '<div style="font-size:13px;font-weight:600;color:#22c55e;margin-bottom:12px;">๐ŸŽ“ Package Completed Today!</div>' +
      gl +
      '<div style="font-size:11px;color:#555;margin-top:8px;">Review request + referral code + social post will be auto-generated</div></div>';
  }

  // Signups
  let signupsBlock = '';
  if (d.newSignups.length > 0 || d.new5HourSignups.length > 0) {
    let sl = '';
    d.newSignups.forEach(s => {
      sl += '<div style="display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.04);">' +
        '<span style="color:#fff;font-size:13px;">' + esc_(s.name) + '</span>' +
        '<span style="color:#3b82f6;font-size:12px;font-weight:600;">' + esc_(s.package) + '</span></div>';
    });
    d.new5HourSignups.forEach(s => {
      sl += '<div style="display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid rgba(255,255,255,0.04);">' +
        '<span style="color:#fff;font-size:13px;">' + esc_(s.name) + '</span>' +
        '<span style="color:#f59e0b;font-size:12px;font-weight:600;">5-Hour Class</span></div>';
    });
    signupsBlock =
      '<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:14px;padding:16px 20px;margin-bottom:24px;">' +
      '<div style="font-size:13px;font-weight:600;color:#fff;margin-bottom:12px;">New Signups Today</div>' + sl + '</div>';
  }

  // Today's lessons table
  let lessonRows = '';
  if (d.todaysLessons.length > 0) {
    d.todaysLessons.forEach(l => {
      const isNoShow = l.status.toLowerCase() === 'no show';
      const isCan = isCancelled_(l.status);
      const sColor = isNoShow ? '#ff2d2d' : isCan ? '#555' : '#22c55e';
      const sIcon = isNoShow ? 'โœ—' : isCan ? 'โ€”' : 'โœ“';
      lessonRows +=
        '<tr>' +
        '<td style="color:#888;font-size:13px;padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.04);white-space:nowrap;">' + esc_(l.time) + '</td>' +
        '<td style="color:#fff;font-size:13px;padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.04);">' + esc_(l.student) + '</td>' +
        '<td style="color:#888;font-size:13px;padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.04);">' + esc_(l.instructor) + '</td>' +
        '<td align="right" style="padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.04);"><span style="color:' + sColor + ';font-size:12px;font-weight:600;">' + sIcon + ' ' + esc_(l.status) + '</span></td></tr>';
    });
  } else {
    lessonRows = '<tr><td colspan="4" style="color:#444;font-size:13px;padding:16px 8px;text-align:center;">No lessons scheduled today</td></tr>';
  }

  const lessonsBlock = sectionTable_('Today\'s Lessons',
    '<tr>' +
    thCell_('Time') + thCell_('Student') + thCell_('Instructor') +
    '<td align="right" style="color:#ff2d2d;font-size:10px;text-transform:uppercase;letter-spacing:1px;padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.06);">Status</td>' +
    '</tr>' + lessonRows);

  // Payments
  let paymentsBlock = '';
  if (d.todaysPayments.length > 0) {
    let pr = '';
    d.todaysPayments.forEach(p => {
      pr += '<tr>' +
        '<td style="color:#fff;font-size:13px;padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.04);">' + esc_(p.student) + '</td>' +
        '<td style="color:#22c55e;font-size:14px;font-weight:700;padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.04);">+$' + p.amount.toFixed(2) + '</td>' +
        '<td align="right" style="color:#555;font-size:12px;padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.04);">' + esc_(p.method) + '</td></tr>';
    });
    paymentsBlock = sectionTable_('Payments Received', pr);
  }

  // Expenses
  let expensesBlock = '';
  if (d.todaysExpenses.length > 0) {
    let er = '';
    d.todaysExpenses.forEach(e => {
      er += '<tr>' +
        '<td style="color:#888;font-size:13px;padding:8px;border-bottom:1px solid rgba(255,255,255,0.04);">' + esc_(e.category) + '</td>' +
        '<td style="color:#ff6b6b;font-size:13px;font-weight:600;padding:8px;border-bottom:1px solid rgba(255,255,255,0.04);">-$' + e.amount.toFixed(2) + '</td>' +
        '<td style="color:#555;font-size:12px;padding:8px;border-bottom:1px solid rgba(255,255,255,0.04);">' + esc_(e.notes) + '</td></tr>';
    });
    const net = d.revenueToday - d.expensesToday;
    expensesBlock =
      '<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:14px;overflow:hidden;margin-bottom:24px;">' +
      '<div style="padding:16px 20px;border-bottom:1px solid rgba(255,255,255,0.06);">' +
      '<span style="font-size:13px;font-weight:600;color:#fff;">Expenses</span>' +
      '<span style="font-size:12px;color:' + (net >= 0 ? '#22c55e' : '#ff2d2d') + ';font-weight:600;float:right;">Net: ' + (net >= 0 ? '+' : '') + '$' + net.toFixed(2) + '</span></div>' +
      '<table width="100%" cellpadding="0" cellspacing="0">' + er + '</table></div>';
  }

  // Tomorrow schedule by instructor
  const instructorMap = {};
  CONFIG.INSTRUCTORS.forEach(i => { instructorMap[i] = []; });
  d.tomorrowSchedule.forEach(l => {
    const inst = l.instructor || 'Other';
    if (!instructorMap[inst]) instructorMap[inst] = [];
    instructorMap[inst].push(l);
  });

  let tomorrowRows = '';
  const allInstructors = CONFIG.INSTRUCTORS.slice();
  Object.keys(instructorMap).forEach(inst => { if (!allInstructors.includes(inst)) allInstructors.push(inst); });

  allInstructors.forEach(inst => {
    const lessons = instructorMap[inst] || [];
    tomorrowRows +=
      '<tr><td colspan="2" style="padding:12px 8px 6px;border-bottom:1px solid rgba(255,255,255,0.04);">' +
      '<span style="color:#ff2d2d;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:1px;">' + esc_(inst) + '</span>' +
      '<span style="color:#444;font-size:11px;margin-left:8px;">' + lessons.length + ' lesson' + (lessons.length !== 1 ? 's' : '') + '</span></td></tr>';
    if (lessons.length > 0) {
      lessons.forEach(l => {
        tomorrowRows += '<tr>' +
          '<td style="color:#888;font-size:13px;padding:6px 8px 6px 20px;border-bottom:1px solid rgba(255,255,255,0.02);">' + esc_(l.time) + '</td>' +
          '<td style="color:#ccc;font-size:13px;padding:6px 8px;border-bottom:1px solid rgba(255,255,255,0.02);">' + esc_(l.student) + '</td></tr>';
      });
    } else {
      tomorrowRows += '<tr><td colspan="2" style="color:#333;font-size:12px;padding:6px 8px 6px 20px;">No lessons booked</td></tr>';
    }
  });

  const tomorrowBlock =
    '<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:14px;overflow:hidden;margin-bottom:24px;">' +
    '<div style="padding:16px 20px;border-bottom:1px solid rgba(255,255,255,0.06);">' +
    '<span style="font-size:13px;font-weight:600;color:#fff;">Tomorrow โ€” ' + esc_(tomorrowDay) + '</span>' +
    '<span style="font-size:12px;color:#f59e0b;margin-left:10px;">' + d.tomorrowLessonCount + ' booked ยท ' + d.tomorrowOpenSlots + ' open</span></div>' +
    '<table width="100%" cellpadding="0" cellspacing="0">' + tomorrowRows + '</table></div>';

  // Alerts
  let alertsBlock = '';
  if (d.overdueStudents.length > 0 || d.cancellations > 0) {
    let items = '';
    if (d.cancellations > 0) {
      items += '<div style="padding:8px 0;"><span style="color:#f59e0b;">' + d.cancellations + ' cancellation' + (d.cancellations > 1 ? 's' : '') + ' today</span></div>';
    }
    d.overdueStudents.slice(0, 5).forEach(s => {
      items += '<div style="padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.04);">' +
        '<span style="color:#ff6b6b;">' + esc_(s.name) + '</span>' +
        '<span style="color:#555;font-size:12px;"> โ€” $' + s.balance.toFixed(2) + ' overdue (' + s.daysSince + ' days)</span></div>';
    });
    alertsBlock =
      '<div style="background:rgba(255,45,45,0.04);border:1px solid rgba(255,45,45,0.15);border-radius:14px;padding:16px 20px;margin-bottom:24px;">' +
      '<div style="font-size:13px;font-weight:600;color:#ff2d2d;margin-bottom:10px;">โš ๏ธ Attention Needed</div>' + items + '</div>';
  }

  // Unsubscribe
  const unsubLink = '<p style="margin:8px 0 0;color:#333;font-size:10px;"><a href="mailto:' + esc_(CONFIG.SCHOOL_EMAIL) + '?subject=Unsubscribe%20Daily%20Briefing" style="color:#555;text-decoration:underline;">Unsubscribe</a></p>';

  return '<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"></head>' +
    '<body style="margin:0;padding:0;background:#000;font-family:-apple-system,BlinkMacSystemFont,\'SF Pro Display\',sans-serif;">' +
    '<table width="100%" cellpadding="0" cellspacing="0" style="background:#000;padding:20px;">' +
    '<tr><td align="center">' +
    '<table width="620" cellpadding="0" cellspacing="0" style="background:#0d0d0d;border-radius:20px;overflow:hidden;border:1px solid rgba(255,255,255,0.06);box-shadow:0 8px 30px rgba(0,0,0,0.5);">' +

    // Header
    '<tr><td style="padding:32px 36px;border-bottom:1px solid rgba(255,255,255,0.06);">' +
    '<table width="100%"><tr><td>' +
    '<div style="font-size:10px;color:#ff2d2d;text-transform:uppercase;letter-spacing:2px;font-weight:700;margin-bottom:8px;">DAILY BRIEFING</div>' +
    '<div style="font-size:24px;font-weight:800;color:#fff;letter-spacing:-0.5px;">' + esc_(dayName) + ', ' + esc_(dateStr) + '</div>' +
    '</td><td align="right" style="vertical-align:top;">' +
    '<div style="background:rgba(255,45,45,0.06);border:1px solid rgba(255,45,45,0.12);border-radius:12px;padding:8px 14px;">' +
    '<div style="font-size:10px;color:#555;text-transform:uppercase;letter-spacing:1px;">7 PM Report</div>' +
    '<div style="font-size:14px;color:#ff2d2d;font-weight:600;margin-top:2px;">' + esc_(CONFIG.SCHOOL_NAME) + '</div></div>' +
    '</td></tr></table>' +
    '<div style="font-size:14px;color:#888;margin-top:12px;">' + esc_(greeting) + '</div>' +
    '</td></tr>' +

    // Body
    '<tr><td style="padding:28px 36px;">' +
    demoTag + stats + week + graduatesBlock + signupsBlock + lessonsBlock + paymentsBlock + expensesBlock + tomorrowBlock + alertsBlock +
    '<div style="text-align:center;padding:20px 0 0;"><div style="font-size:13px;color:#555;">That\'s your day. See you tomorrow at 7 PM.</div></div>' +
    '</td></tr>' +

    // Footer
    '<tr><td style="padding:16px 36px;border-top:1px solid rgba(255,255,255,0.06);text-align:center;">' +
    '<p style="color:#333;font-size:11px;margin:0;">End-of-Day Summary โ€ข <span style="color:#ff2d2d;">' + esc_(CONFIG.SCHOOL_NAME) + '</span></p>' +
    '<p style="color:#222;font-size:10px;margin:4px 0 0;">Powered by AI Automation</p>' +
    unsubLink +
    '</td></tr></table></td></tr></table></body></html>';
}

// โ”€โ”€ Email helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function statCard_(label, value, subtitle, valueColor, subColor) {
  return '<td width="25%" style="padding:0 4px;">' +
    '<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:14px;padding:18px 14px;text-align:center;">' +
    '<div style="font-size:11px;color:#555;text-transform:uppercase;letter-spacing:1px;">' + esc_(label) + '</div>' +
    '<div style="font-size:28px;font-weight:800;color:' + valueColor + ';margin:6px 0 2px;">' + value + '</div>' +
    '<div style="font-size:11px;color:' + subColor + ';">' + subtitle + '</div></div></td>';
}

function weekRow_(label, value, color) {
  return '<tr><td style="color:#888;font-size:13px;padding:4px 0;">' + esc_(label) + '</td>' +
    '<td align="right" style="color:' + color + ';font-size:14px;font-weight:700;">' + value + '</td></tr>';
}

function sectionTable_(title, rows) {
  return '<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:14px;overflow:hidden;margin-bottom:24px;">' +
    '<div style="padding:16px 20px;border-bottom:1px solid rgba(255,255,255,0.06);"><span style="font-size:13px;font-weight:600;color:#fff;">' + esc_(title) + '</span></div>' +
    '<table width="100%" cellpadding="0" cellspacing="0">' + rows + '</table></div>';
}

function thCell_(text) {
  return '<td style="color:#ff2d2d;font-size:10px;text-transform:uppercase;letter-spacing:1px;padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.06);">' + esc_(text) + '</td>';
}


// ================================================================
//  FUZZY NAME MATCHING
// ================================================================

function fuzzyNameMatch_(name1, name2) {
  if (!name1 || !name2) return false;
  const n1 = name1.toLowerCase().replace(/\s+/g, ' ').trim();
  const n2 = name2.toLowerCase().replace(/\s+/g, ' ').trim();
  if (n1 === n2) return true;
  if (n1.includes(n2) || n2.includes(n1)) return true;

  const p1 = n1.split(' ').filter(Boolean);
  const p2 = n2.split(' ').filter(Boolean);

  if (p1.length >= 2 && p2.length >= 2) {
    if (p1[p1.length-1] === p2[p2.length-1] && p1[0].substring(0,3) === p2[0].substring(0,3)) return true;
    if (p1[0] === p2[p2.length-1] && p1[p1.length-1] === p2[0]) return true;
  }
  if (levenshtein_(n1, n2) <= 2) return true;
  return false;
}

function levenshtein_(a, b) {
  if (a.length === 0) return b.length;
  if (b.length === 0) return a.length;
  const m = [];
  for (let i = 0; i <= b.length; i++) m[i] = [i];
  for (let j = 0; j <= a.length; j++) m[0][j] = j;
  for (let i = 1; i <= b.length; i++) {
    for (let j = 1; j <= a.length; j++) {
      m[i][j] = b.charAt(i-1) === a.charAt(j-1) ? m[i-1][j-1] : Math.min(m[i-1][j-1]+1, m[i][j-1]+1, m[i-1][j]+1);
    }
  }
  return m[b.length][a.length];
}


// ================================================================
//  UTILITIES
// ================================================================

function findCol_(headers, candidates) {
  for (const c of candidates) {
    const idx = headers.findIndex(h => h.includes(c.toLowerCase()));
    if (idx !== -1) return idx;
  }
  return -1;
}

function isCancelled_(status) {
  const s = String(status || '').toLowerCase().trim();
  return s === 'cancelled' || s === 'canceled';
}

function esc_(str) {
  if (!str) return '';
  return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function formatDate_(date) {
  if (!(date instanceof Date)) date = new Date(date);
  if (isNaN(date.getTime())) return '';
  return (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear();
}

function formatDateLong_(date) {
  if (!(date instanceof Date)) date = new Date(date);
  if (isNaN(date.getTime())) return '';
  const months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
  return months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
}

function extractLessonCount_(pkg) {
  if (!pkg) return 0;
  const raw = String(pkg).toLowerCase().trim();
  if (raw.includes('5-hour') || raw.includes('5 hour')) return 1;
  const match = raw.match(/(\d+)\s*lesson/i);
  if (match) return parseInt(match[1]);
  const match2 = raw.match(/^(\d+)$/);
  if (match2) return parseInt(match2[1]);
  if (raw.includes('beginner')) return 10;
  if (raw.includes('standard')) return 10;
  if (raw.includes('premium')) return 15;
  if (raw.includes('intensive')) return 20;
  return 0;
}

function parseTime_(timeStr) {
  if (!timeStr) return 0;
  const match = timeStr.toString().match(/(\d{1,2}):?(\d{2})?\s*(AM|PM)?/i);
  if (!match) return 0;
  let h = parseInt(match[1]);
  const m = parseInt(match[2] || '0');
  const period = (match[3] || '').toUpperCase();
  if (period === 'PM' && h < 12) h += 12;
  if (period === 'AM' && h === 12) h = 0;
  return h * 60 + m;
}
๐Ÿ’ธ Expense Tracker 43KB ยท 1,065 linesGSโ–ถ
/**
 * =========================================================
 * EXPENSE TRACKER
 * Flavors Driving School
 * =========================================================
 * Processes expense form submissions, categorizes spending,
 * tracks budgets, and generates monthly reports.
 *
 * - Auto-processes form submissions with Expense IDs (EXP-0001)
 * - Budget threshold alerts by category
 * - Monthly expense summary emails (1st of each month)
 * - Weekly spending digest (Monday mornings)
 * - Duplicate receipt detection
 * - Category breakdown with YTD totals
 * - getExpenseSummary() API for BI Dashboard
 * - Demo mode for safe presentations
 * =========================================================
 */

const CONFIG = {
  // โ”€โ”€ Sheet ID โ”€โ”€
  EXPENSE_SHEET_ID: '1QyC39fjuslDXk-a_u09XACyHSq782H7NUJajV3vBp8M',

  // โ”€โ”€ Sheet tabs โ”€โ”€
  FORM_RESPONSES_TAB: 'Form Responses 1',
  EXPENSE_LOG_TAB: 'Expense Log',
  MONTHLY_REPORT_TAB: 'Monthly Report',
  SETTINGS_TAB: 'Settings',
  BUDGET_TAB: 'Budgets',

  // โ”€โ”€ Admin โ”€โ”€
  ADMIN_EMAIL: '[email protected]',
  SCHOOL_NAME: 'Flavors Driving School',

  // โ”€โ”€ Demo Mode โ”€โ”€
  DEMO_MODE: true,

  // โ”€โ”€ Default Categories โ”€โ”€
  CATEGORIES: [
    'Vehicle Maintenance',
    'Fuel / Gas',
    'Insurance',
    'Instructor Pay',
    'Office Supplies',
    'Marketing / Advertising',
    'Rent / Utilities',
    'Technology / Software',
    'Licensing / Permits',
    'Training Materials',
    'Miscellaneous'
  ],

  // โ”€โ”€ Default Monthly Budgets (per category) โ”€โ”€
  DEFAULT_BUDGETS: {
    'Vehicle Maintenance': 500,
    'Fuel / Gas': 800,
    'Insurance': 1200,
    'Instructor Pay': 5000,
    'Office Supplies': 200,
    'Marketing / Advertising': 300,
    'Rent / Utilities': 1500,
    'Technology / Software': 100,
    'Licensing / Permits': 150,
    'Training Materials': 100,
    'Miscellaneous': 200
  },

  // โ”€โ”€ Alert threshold (% of budget) โ”€โ”€
  BUDGET_ALERT_PERCENT: 80
};

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   AUTH & SETUP
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function forceAuth() {
  const ss = SpreadsheetApp.openById(CONFIG.EXPENSE_SHEET_ID);
  Logger.log('Expense Tracker sheet: ' + ss.getName());
  MailApp.getRemainingDailyQuota();
  Logger.log('MailApp authorized. Remaining quota: ' + MailApp.getRemainingDailyQuota());
  Logger.log('forceAuth complete โœ…');
}

function fullSetup() {
  const ss = getSpreadsheet_();

  // โ”€โ”€ Expense Log sheet โ”€โ”€
  let logSheet = ss.getSheetByName(CONFIG.EXPENSE_LOG_TAB);
  if (!logSheet) {
    logSheet = ss.insertSheet(CONFIG.EXPENSE_LOG_TAB);
    logSheet.getRange(1, 1, 1, 12).setValues([[
      'Expense ID', 'Timestamp', 'Date of Expense', 'Expense Category',
      'Amount ($)', 'Receipt Number', 'Notes / Description', 'Processed Date',
      'Month-Year', 'Fiscal Quarter', 'Duplicate Flag', 'Status'
    ]]);
    logSheet.getRange(1, 1, 1, 12).setFontWeight('bold').setBackground('#0d0d0d').setFontColor('#ff2d2d');
    logSheet.setFrozenRows(1);
    Logger.log('Created Expense Log sheet โœ…');
  } else {
    Logger.log('Expense Log sheet already exists โœ…');
  }

  // โ”€โ”€ Settings sheet โ”€โ”€
  let settingsSheet = ss.getSheetByName(CONFIG.SETTINGS_TAB);
  if (!settingsSheet) {
    settingsSheet = ss.insertSheet(CONFIG.SETTINGS_TAB);
    settingsSheet.getRange(1, 1, 1, 3).setValues([['Setting', 'Value', 'Description']]);
    settingsSheet.getRange(1, 1, 1, 3).setFontWeight('bold').setBackground('#0d0d0d').setFontColor('#ff2d2d');
    const settings = [
      ['DEMO_MODE', 'true', 'Set to false for production'],
      ['ADMIN_EMAIL', CONFIG.ADMIN_EMAIL, 'Admin receives alerts and reports'],
      ['BUDGET_ALERT_PERCENT', '80', 'Alert when category reaches this % of budget'],
      ['MONTHLY_REPORT_DAY', '1', 'Day of month to send expense report (1-28)'],
      ['WEEKLY_DIGEST', 'true', 'Send weekly spending digest on Mondays'],
      ['SCHOOL_NAME', CONFIG.SCHOOL_NAME, 'Business name for reports']
    ];
    settingsSheet.getRange(2, 1, settings.length, 3).setValues(settings);
    settingsSheet.autoResizeColumns(1, 3);
    Logger.log('Created Settings sheet โœ…');
  } else {
    Logger.log('Settings sheet already exists โœ…');
  }

  // โ”€โ”€ Budgets sheet โ”€โ”€
  let budgetSheet = ss.getSheetByName(CONFIG.BUDGET_TAB);
  if (!budgetSheet) {
    budgetSheet = ss.insertSheet(CONFIG.BUDGET_TAB);
    budgetSheet.getRange(1, 1, 1, 4).setValues([['Category', 'Monthly Budget ($)', 'Current Month Spent ($)', 'Remaining ($)']]);
    budgetSheet.getRange(1, 1, 1, 4).setFontWeight('bold').setBackground('#0d0d0d').setFontColor('#ff2d2d');
    const budgetRows = CONFIG.CATEGORIES.map(cat => [cat, CONFIG.DEFAULT_BUDGETS[cat] || 0, 0, 0]);
    budgetSheet.getRange(2, 1, budgetRows.length, 4).setValues(budgetRows);
    budgetSheet.autoResizeColumns(1, 4);
    Logger.log('Created Budgets sheet โœ…');
  } else {
    Logger.log('Budgets sheet already exists โœ…');
  }

  // โ”€โ”€ Monthly Report sheet โ”€โ”€
  let reportSheet = ss.getSheetByName(CONFIG.MONTHLY_REPORT_TAB);
  if (!reportSheet) {
    reportSheet = ss.insertSheet(CONFIG.MONTHLY_REPORT_TAB);
    reportSheet.getRange(1, 1, 1, 6).setValues([['Month-Year', 'Category', 'Total Spent ($)', 'Budget ($)', 'Variance ($)', '% of Budget']]);
    reportSheet.getRange(1, 1, 1, 6).setFontWeight('bold').setBackground('#0d0d0d').setFontColor('#ff2d2d');
    reportSheet.setFrozenRows(1);
    Logger.log('Created Monthly Report sheet โœ…');
  } else {
    Logger.log('Monthly Report sheet already exists โœ…');
  }

  // โ”€โ”€ Triggers (clean duplicates first) โ”€โ”€
  const triggers = ScriptApp.getProjectTriggers();
  const wantedFns = ['onExpenseSubmit', 'sendMonthlyExpenseReport', 'sendWeeklyDigest', 'updateBudgetTracking'];
  for (const t of triggers) {
    if (wantedFns.includes(t.getHandlerFunction())) {
      ScriptApp.deleteTrigger(t);
      Logger.log('Removed old trigger: ' + t.getHandlerFunction());
    }
  }

  // onFormSubmit trigger
  ScriptApp.newTrigger('onExpenseSubmit')
    .forSpreadsheet(CONFIG.EXPENSE_SHEET_ID)
    .onFormSubmit()
    .create();
  Logger.log('Created trigger: onExpenseSubmit (form submit) โœ…');

  // Monthly report โ€” 1st of each month at 8 AM
  ScriptApp.newTrigger('sendMonthlyExpenseReport')
    .timeBased()
    .onMonthDay(1)
    .atHour(8)
    .create();
  Logger.log('Created trigger: sendMonthlyExpenseReport (1st of month 8 AM) โœ…');

  // Weekly digest โ€” every Monday 9 AM
  ScriptApp.newTrigger('sendWeeklyDigest')
    .timeBased()
    .onWeekDay(ScriptApp.WeekDay.MONDAY)
    .atHour(9)
    .create();
  Logger.log('Created trigger: sendWeeklyDigest (Monday 9 AM) โœ…');

  // Budget tracking update โ€” daily at midnight
  ScriptApp.newTrigger('updateBudgetTracking')
    .timeBased()
    .everyDays(1)
    .atHour(0)
    .create();
  Logger.log('Created trigger: updateBudgetTracking (daily midnight) โœ…');

  Logger.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
  Logger.log('fullSetup complete โœ…');
  Logger.log('Sheets created: Expense Log, Settings, Budgets, Monthly Report');
  Logger.log('Triggers: onExpenseSubmit, sendMonthlyExpenseReport, sendWeeklyDigest, updateBudgetTracking');
  Logger.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   HELPERS
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function getSpreadsheet_() {
  return SpreadsheetApp.openById(CONFIG.EXPENSE_SHEET_ID);
}

function getSetting_(key, fallback) {
  try {
    const ss = getSpreadsheet_();
    const sheet = ss.getSheetByName(CONFIG.SETTINGS_TAB);
    if (!sheet) return fallback;
    const data = sheet.getDataRange().getValues();
    for (let i = 1; i < data.length; i++) {
      if (String(data[i][0]).trim().toUpperCase() === key.toUpperCase()) {
        return String(data[i][1]).trim();
      }
    }
  } catch (e) { /* ignore */ }
  return fallback;
}

function isDemoMode_() {
  return getSetting_('DEMO_MODE', 'true').toLowerCase() === 'true';
}

function getAdminEmail_() {
  return getSetting_('ADMIN_EMAIL', CONFIG.ADMIN_EMAIL);
}

function findCol_(headers, label) {
  for (let i = 0; i < headers.length; i++) {
    if (String(headers[i]).toLowerCase().includes(label.toLowerCase())) return i;
  }
  return -1;
}

function escHtml_(s) {
  return String(s || '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

function sanitize_(s, maxLen) {
  return String(s || '').replace(/[^\w\s@.\-\/,#$()&+:;'"!?]/g, '').substring(0, maxLen || 200);
}

function nextExpenseId_() {
  const ss = getSpreadsheet_();
  const logSheet = ss.getSheetByName(CONFIG.EXPENSE_LOG_TAB);
  if (!logSheet || logSheet.getLastRow() < 2) return 'EXP-0001';
  const ids = logSheet.getRange(2, 1, logSheet.getLastRow() - 1, 1).getValues().flat().filter(Boolean);
  let maxNum = 0;
  for (const id of ids) {
    const match = String(id).match(/EXP-(\d+)/);
    if (match) maxNum = Math.max(maxNum, parseInt(match[1], 10));
  }
  return 'EXP-' + String(maxNum + 1).padStart(4, '0');
}

function getMonthYear_(date) {
  const d = date instanceof Date ? date : new Date(date);
  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  return months[d.getMonth()] + ' ' + d.getFullYear();
}

function getFiscalQuarter_(date) {
  const d = date instanceof Date ? date : new Date(date);
  const m = d.getMonth();
  if (m < 3) return 'Q1';
  if (m < 6) return 'Q2';
  if (m < 9) return 'Q3';
  return 'Q4';
}

function formatCurrency_(amount) {
  return '$' + Number(amount || 0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   FORM SUBMIT HANDLER
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function onExpenseSubmit(e) {
  try {
    const demo = isDemoMode_();
    if (demo) {
      Logger.log('DEMO MODE โ€” processing expense but no real alerts');
    }

    const ss = getSpreadsheet_();
    const logSheet = ss.getSheetByName(CONFIG.EXPENSE_LOG_TAB);
    if (!logSheet) {
      Logger.log('ERROR: Expense Log sheet not found. Run fullSetup() first.');
      return;
    }

    // Get submitted data
    let timestamp, dateOfExpense, category, amount, receiptNum, notes;

    if (e && e.namedValues) {
      timestamp = e.namedValues['Timestamp'] ? e.namedValues['Timestamp'][0] : new Date();
      dateOfExpense = e.namedValues['Date of Expense'] ? e.namedValues['Date of Expense'][0] : '';
      category = e.namedValues['Expense Category'] ? e.namedValues['Expense Category'][0] : '';
      amount = e.namedValues['Amount ($)'] ? e.namedValues['Amount ($)'][0] : '0';
      receiptNum = e.namedValues['Receipt Number'] ? e.namedValues['Receipt Number'][0] : '';
      notes = e.namedValues['Notes / Description'] ? e.namedValues['Notes / Description'][0] : '';
    } else if (e && e.range) {
      const row = e.range.getRow();
      const sheet = ss.getSheetByName(CONFIG.FORM_RESPONSES_TAB);
      const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
      const data = sheet.getRange(row, 1, 1, sheet.getLastColumn()).getValues()[0];
      const cTimestamp = findCol_(headers, 'timestamp');
      const cDate = findCol_(headers, 'date of expense');
      const cCategory = findCol_(headers, 'expense category');
      const cAmount = findCol_(headers, 'amount');
      const cReceipt = findCol_(headers, 'receipt');
      const cNotes = findCol_(headers, 'notes');
      timestamp = cTimestamp > -1 ? data[cTimestamp] : new Date();
      dateOfExpense = cDate > -1 ? data[cDate] : '';
      category = cCategory > -1 ? data[cCategory] : '';
      amount = cAmount > -1 ? data[cAmount] : 0;
      receiptNum = cReceipt > -1 ? data[cReceipt] : '';
      notes = cNotes > -1 ? data[cNotes] : '';
    } else {
      Logger.log('No event data โ€” skipping');
      return;
    }

    // Clean and validate
    category = sanitize_(category, 100);
    amount = parseFloat(String(amount).replace(/[^0-9.\-]/g, '')) || 0;
    receiptNum = sanitize_(receiptNum, 50);
    notes = sanitize_(notes, 500);

    if (amount <= 0) {
      Logger.log('WARNING: Amount is $0 or negative for receipt ' + receiptNum);
    }

    const expenseDate = dateOfExpense ? new Date(dateOfExpense) : new Date();
    const expenseId = nextExpenseId_();
    const monthYear = getMonthYear_(expenseDate);
    const quarter = getFiscalQuarter_(expenseDate);

    // Duplicate receipt check
    let dupFlag = '';
    if (receiptNum) {
      const existingReceipts = logSheet.getLastRow() > 1
        ? logSheet.getRange(2, 6, logSheet.getLastRow() - 1, 1).getValues().flat()
        : [];
      if (existingReceipts.some(r => String(r).trim().toUpperCase() === receiptNum.trim().toUpperCase())) {
        dupFlag = 'โš ๏ธ DUPLICATE';
        Logger.log('DUPLICATE RECEIPT DETECTED: ' + receiptNum);
      }
    }

    // Write to Expense Log
    logSheet.appendRow([
      expenseId,
      timestamp,
      expenseDate,
      category,
      amount,
      receiptNum,
      notes,
      new Date(),       // Processed Date
      monthYear,
      quarter,
      dupFlag,
      'Approved'        // Status (auto-approved from form)
    ]);

    Logger.log('Expense logged: ' + expenseId + ' | ' + category + ' | ' + formatCurrency_(amount));

    // Check budget threshold
    checkBudgetAlert_(category, amount, expenseId, demo);

    // Send duplicate alert if needed
    if (dupFlag && !demo) {
      sendDuplicateAlert_(expenseId, category, amount, receiptNum);
    }

  } catch (err) {
    Logger.log('ERROR in onExpenseSubmit: ' + err.message);
    try {
      if (!isDemoMode_()) {
        MailApp.sendEmail({
          to: getAdminEmail_(),
          subject: 'โš ๏ธ Expense Tracker Error',
          htmlBody: buildEmailHtml_('Expense Tracker Error',
            '<p style="color:#ccc;">An error occurred processing an expense:</p>' +
            '<p style="color:#ff2d2d;font-family:monospace;">' + escHtml_(err.message) + '</p>')
        });
      }
    } catch (e2) { /* silent */ }
  }
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   BUDGET TRACKING
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function checkBudgetAlert_(category, newAmount, expenseId, demo) {
  const ss = getSpreadsheet_();
  const budgetSheet = ss.getSheetByName(CONFIG.BUDGET_TAB);
  if (!budgetSheet) return;

  const data = budgetSheet.getDataRange().getValues();
  const headers = data[0];
  const cCat = findCol_(headers, 'category');
  const cBudget = findCol_(headers, 'monthly budget');
  const cSpent = findCol_(headers, 'current month spent');
  const cRemaining = findCol_(headers, 'remaining');

  if (cCat === -1 || cBudget === -1) return;

  for (let i = 1; i < data.length; i++) {
    if (String(data[i][cCat]).trim().toLowerCase() === category.trim().toLowerCase()) {
      const budget = parseFloat(data[i][cBudget]) || 0;
      const currentSpent = parseFloat(data[i][cSpent]) || 0;
      const newTotal = currentSpent + newAmount;
      const remaining = budget - newTotal;

      // Update spent and remaining
      if (cSpent > -1) budgetSheet.getRange(i + 1, cSpent + 1).setValue(newTotal);
      if (cRemaining > -1) budgetSheet.getRange(i + 1, cRemaining + 1).setValue(remaining);

      // Check threshold
      const alertPercent = parseInt(getSetting_('BUDGET_ALERT_PERCENT', '80'), 10);
      if (budget > 0 && (newTotal / budget * 100) >= alertPercent) {
        const pct = Math.round(newTotal / budget * 100);
        Logger.log('BUDGET ALERT: ' + category + ' at ' + pct + '% (' + formatCurrency_(newTotal) + ' of ' + formatCurrency_(budget) + ')');

        if (!demo) {
          const overBudget = newTotal > budget;
          const alertColor = overBudget ? '#ff2d2d' : '#ffaa00';
          const alertIcon = overBudget ? '๐Ÿšจ' : 'โš ๏ธ';
          const alertTitle = overBudget
            ? alertIcon + ' OVER BUDGET: ' + category
            : alertIcon + ' Budget Alert: ' + category;

          MailApp.sendEmail({
            to: getAdminEmail_(),
            subject: alertTitle,
            htmlBody: buildEmailHtml_('Budget Alert',
              '<div style="text-align:center;margin-bottom:20px;">' +
                '<span style="font-size:48px;">' + alertIcon + '</span>' +
              '</div>' +
              '<table style="width:100%;border-collapse:collapse;">' +
                buildRow_('Category', escHtml_(category)) +
                buildRow_('This Expense', formatCurrency_(newAmount) + ' (' + escHtml_(expenseId) + ')') +
                buildRow_('Month Total', '<span style="color:' + alertColor + ';font-weight:bold;">' + formatCurrency_(newTotal) + '</span>') +
                buildRow_('Monthly Budget', formatCurrency_(budget)) +
                buildRow_('Usage', '<span style="color:' + alertColor + ';font-weight:bold;">' + pct + '%</span>') +
                buildRow_('Remaining', remaining >= 0 ? formatCurrency_(remaining) : '<span style="color:#ff2d2d;">-' + formatCurrency_(Math.abs(remaining)) + '</span>') +
              '</table>')
          });
        }
      }
      break;
    }
  }
}

function updateBudgetTracking() {
  const ss = getSpreadsheet_();
  const logSheet = ss.getSheetByName(CONFIG.EXPENSE_LOG_TAB);
  const budgetSheet = ss.getSheetByName(CONFIG.BUDGET_TAB);
  if (!logSheet || !budgetSheet) return;

  const now = new Date();
  const currentMonthYear = getMonthYear_(now);

  // Get all expenses for current month
  const logData = logSheet.getLastRow() > 1
    ? logSheet.getRange(2, 1, logSheet.getLastRow() - 1, 12).getValues()
    : [];

  const spending = {};
  for (const row of logData) {
    const monthYear = String(row[8]);  // Month-Year column
    const category = String(row[3]);   // Category column
    const amount = parseFloat(row[4]) || 0;
    const status = String(row[11]);

    if (monthYear === currentMonthYear && status !== 'Voided') {
      spending[category] = (spending[category] || 0) + amount;
    }
  }

  // Update budget sheet
  const budgetData = budgetSheet.getDataRange().getValues();
  const headers = budgetData[0];
  const cCat = findCol_(headers, 'category');
  const cBudget = findCol_(headers, 'monthly budget');
  const cSpent = findCol_(headers, 'current month spent');
  const cRemaining = findCol_(headers, 'remaining');

  if (cCat === -1) return;

  for (let i = 1; i < budgetData.length; i++) {
    const cat = String(budgetData[i][cCat]).trim();
    const budget = parseFloat(budgetData[i][cBudget]) || 0;
    const spent = spending[cat] || 0;

    if (cSpent > -1) budgetSheet.getRange(i + 1, cSpent + 1).setValue(spent);
    if (cRemaining > -1) budgetSheet.getRange(i + 1, cRemaining + 1).setValue(budget - spent);
  }

  Logger.log('Budget tracking updated for ' + currentMonthYear + ' โœ…');
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   DUPLICATE ALERT
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function sendDuplicateAlert_(expenseId, category, amount, receiptNum) {
  MailApp.sendEmail({
    to: getAdminEmail_(),
    subject: 'โš ๏ธ Duplicate Receipt Detected โ€” ' + receiptNum,
    htmlBody: buildEmailHtml_('Duplicate Receipt Alert',
      '<div style="text-align:center;margin-bottom:20px;">' +
        '<span style="font-size:48px;">โš ๏ธ</span>' +
      '</div>' +
      '<p style="color:#ccc;text-align:center;">A duplicate receipt number was detected.</p>' +
      '<table style="width:100%;border-collapse:collapse;">' +
        buildRow_('Expense ID', expenseId) +
        buildRow_('Category', escHtml_(category)) +
        buildRow_('Amount', formatCurrency_(amount)) +
        buildRow_('Receipt #', escHtml_(receiptNum)) +
      '</table>' +
      '<p style="color:#999;text-align:center;margin-top:16px;">Please verify this is not a duplicate entry.</p>')
  });
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   MONTHLY EXPENSE REPORT
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function sendMonthlyExpenseReport() {
  const demo = isDemoMode_();
  const ss = getSpreadsheet_();
  const logSheet = ss.getSheetByName(CONFIG.EXPENSE_LOG_TAB);
  const budgetSheet = ss.getSheetByName(CONFIG.BUDGET_TAB);

  // Get previous month
  const now = new Date();
  const prevMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
  const reportMonthYear = getMonthYear_(prevMonth);

  let categoryTotals = {};
  let totalSpent = 0;
  let expenseCount = 0;

  if (demo) {
    // Demo data
    categoryTotals = {
      'Fuel / Gas': 645.50,
      'Insurance': 1200.00,
      'Instructor Pay': 4500.00,
      'Vehicle Maintenance': 325.75,
      'Office Supplies': 89.99,
      'Marketing / Advertising': 150.00,
      'Miscellaneous': 42.30
    };
    totalSpent = Object.values(categoryTotals).reduce((a, b) => a + b, 0);
    expenseCount = 23;
  } else {
    // Real data from Expense Log
    if (!logSheet || logSheet.getLastRow() < 2) {
      Logger.log('No expense data for ' + reportMonthYear);
      return;
    }
    const logData = logSheet.getRange(2, 1, logSheet.getLastRow() - 1, 12).getValues();
    for (const row of logData) {
      if (String(row[8]) === reportMonthYear && String(row[11]) !== 'Voided') {
        const cat = String(row[3]);
        const amt = parseFloat(row[4]) || 0;
        categoryTotals[cat] = (categoryTotals[cat] || 0) + amt;
        totalSpent += amt;
        expenseCount++;
      }
    }
  }

  if (expenseCount === 0) {
    Logger.log('No expenses for ' + reportMonthYear);
    return;
  }

  // Get budgets for comparison
  const budgets = {};
  if (budgetSheet && budgetSheet.getLastRow() > 1) {
    const bData = budgetSheet.getDataRange().getValues();
    const bHeaders = bData[0];
    const cCat = findCol_(bHeaders, 'category');
    const cBudget = findCol_(bHeaders, 'monthly budget');
    if (cCat > -1 && cBudget > -1) {
      for (let i = 1; i < bData.length; i++) {
        budgets[String(bData[i][cCat]).trim()] = parseFloat(bData[i][cBudget]) || 0;
      }
    }
  }

  // Save to Monthly Report sheet
  const reportSheet = ss.getSheetByName(CONFIG.MONTHLY_REPORT_TAB);
  if (reportSheet) {
    const reportRows = Object.entries(categoryTotals)
      .sort((a, b) => b[1] - a[1])
      .map(([cat, spent]) => {
        const budget = budgets[cat] || 0;
        const variance = budget - spent;
        const pct = budget > 0 ? Math.round(spent / budget * 100) : 0;
        return [reportMonthYear, cat, spent, budget, variance, pct + '%'];
      });
    // Add total row
    const totalBudget = Object.values(budgets).reduce((a, b) => a + b, 0);
    reportRows.push([reportMonthYear, 'โ•โ•โ• TOTAL โ•โ•โ•', totalSpent, totalBudget, totalBudget - totalSpent, totalBudget > 0 ? Math.round(totalSpent / totalBudget * 100) + '%' : 'N/A']);
    reportSheet.getRange(reportSheet.getLastRow() + 1, 1, reportRows.length, 6).setValues(reportRows);
  }

  // Build email
  const sorted = Object.entries(categoryTotals).sort((a, b) => b[1] - a[1]);
  const totalBudget = Object.values(budgets).reduce((a, b) => a + b, 0);

  let categoryRows = '';
  for (const [cat, spent] of sorted) {
    const budget = budgets[cat] || 0;
    const pct = budget > 0 ? Math.round(spent / budget * 100) : 0;
    const color = pct > 100 ? '#ff2d2d' : pct >= 80 ? '#ffaa00' : '#4CAF50';
    const bar = budget > 0
      ? '<div style="background:#1a1a1a;border-radius:4px;height:8px;width:100%;margin-top:4px;">' +
        '<div style="background:' + color + ';border-radius:4px;height:8px;width:' + Math.min(pct, 100) + '%;"></div></div>'
      : '';
    categoryRows +=
      '<tr>' +
        '<td style="padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#ccc;">' + escHtml_(cat) + '</td>' +
        '<td style="padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#fff;text-align:right;font-weight:bold;">' + formatCurrency_(spent) + '</td>' +
        '<td style="padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#999;text-align:right;">' + (budget > 0 ? formatCurrency_(budget) : 'โ€”') + '</td>' +
        '<td style="padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:' + color + ';text-align:center;width:100px;">' + (budget > 0 ? pct + '%' : 'โ€”') + bar + '</td>' +
      '</tr>';
  }

  const overBudgetCount = sorted.filter(([cat, spent]) => (budgets[cat] || 0) > 0 && spent > (budgets[cat] || 0)).length;
  const avgExpense = totalSpent / expenseCount;

  const body =
    '<div style="text-align:center;margin-bottom:24px;">' +
      '<span style="font-size:48px;">๐Ÿ“Š</span>' +
      '<h2 style="color:#ff2d2d;margin:12px 0 4px;">' + reportMonthYear + '</h2>' +
      '<p style="color:#999;margin:0;">Monthly Expense Report</p>' +
    '</div>' +

    // Summary cards
    '<table style="width:100%;border-collapse:collapse;margin-bottom:20px;"><tr>' +
      '<td style="width:33%;text-align:center;padding:12px;background:#1a1a1a;border-radius:8px;">' +
        '<div style="color:#ff2d2d;font-size:24px;font-weight:bold;">' + formatCurrency_(totalSpent) + '</div>' +
        '<div style="color:#999;font-size:12px;">Total Spent</div>' +
      '</td>' +
      '<td style="width:4px;"></td>' +
      '<td style="width:33%;text-align:center;padding:12px;background:#1a1a1a;border-radius:8px;">' +
        '<div style="color:#fff;font-size:24px;font-weight:bold;">' + expenseCount + '</div>' +
        '<div style="color:#999;font-size:12px;">Expenses</div>' +
      '</td>' +
      '<td style="width:4px;"></td>' +
      '<td style="width:33%;text-align:center;padding:12px;background:#1a1a1a;border-radius:8px;">' +
        '<div style="color:' + (overBudgetCount > 0 ? '#ff2d2d' : '#4CAF50') + ';font-size:24px;font-weight:bold;">' + overBudgetCount + '</div>' +
        '<div style="color:#999;font-size:12px;">Over Budget</div>' +
      '</td>' +
    '</tr></table>' +

    // Category breakdown
    '<table style="width:100%;border-collapse:collapse;background:#0d0d0d;border-radius:8px;overflow:hidden;">' +
      '<tr style="background:#1a1a1a;">' +
        '<th style="padding:10px 12px;text-align:left;color:#ff2d2d;font-size:12px;text-transform:uppercase;">Category</th>' +
        '<th style="padding:10px 12px;text-align:right;color:#ff2d2d;font-size:12px;text-transform:uppercase;">Spent</th>' +
        '<th style="padding:10px 12px;text-align:right;color:#ff2d2d;font-size:12px;text-transform:uppercase;">Budget</th>' +
        '<th style="padding:10px 12px;text-align:center;color:#ff2d2d;font-size:12px;text-transform:uppercase;">Usage</th>' +
      '</tr>' +
      categoryRows +
      '<tr style="background:#1a1a1a;">' +
        '<td style="padding:12px;color:#ff2d2d;font-weight:bold;">TOTAL</td>' +
        '<td style="padding:12px;color:#ff2d2d;font-weight:bold;text-align:right;">' + formatCurrency_(totalSpent) + '</td>' +
        '<td style="padding:12px;color:#999;text-align:right;">' + formatCurrency_(totalBudget) + '</td>' +
        '<td style="padding:12px;color:#ff2d2d;font-weight:bold;text-align:center;">' + (totalBudget > 0 ? Math.round(totalSpent / totalBudget * 100) + '%' : 'โ€”') + '</td>' +
      '</tr>' +
    '</table>' +

    '<p style="color:#999;text-align:center;margin-top:16px;font-size:12px;">Avg expense: ' + formatCurrency_(avgExpense) + ' | Top category: ' + escHtml_(sorted[0][0]) + '</p>';

  const subject = (demo ? '[DEMO] ' : '') + '๐Ÿ“Š ' + reportMonthYear + ' Expense Report โ€” ' + formatCurrency_(totalSpent);

  if (!demo) {
    MailApp.sendEmail({
      to: getAdminEmail_(),
      subject: subject,
      htmlBody: buildEmailHtml_('Monthly Expense Report', body)
    });
  }

  Logger.log('Monthly expense report sent for ' + reportMonthYear + ': ' + formatCurrency_(totalSpent) + ' across ' + expenseCount + ' expenses');
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   WEEKLY DIGEST
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function sendWeeklyDigest() {
  const demo = isDemoMode_();
  if (getSetting_('WEEKLY_DIGEST', 'true').toLowerCase() !== 'true') {
    Logger.log('Weekly digest disabled in Settings');
    return;
  }

  const ss = getSpreadsheet_();
  const logSheet = ss.getSheetByName(CONFIG.EXPENSE_LOG_TAB);

  let weekExpenses = [];
  let weekTotal = 0;

  if (demo) {
    weekExpenses = [
      { id: 'EXP-0042', date: 'Feb 12', category: 'Fuel / Gas', amount: 85.50, notes: 'Shell station โ€” Car #1' },
      { id: 'EXP-0043', date: 'Feb 13', category: 'Office Supplies', amount: 34.99, notes: 'Printer paper + ink' },
      { id: 'EXP-0044', date: 'Feb 14', category: 'Vehicle Maintenance', amount: 125.00, notes: 'Oil change โ€” Car #2' },
      { id: 'EXP-0045', date: 'Feb 15', category: 'Fuel / Gas', amount: 72.30, notes: 'BP station โ€” Car #2' },
      { id: 'EXP-0046', date: 'Feb 16', category: 'Marketing / Advertising', amount: 50.00, notes: 'Facebook ad boost' }
    ];
    weekTotal = weekExpenses.reduce((sum, e) => sum + e.amount, 0);
  } else {
    if (!logSheet || logSheet.getLastRow() < 2) {
      Logger.log('No expenses to report');
      return;
    }
    const now = new Date();
    const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
    const logData = logSheet.getRange(2, 1, logSheet.getLastRow() - 1, 12).getValues();

    for (const row of logData) {
      const processedDate = row[7] instanceof Date ? row[7] : new Date(row[7]);
      if (processedDate >= weekAgo && String(row[11]) !== 'Voided') {
        weekExpenses.push({
          id: String(row[0]),
          date: Utilities.formatDate(row[2] instanceof Date ? row[2] : new Date(row[2]), Session.getScriptTimeZone(), 'MMM d'),
          category: String(row[3]),
          amount: parseFloat(row[4]) || 0,
          notes: String(row[6]).substring(0, 60)
        });
        weekTotal += parseFloat(row[4]) || 0;
      }
    }
  }

  if (weekExpenses.length === 0) {
    Logger.log('No expenses this week');
    return;
  }

  let expenseRows = '';
  for (const exp of weekExpenses) {
    expenseRows +=
      '<tr>' +
        '<td style="padding:8px 10px;border-bottom:1px solid rgba(255,255,255,0.05);color:#999;font-size:12px;">' + escHtml_(exp.id) + '</td>' +
        '<td style="padding:8px 10px;border-bottom:1px solid rgba(255,255,255,0.05);color:#ccc;">' + escHtml_(exp.date) + '</td>' +
        '<td style="padding:8px 10px;border-bottom:1px solid rgba(255,255,255,0.05);color:#ccc;">' + escHtml_(exp.category) + '</td>' +
        '<td style="padding:8px 10px;border-bottom:1px solid rgba(255,255,255,0.05);color:#fff;text-align:right;font-weight:bold;">' + formatCurrency_(exp.amount) + '</td>' +
        '<td style="padding:8px 10px;border-bottom:1px solid rgba(255,255,255,0.05);color:#666;font-size:11px;">' + escHtml_(exp.notes) + '</td>' +
      '</tr>';
  }

  const avgDaily = weekTotal / 7;
  const body =
    '<div style="text-align:center;margin-bottom:24px;">' +
      '<span style="font-size:48px;">๐Ÿ’ฐ</span>' +
      '<h2 style="color:#ff2d2d;margin:12px 0 4px;">Weekly Spending Digest</h2>' +
      '<p style="color:#999;margin:0;">' + weekExpenses.length + ' expenses | ' + formatCurrency_(weekTotal) + ' total | ~' + formatCurrency_(avgDaily) + '/day</p>' +
    '</div>' +

    '<table style="width:100%;border-collapse:collapse;background:#0d0d0d;border-radius:8px;overflow:hidden;">' +
      '<tr style="background:#1a1a1a;">' +
        '<th style="padding:8px 10px;text-align:left;color:#ff2d2d;font-size:11px;text-transform:uppercase;">ID</th>' +
        '<th style="padding:8px 10px;text-align:left;color:#ff2d2d;font-size:11px;text-transform:uppercase;">Date</th>' +
        '<th style="padding:8px 10px;text-align:left;color:#ff2d2d;font-size:11px;text-transform:uppercase;">Category</th>' +
        '<th style="padding:8px 10px;text-align:right;color:#ff2d2d;font-size:11px;text-transform:uppercase;">Amount</th>' +
        '<th style="padding:8px 10px;text-align:left;color:#ff2d2d;font-size:11px;text-transform:uppercase;">Notes</th>' +
      '</tr>' +
      expenseRows +
      '<tr style="background:#1a1a1a;">' +
        '<td colspan="3" style="padding:10px;color:#ff2d2d;font-weight:bold;">TOTAL</td>' +
        '<td style="padding:10px;color:#ff2d2d;font-weight:bold;text-align:right;">' + formatCurrency_(weekTotal) + '</td>' +
        '<td></td>' +
      '</tr>' +
    '</table>';

  const subject = (demo ? '[DEMO] ' : '') + '๐Ÿ’ฐ Weekly Expenses: ' + formatCurrency_(weekTotal) + ' (' + weekExpenses.length + ' items)';

  if (!demo) {
    MailApp.sendEmail({
      to: getAdminEmail_(),
      subject: subject,
      htmlBody: buildEmailHtml_('Weekly Spending Digest', body)
    });
  }

  Logger.log('Weekly digest: ' + formatCurrency_(weekTotal) + ' across ' + weekExpenses.length + ' expenses');
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   API โ€” For BI Dashboard
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function getExpenseSummary(monthYear) {
  const demo = isDemoMode_();

  if (demo) {
    return {
      demo: true,
      monthYear: monthYear || 'Feb 2026',
      totalSpent: 6953.54,
      expenseCount: 23,
      categories: {
        'Instructor Pay': 4500.00,
        'Insurance': 1200.00,
        'Fuel / Gas': 645.50,
        'Vehicle Maintenance': 325.75,
        'Marketing / Advertising': 150.00,
        'Office Supplies': 89.99,
        'Miscellaneous': 42.30
      },
      topCategory: 'Instructor Pay',
      avgExpense: 302.33,
      overBudget: ['Insurance']
    };
  }

  const ss = getSpreadsheet_();
  const logSheet = ss.getSheetByName(CONFIG.EXPENSE_LOG_TAB);
  if (!logSheet || logSheet.getLastRow() < 2) return { totalSpent: 0, expenseCount: 0, categories: {} };

  const target = monthYear || getMonthYear_(new Date());
  const logData = logSheet.getRange(2, 1, logSheet.getLastRow() - 1, 12).getValues();
  const categories = {};
  let totalSpent = 0;
  let count = 0;

  for (const row of logData) {
    if (String(row[8]) === target && String(row[11]) !== 'Voided') {
      const cat = String(row[3]);
      const amt = parseFloat(row[4]) || 0;
      categories[cat] = (categories[cat] || 0) + amt;
      totalSpent += amt;
      count++;
    }
  }

  // Check which are over budget
  const budgetSheet = ss.getSheetByName(CONFIG.BUDGET_TAB);
  const overBudget = [];
  if (budgetSheet && budgetSheet.getLastRow() > 1) {
    const bData = budgetSheet.getDataRange().getValues();
    const cCat = findCol_(bData[0], 'category');
    const cBudget = findCol_(bData[0], 'monthly budget');
    if (cCat > -1 && cBudget > -1) {
      for (let i = 1; i < bData.length; i++) {
        const cat = String(bData[i][cCat]).trim();
        const budget = parseFloat(bData[i][cBudget]) || 0;
        if (budget > 0 && (categories[cat] || 0) > budget) overBudget.push(cat);
      }
    }
  }

  const sorted = Object.entries(categories).sort((a, b) => b[1] - a[1]);
  return {
    demo: false,
    monthYear: target,
    totalSpent: totalSpent,
    expenseCount: count,
    categories: categories,
    topCategory: sorted.length > 0 ? sorted[0][0] : 'N/A',
    avgExpense: count > 0 ? Math.round(totalSpent / count * 100) / 100 : 0,
    overBudget: overBudget
  };
}

/** YTD totals by category */
function getYTDSummary() {
  const demo = isDemoMode_();
  const now = new Date();
  const year = now.getFullYear();

  if (demo) {
    return {
      demo: true,
      year: year,
      totalSpent: 13907.08,
      categories: {
        'Instructor Pay': 9000.00,
        'Insurance': 2400.00,
        'Fuel / Gas': 1291.00,
        'Vehicle Maintenance': 651.50,
        'Marketing / Advertising': 300.00,
        'Office Supplies': 179.98,
        'Miscellaneous': 84.60
      }
    };
  }

  const ss = getSpreadsheet_();
  const logSheet = ss.getSheetByName(CONFIG.EXPENSE_LOG_TAB);
  if (!logSheet || logSheet.getLastRow() < 2) return { year: year, totalSpent: 0, categories: {} };

  const logData = logSheet.getRange(2, 1, logSheet.getLastRow() - 1, 12).getValues();
  const categories = {};
  let totalSpent = 0;

  for (const row of logData) {
    const expDate = row[2] instanceof Date ? row[2] : new Date(row[2]);
    if (expDate.getFullYear() === year && String(row[11]) !== 'Voided') {
      const cat = String(row[3]);
      const amt = parseFloat(row[4]) || 0;
      categories[cat] = (categories[cat] || 0) + amt;
      totalSpent += amt;
    }
  }

  return { demo: false, year: year, totalSpent: totalSpent, categories: categories };
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   VOID / MANAGE EXPENSES
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

/** Void an expense by ID (marks as Voided, doesn't delete) */
function voidExpense(expenseId, reason) {
  const ss = getSpreadsheet_();
  const logSheet = ss.getSheetByName(CONFIG.EXPENSE_LOG_TAB);
  if (!logSheet || logSheet.getLastRow() < 2) return { success: false, error: 'No expenses found' };

  const data = logSheet.getRange(2, 1, logSheet.getLastRow() - 1, 12).getValues();
  for (let i = 0; i < data.length; i++) {
    if (String(data[i][0]).trim() === expenseId.trim()) {
      const statusCol = 12; // Column L (Status)
      logSheet.getRange(i + 2, statusCol).setValue('Voided');

      // Append reason to notes
      if (reason) {
        const notesCol = 7;
        const currentNotes = String(data[i][6]);
        logSheet.getRange(i + 2, notesCol).setValue(currentNotes + ' [VOIDED: ' + sanitize_(reason, 200) + ']');
      }

      Logger.log('Voided expense: ' + expenseId);
      return { success: true, expenseId: expenseId };
    }
  }
  return { success: false, error: 'Expense ID not found: ' + expenseId };
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   DEMO DATA GENERATOR
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function generateDemoExpenses() {
  const ss = getSpreadsheet_();
  const logSheet = ss.getSheetByName(CONFIG.EXPENSE_LOG_TAB);
  if (!logSheet) {
    Logger.log('Run fullSetup() first');
    return;
  }

  const demoExpenses = [
    ['2026-02-03', 'Fuel / Gas', 85.50, 'REC-1001', 'Shell station โ€” Car #1'],
    ['2026-02-05', 'Instructor Pay', 750.00, 'REC-1002', 'Anisha โ€” Week 1 Feb'],
    ['2026-02-05', 'Instructor Pay', 750.00, 'REC-1003', 'Carlos โ€” Week 1 Feb'],
    ['2026-02-05', 'Instructor Pay', 750.00, 'REC-1004', 'Nick โ€” Week 1 Feb'],
    ['2026-02-07', 'Vehicle Maintenance', 125.00, 'REC-1005', 'Oil change โ€” Car #2'],
    ['2026-02-08', 'Office Supplies', 34.99, 'REC-1006', 'Printer paper + ink cartridge'],
    ['2026-02-10', 'Fuel / Gas', 72.30, 'REC-1007', 'BP station โ€” Car #2'],
    ['2026-02-10', 'Marketing / Advertising', 50.00, 'REC-1008', 'Facebook ad boost โ€” Feb special'],
    ['2026-02-12', 'Insurance', 600.00, 'REC-1009', 'Monthly vehicle insurance โ€” 2 cars'],
    ['2026-02-12', 'Instructor Pay', 750.00, 'REC-1010', 'Anisha โ€” Week 2 Feb'],
    ['2026-02-12', 'Instructor Pay', 750.00, 'REC-1011', 'Carlos โ€” Week 2 Feb'],
    ['2026-02-12', 'Instructor Pay', 750.00, 'REC-1012', 'Nick โ€” Week 2 Feb'],
    ['2026-02-14', 'Fuel / Gas', 90.20, 'REC-1013', 'Mobil station โ€” Car #1'],
    ['2026-02-15', 'Rent / Utilities', 1500.00, 'REC-1014', 'Monthly office rent'],
    ['2026-02-16', 'Technology / Software', 29.99, 'REC-1015', 'Scheduling software subscription'],
    ['2026-02-17', 'Training Materials', 45.00, 'REC-1016', 'Student handbooks (25 copies)'],
    ['2026-02-18', 'Fuel / Gas', 78.40, 'REC-1017', 'Shell station โ€” Car #2'],
    ['2026-02-18', 'Miscellaneous', 22.50, 'REC-1018', 'Parking โ€” DMV trips'],
    ['2026-02-19', 'Vehicle Maintenance', 200.00, 'REC-1019', 'Brake pads โ€” Car #1'],
    ['2026-02-19', 'Marketing / Advertising', 100.00, 'REC-1020', 'Instagram promoted post']
  ];

  const rows = demoExpenses.map((exp, i) => {
    const expDate = new Date(exp[0]);
    const id = 'EXP-' + String(i + 1).padStart(4, '0');
    return [
      id,
      expDate,                    // Timestamp
      expDate,                    // Date of Expense
      exp[1],                     // Category
      exp[2],                     // Amount
      exp[3],                     // Receipt Number
      exp[4],                     // Notes
      new Date(),                 // Processed Date
      getMonthYear_(expDate),     // Month-Year
      getFiscalQuarter_(expDate), // Quarter
      '',                         // Duplicate Flag
      'Approved'                  // Status
    ];
  });

  logSheet.getRange(2, 1, rows.length, 12).setValues(rows);
  Logger.log('Generated ' + rows.length + ' demo expenses โœ…');
  Logger.log('Total: ' + formatCurrency_(demoExpenses.reduce((s, e) => s + e[2], 0)));

  // Update budget tracking
  updateBudgetTracking();
  Logger.log('Budget tracking updated with demo data โœ…');
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   EMAIL TEMPLATE โ€” Mission Control Theme
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function buildEmailHtml_(title, bodyContent) {
  return '<!DOCTYPE html><html><head><meta charset="utf-8"></head>' +
    '<body style="margin:0;padding:0;background:#000;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;">' +
    '<table width="100%" cellpadding="0" cellspacing="0" style="background:#000;"><tr><td align="center" style="padding:20px;">' +
      '<table width="600" cellpadding="0" cellspacing="0" style="background:#0d0d0d;border:1px solid rgba(255,45,45,0.2);border-radius:12px;overflow:hidden;">' +

        // Header
        '<tr><td style="background:linear-gradient(135deg,#1a0000,#0d0d0d);padding:24px 32px;border-bottom:1px solid rgba(255,45,45,0.15);">' +
          '<table width="100%"><tr>' +
            '<td style="color:#ff2d2d;font-size:20px;font-weight:bold;">๐Ÿซ ' + escHtml_(CONFIG.SCHOOL_NAME) + '</td>' +
          '</tr></table>' +
        '</td></tr>' +

        // Title bar
        '<tr><td style="padding:20px 32px 0;">' +
          '<h1 style="color:#fff;font-size:22px;margin:0 0 4px;font-weight:600;">' + title + '</h1>' +
          '<div style="width:40px;height:3px;background:#ff2d2d;border-radius:2px;"></div>' +
        '</td></tr>' +

        // Body
        '<tr><td style="padding:20px 32px 32px;">' + bodyContent + '</td></tr>' +

        // Footer
        '<tr><td style="padding:20px 32px;border-top:1px solid rgba(255,255,255,0.05);text-align:center;">' +
          '<p style="color:#444;font-size:11px;margin:0;">' + CONFIG.SCHOOL_NAME + ' โ€” Expense Tracker</p>' +
          '<p style="color:#333;font-size:10px;margin:4px 0 0;">Automated by Mission Control</p>' +
        '</td></tr>' +

      '</table>' +
    '</td></tr></table></body></html>';
}

function buildRow_(label, value) {
  return '<tr>' +
    '<td style="padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#999;width:40%;">' + label + '</td>' +
    '<td style="padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#fff;">' + value + '</td>' +
  '</tr>';
}
โญ Google Review Demo Page 7KB ยท 308 linesGSโ–ถ
/**
 * Google Review Demo Landing Page
 * 
 * Simulates what happens when a student clicks "Leave a Review" in the email.
 * Shows a branded demo page instead of the real Google review form.
 * 
 * Deploy as: Web App (Anyone can access)
 */

function doGet(e) {
  const studentName = e && e.parameter && e.parameter.name ? e.parameter.name : 'Student';
  const schoolName = e && e.parameter && e.parameter.school ? e.parameter.school : 'Flavors Driving School';
  
  const html = `<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Leave a Review โ€” ${schoolName}</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    
    body {
      background: #000000;
      font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif;
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 20px;
    }
    
    .container {
      max-width: 480px;
      width: 100%;
      background: #0d0d0d;
      border-radius: 24px;
      border: 1px solid rgba(255,255,255,0.06);
      overflow: hidden;
      box-shadow: 0 12px 40px rgba(0,0,0,0.6);
    }
    
    .header {
      padding: 40px 32px 24px;
      text-align: center;
      background: linear-gradient(180deg, rgba(255,45,45,0.08) 0%, transparent 100%);
    }
    
    .stars { font-size: 42px; letter-spacing: 6px; margin-bottom: 16px; }
    
    .header h1 {
      color: #ffffff;
      font-size: 22px;
      font-weight: 700;
      margin-bottom: 6px;
    }
    
    .header p {
      color: #888;
      font-size: 14px;
    }
    
    .school-name { color: #ff2d2d; font-weight: 600; }
    
    .review-box {
      padding: 24px 32px;
    }
    
    .star-rating {
      display: flex;
      justify-content: center;
      gap: 8px;
      margin-bottom: 20px;
    }
    
    .star-btn {
      background: none;
      border: none;
      font-size: 36px;
      cursor: pointer;
      filter: grayscale(1) opacity(0.4);
      transition: all 0.2s ease;
    }
    
    .star-btn.active,
    .star-btn:hover {
      filter: none;
      transform: scale(1.15);
    }
    
    .review-textarea {
      width: 100%;
      min-height: 120px;
      background: #1a1a1a;
      border: 1px solid rgba(255,255,255,0.08);
      border-radius: 14px;
      padding: 16px;
      color: #fff;
      font-size: 14px;
      font-family: inherit;
      resize: vertical;
      outline: none;
      transition: border-color 0.2s;
    }
    
    .review-textarea:focus {
      border-color: rgba(255,45,45,0.4);
    }
    
    .review-textarea::placeholder {
      color: #555;
    }
    
    .submit-btn {
      display: block;
      width: 100%;
      margin-top: 16px;
      padding: 14px;
      background: linear-gradient(135deg, #ff2d2d, #cc0000);
      color: #fff;
      font-size: 16px;
      font-weight: 700;
      border: none;
      border-radius: 14px;
      cursor: pointer;
      transition: all 0.2s;
      box-shadow: 0 4px 16px rgba(255,45,45,0.3);
    }
    
    .submit-btn:hover {
      transform: translateY(-1px);
      box-shadow: 0 6px 24px rgba(255,45,45,0.4);
    }
    
    /* Demo banner */
    .demo-banner {
      background: linear-gradient(135deg, #1a0a0a, #0d0d0d);
      border-top: 1px solid rgba(255,45,45,0.15);
      padding: 16px 32px;
      text-align: center;
    }
    
    .demo-badge {
      display: inline-block;
      background: rgba(255,45,45,0.12);
      color: #ff6b6b;
      font-size: 11px;
      font-weight: 700;
      letter-spacing: 1.5px;
      text-transform: uppercase;
      padding: 5px 14px;
      border-radius: 20px;
      border: 1px solid rgba(255,45,45,0.2);
    }
    
    .demo-text {
      color: #555;
      font-size: 12px;
      margin-top: 8px;
      line-height: 1.5;
    }
    
    /* Success state */
    .success-overlay {
      display: none;
      position: fixed;
      top: 0; left: 0; right: 0; bottom: 0;
      background: rgba(0,0,0,0.85);
      z-index: 100;
      align-items: center;
      justify-content: center;
    }
    
    .success-card {
      background: #0d0d0d;
      border-radius: 24px;
      border: 1px solid rgba(255,255,255,0.06);
      padding: 48px 40px;
      text-align: center;
      max-width: 400px;
      width: 90%;
      box-shadow: 0 12px 40px rgba(0,0,0,0.6);
    }
    
    .success-icon { font-size: 56px; margin-bottom: 16px; }
    
    .success-card h2 {
      color: #fff;
      font-size: 20px;
      margin-bottom: 8px;
    }
    
    .success-card p {
      color: #888;
      font-size: 14px;
      line-height: 1.6;
    }
    
    .success-note {
      margin-top: 20px;
      color: #ff6b6b;
      font-size: 12px;
      font-weight: 600;
    }

    /* Powered by footer */
    .powered-by {
      padding: 12px 32px 16px;
      text-align: center;
    }
    
    .powered-by span {
      color: #333;
      font-size: 11px;
    }
  </style>
</head>
<body>

<div class="container">
  <div class="header">
    <div class="stars">โญโญโญโญโญ</div>
    <h1>How was your experience?</h1>
    <p>Tell us about your time at <span class="school-name">${schoolName}</span></p>
  </div>
  
  <div class="review-box">
    <div class="star-rating" id="starRating">
      <button class="star-btn" data-rating="1" onclick="setRating(1)">โญ</button>
      <button class="star-btn" data-rating="2" onclick="setRating(2)">โญ</button>
      <button class="star-btn" data-rating="3" onclick="setRating(3)">โญ</button>
      <button class="star-btn" data-rating="4" onclick="setRating(4)">โญ</button>
      <button class="star-btn" data-rating="5" onclick="setRating(5)">โญ</button>
    </div>
    
    <textarea class="review-textarea" placeholder="Share your experience... What did you enjoy most about your driving lessons?"></textarea>
    
    <button class="submit-btn" onclick="submitReview()">Submit Review</button>
  </div>
  
  <div class="demo-banner">
    <div class="demo-badge">โœฆ Demo Mode</div>
    <p class="demo-text">This is a demo review page. In production, this button links directly to your Google Business review form.</p>
  </div>
  
  <div class="powered-by">
    <span>Powered by Automated Review System</span>
  </div>
</div>

<!-- Success overlay -->
<div class="success-overlay" id="successOverlay">
  <div class="success-card">
    <div class="success-icon">๐ŸŽ‰</div>
    <h2>Thank you for your review!</h2>
    <p>Your feedback helps us improve and helps future students find great driving instruction.</p>
    <p class="success-note">โšก DEMO โ€” In production, this submits to Google Reviews</p>
  </div>
</div>

<script>
  let selectedRating = 0;
  
  function setRating(rating) {
    selectedRating = rating;
    const buttons = document.querySelectorAll('.star-btn');
    buttons.forEach((btn, i) => {
      btn.classList.toggle('active', i < rating);
    });
  }
  
  function submitReview() {
    const overlay = document.getElementById('successOverlay');
    overlay.style.display = 'flex';
    
    setTimeout(() => {
      overlay.style.display = 'none';
    }, 4000);
  }
  
  // Hover effect for stars
  const stars = document.querySelectorAll('.star-btn');
  stars.forEach((star, i) => {
    star.addEventListener('mouseenter', () => {
      stars.forEach((s, j) => {
        s.classList.toggle('active', j <= i);
      });
    });
    star.addEventListener('mouseleave', () => {
      stars.forEach((s, j) => {
        s.classList.toggle('active', j < selectedRating);
      });
    });
  });
</script>

</body>
</html>`;
  
  return HtmlService.createHtmlOutput(html)
    .setTitle('Leave a Review โ€” ' + schoolName)
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function forceAuth() {
  Logger.log('Auth complete.');
}
๐ŸŒŸ Google Review Request 34KB ยท 876 linesGSโ–ถ
/**
 * =========================================================
 *  GOOGLE REVIEW REQUEST
 *  Flavors Driving School
 * =========================================================
 *  Automatically asks students for a Google review after
 *  completing their lesson package.
 *
 *  - Detects completion from Schedule Board attendance
 *  - 24h delay after last lesson (configurable)
 *  - Review request email + 7-day reminder + 14-day 2nd nudge
 *  - Review tracking with conversion stats
 *  - Thank-you email when review is confirmed
 *  - Incentive text (ties into Referral Rewards)
 *  - Demo mode for safe presentations
 * =========================================================
 */

const CONFIG = {
  // โ”€โ”€ Sheet IDs โ”€โ”€
  REVIEW_SHEET_ID:    '1GcitpI5ULneXiKSLwBB_-VnHOJiJ22qXxwn8ZAgJklc',
  REGISTRATION_ID:    '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',
  SCHEDULE_BOARD_ID:  '1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE',

  // โ”€โ”€ Sheet tabs โ”€โ”€
  REGISTRATION_SHEET_TAB: '',
  BOOKINGS_SHEET_TAB:     'Bookings',

  // โ”€โ”€ Google Review URL โ”€โ”€
  // Real URL (swap DEMO_REVIEW_URL to this when going live):
  // https://search.google.com/local/writereview?placeid=ChIJ-0FMs_VdwokReC3dHtvqXnQ
  GOOGLE_REVIEW_URL: 'https://search.google.com/local/writereview?placeid=DEMO_PLACEHOLDER',
  GOOGLE_PLACE_ID:   'ChIJ-0FMs_VdwokReC3dHtvqXnQ',

  // โ”€โ”€ Branding โ”€โ”€
  SCHOOL_NAME:    'Flavors Driving School',
  SCHOOL_TAGLINE: 'Your Road to Freedom Starts Here',
  ADMIN_EMAIL:    '[email protected]',
  SCHOOL_PHONE:   '(718) 555-0100',

  TIMEZONE: 'America/New_York',

  // โ”€โ”€ Timing โ”€โ”€
  DELAY_HOURS:        24,
  REMINDER_1_DAYS:    7,
  REMINDER_2_DAYS:    14,

  // โ”€โ”€ Limits โ”€โ”€
  MAX_EMAILS_PER_RUN: 20,

  // โ”€โ”€ Packages โ”€โ”€
  PACKAGES: {
    '3 Lessons':    3,
    '5 Lessons':    5,
    '10 Lessons':   10,
    '15 Lessons':   15,
    '25 Lessons':   25,
    '5-Hour Class': 1,
    'Beginner':     10,
    'Standard':     10,
    'Premium':      15,
    'Intensive':    20
  },

  LESSON_PACKAGES_ONLY: false,

  // โ”€โ”€ Confirmed attendance statuses โ”€โ”€
  CONFIRMED_STATUSES: ['completed', 'on time', 'late', 'present', 'attended'],
  SKIP_STATUSES:      ['cancelled', 'canceled', 'no-show', 'no show', 'pending', 'scheduled', 'upcoming', 'rescheduled'],

  // โ”€โ”€ Incentive text โ”€โ”€
  INCENTIVE_TEXT: 'As a thank you, mention your review when you refer a friend and you\'ll both get $25 off!',

  // โ”€โ”€ Demo mode โ”€โ”€
  DEMO_MODE: true
};

/* ================================================================
   DEMO DATA
   ================================================================ */
const DEMO = {
  completedStudents: [
    { name: 'Sarah Johnson',   email: '[email protected]',   pkg: 'Premium Package',  lessons: 15 },
    { name: 'Marcus Williams', email: '[email protected]',  pkg: '10 Lessons',        lessons: 10 }
  ],
  stats: {
    totalRequested: 24,
    totalReviewed:  14,
    conversionRate: '58.3%',
    avgDaysToReview: 3.2,
    pendingReminders: 5
  }
};

/* ================================================================
   SETUP & AUTH
   ================================================================ */

function forceAuth() {
  SpreadsheetApp.openById(CONFIG.REVIEW_SHEET_ID).getSheetByName('test_auth_ignore');
  SpreadsheetApp.openById(CONFIG.REGISTRATION_ID).getSheetByName('test_auth_ignore');
  SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID).getSheetByName('test_auth_ignore');
  MailApp.getRemainingDailyQuota();
  Logger.log('โœ… All permissions authorized. You can close this now.');
}

function fullSetup() {
  const ss = SpreadsheetApp.openById(CONFIG.REVIEW_SHEET_ID);

  // โ”€โ”€ Review Tracker sheet โ”€โ”€
  let tracker = ss.getSheetByName('Review Tracker');
  if (!tracker) {
    tracker = ss.insertSheet('Review Tracker');
    tracker.appendRow([
      'Student Name', 'Email', 'Phone', 'Package', 'Lessons Completed',
      'Lessons Purchased', 'Completion Date', 'Request Sent', 'Sent Date',
      'Reminder 1', 'Reminder 2', 'Status', 'Review Date', 'Unsubscribe'
    ]);
    styleHeader_(tracker, 14);
    Logger.log('โœ… Created "Review Tracker" sheet.');
  }

  // โ”€โ”€ Settings sheet โ”€โ”€
  let settings = ss.getSheetByName('Settings');
  if (!settings) {
    settings = ss.insertSheet('Settings');
    const rows = [
      ['Setting', 'Value'],
      ['Google Review URL', CONFIG.GOOGLE_REVIEW_URL],
      ['School Name', CONFIG.SCHOOL_NAME],
      ['Delay Hours After Last Lesson', CONFIG.DELAY_HOURS],
      ['Send to 5-Hour Class Students', 'Yes'],
      ['Reminder 1 After Days', CONFIG.REMINDER_1_DAYS],
      ['Reminder 2 After Days', CONFIG.REMINDER_2_DAYS],
      ['Incentive Text', CONFIG.INCENTIVE_TEXT],
      ['Admin Email', CONFIG.ADMIN_EMAIL]
    ];
    settings.getRange(1, 1, rows.length, 2).setValues(rows);
    styleHeader_(settings, 2);
    settings.setColumnWidth(1, 280);
    settings.setColumnWidth(2, 400);
    settings.getRange(2, 1, rows.length - 1, 1).setFontColor('#888888');
    settings.getRange(2, 2, rows.length - 1, 1).setFontColor('#ffffff').setFontWeight('bold');
    settings.getRange(1, 1, rows.length, 2).setBackground('#0a0a0a');
    settings.getRange(2, 2).setFontColor('#ff2d2d').setFontSize(11);
    Logger.log('โœ… Created "Settings" sheet.');
  }

  // โ”€โ”€ Review Stats sheet โ”€โ”€
  let statsSheet = ss.getSheetByName('Review Stats');
  if (!statsSheet) {
    statsSheet = ss.insertSheet('Review Stats');
    statsSheet.appendRow(['Metric', 'Value']);
    styleHeader_(statsSheet, 2);
    Logger.log('โœ… Created "Review Stats" sheet.');
  }

  // โ”€โ”€ Triggers โ”€โ”€
  ScriptApp.getProjectTriggers().forEach(t => {
    if (t.getHandlerFunction() === 'checkForCompletedStudents') ScriptApp.deleteTrigger(t);
  });
  ScriptApp.newTrigger('checkForCompletedStudents')
    .timeBased().everyDays(1).atHour(10).nearMinute(0).create();
  Logger.log('โœ… Daily trigger set: 10 AM.');

  Logger.log('โœ… Google Review Request setup complete.');
  Logger.log('โš ๏ธ IMPORTANT: Update the Google Review URL in the Settings sheet!');
}

function styleHeader_(sheet, numCols) {
  sheet.getRange(1, 1, 1, numCols).setBackground('#1a1a1a').setFontColor('#ff2d2d').setFontWeight('bold').setFontSize(10);
  sheet.setFrozenRows(1);
}

/* ================================================================
   DAILY CHECK
   ================================================================ */

function checkForCompletedStudents() {
  try {
    if (CONFIG.DEMO_MODE) {
      Logger.log('๐ŸŽญ DEMO MODE โ€” would check for completed students.');
      Logger.log('Would find: ' + DEMO.completedStudents.map(s => s.name).join(', '));
      return;
    }
    runDailyCheck_();
  } catch (e) {
    Logger.log('checkForCompletedStudents error: ' + (e.message || e));
    notifyAdmin_('Review Request โ€” Daily Check Failed', String(e.message || e).substring(0, 500));
    throw e;
  }
}

function runDailyCheck_() {
  const ss = SpreadsheetApp.openById(CONFIG.REVIEW_SHEET_ID);
  const settings = getSettings_(ss);

  let tracker = ss.getSheetByName('Review Tracker');
  if (!tracker) { fullSetup(); tracker = ss.getSheetByName('Review Tracker'); }

  // โ”€โ”€ Already tracked (by email + name) โ”€โ”€
  const trackerData = tracker.getDataRange().getValues();
  const trackerHeaders = trackerData[0].map(h => String(h).toLowerCase().trim());
  const tEmailCol = findCol_(trackerHeaders, ['email']);
  const tNameCol  = findCol_(trackerHeaders, ['student name', 'name']);

  const tracked = new Set();
  for (let i = 1; i < trackerData.length; i++) {
    tracked.add(String(trackerData[i][tNameCol] || '').toLowerCase().trim());
    if (tEmailCol >= 0 && trackerData[i][tEmailCol]) {
      tracked.add(String(trackerData[i][tEmailCol]).toLowerCase().trim());
    }
  }

  // โ”€โ”€ Get registered students โ”€โ”€
  const students = getRegisteredStudents_();

  // โ”€โ”€ Get attendance โ”€โ”€
  const attendance = getAttendance_();

  const skipFiveHour = CONFIG.LESSON_PACKAGES_ONLY || settings.includeFiveHour === false;
  let newCompletions = 0;

  for (const student of students) {
    // Already tracked?
    if (tracked.has(student.email.toLowerCase())) continue;
    if (tracked.has(student.name.toLowerCase())) continue;

    // Skip 5-Hour if configured
    if (skipFiveHour && student.pkg === '5-Hour Class') continue;

    if (student.lessonCount <= 0) continue;

    // โ”€โ”€ Email-first attendance lookup, then fuzzy name โ”€โ”€
    let completed = 0;
    let lastDate = null;

    if (student.email && attendance.byEmail[student.email.toLowerCase()]) {
      const data = attendance.byEmail[student.email.toLowerCase()];
      completed = data.count;
      lastDate = data.lastDate;
    }

    if (completed === 0) {
      const nameKey = student.name.toLowerCase().trim();
      // Exact name
      if (attendance.byName[nameKey]) {
        completed = attendance.byName[nameKey].count;
        lastDate = attendance.byName[nameKey].lastDate;
      } else {
        // Fuzzy match
        const match = fuzzyFind_(nameKey, Object.keys(attendance.byName));
        if (match) {
          completed = attendance.byName[match].count;
          lastDate = attendance.byName[match].lastDate;
        }
      }
    }

    if (completed >= student.lessonCount) {
      tracker.appendRow([
        student.name, student.email, student.phone, student.pkg,
        completed, student.lessonCount,
        lastDate || new Date(),
        'No', '', 'No', 'No', 'Pending', '', ''
      ]);
      tracked.add(student.email.toLowerCase());
      tracked.add(student.name.toLowerCase());
      newCompletions++;
    }
  }

  // โ”€โ”€ Send requests + reminders โ”€โ”€
  const requestsSent = sendPendingRequests_(ss, settings);
  const remindersSent = sendReminders_(ss, settings);

  // โ”€โ”€ Update stats โ”€โ”€
  updateStats_(ss);

  Logger.log('Daily check: ' + newCompletions + ' new, ' + requestsSent + ' requests, ' + remindersSent + ' reminders.');
}

/* ================================================================
   SEND REVIEW REQUESTS
   ================================================================ */

function sendPendingRequests_(ss, settings) {
  const tracker = ss.getSheetByName('Review Tracker');
  if (!tracker || tracker.getLastRow() < 2) return 0;

  const data = tracker.getDataRange().getValues();
  const h = data[0].map(c => String(c || '').toLowerCase().trim());
  const nameCol    = findCol_(h, ['student name', 'name']);
  const emailCol   = findCol_(h, ['email']);
  const compCol    = findCol_(h, ['completion date']);
  const sentCol    = findCol_(h, ['request sent']);
  const sentDtCol  = findCol_(h, ['sent date']);
  const statusCol  = findCol_(h, ['status']);
  const unsubCol   = findCol_(h, ['unsubscribe']);

  const reviewUrl = safeUrl_(settings.reviewUrl) || CONFIG.GOOGLE_REVIEW_URL;
  let sent = 0;

  for (let i = 1; i < data.length && sent < CONFIG.MAX_EMAILS_PER_RUN; i++) {
    const row = data[i];
    if (String(row[sentCol] || '').toLowerCase() === 'yes') continue;
    if (unsubCol >= 0 && String(row[unsubCol] || '').toLowerCase() === 'yes') continue;

    const name  = String(row[nameCol] || '').trim();
    const email = String(row[emailCol] || '').trim();
    if (!email.includes('@')) continue;

    // Check delay
    let compDate = row[compCol];
    if (compDate && !(compDate instanceof Date)) compDate = new Date(compDate);
    if (compDate instanceof Date && !isNaN(compDate.getTime())) {
      const hoursSince = (Date.now() - compDate.getTime()) / 3600000;
      if (hoursSince < (settings.delayHours || CONFIG.DELAY_HOURS)) continue;
    }

    const html = buildReviewEmail_(name, reviewUrl, settings.schoolName, settings.incentiveText);

    try {
      MailApp.sendEmail({
        to: email,
        subject: 'How was your experience at ' + settings.schoolName + '? โญ',
        body: 'Hi ' + name.split(' ')[0] + '! We\'d love to hear about your experience. Leave a review: ' + reviewUrl,
        htmlBody: html,
        name: CONFIG.SCHOOL_NAME
      });

      const r = i + 1;
      tracker.getRange(r, sentCol + 1).setValue('Yes').setBackground('#0a2e0a').setFontColor('#22c55e');
      if (sentDtCol >= 0) tracker.getRange(r, sentDtCol + 1).setValue(new Date());
      if (statusCol >= 0) tracker.getRange(r, statusCol + 1).setValue('Requested');
      sent++;
    } catch (e) {
      Logger.log('Review email failed for ' + email + ': ' + e.message);
    }
  }

  // Admin summary
  if (sent > 0) {
    notifyAdmin_('๐Ÿ“ฌ ' + sent + ' Review Request(s) Sent', sent + ' students were sent a Google review request today.');
  }

  return sent;
}

/* ================================================================
   REMINDERS (1st at 7 days, 2nd at 14 days)
   ================================================================ */

function sendReminders_(ss, settings) {
  const tracker = ss.getSheetByName('Review Tracker');
  if (!tracker || tracker.getLastRow() < 2) return 0;

  const data = tracker.getDataRange().getValues();
  const h = data[0].map(c => String(c || '').toLowerCase().trim());
  const nameCol   = findCol_(h, ['student name', 'name']);
  const emailCol  = findCol_(h, ['email']);
  const sentCol   = findCol_(h, ['request sent']);
  const sentDtCol = findCol_(h, ['sent date']);
  const rem1Col   = findCol_(h, ['reminder 1']);
  const rem2Col   = findCol_(h, ['reminder 2']);
  const statusCol = findCol_(h, ['status']);
  const unsubCol  = findCol_(h, ['unsubscribe']);

  const reviewUrl = safeUrl_(settings.reviewUrl) || CONFIG.GOOGLE_REVIEW_URL;
  const rem1Days = settings.reminder1Days || CONFIG.REMINDER_1_DAYS;
  const rem2Days = settings.reminder2Days || CONFIG.REMINDER_2_DAYS;
  let sent = 0;

  for (let i = 1; i < data.length && sent < CONFIG.MAX_EMAILS_PER_RUN; i++) {
    const row = data[i];
    if (String(row[sentCol] || '').toLowerCase() !== 'yes') continue;
    if (unsubCol >= 0 && String(row[unsubCol] || '').toLowerCase() === 'yes') continue;

    const status = statusCol >= 0 ? String(row[statusCol] || '').toLowerCase() : '';
    if (status === 'reviewed' || status === 'thanked') continue;

    const name  = String(row[nameCol] || '').trim();
    const email = String(row[emailCol] || '').trim();
    if (!email.includes('@')) continue;

    let sentDate = row[sentDtCol];
    if (sentDate && !(sentDate instanceof Date)) sentDate = new Date(sentDate);
    if (!(sentDate instanceof Date) || isNaN(sentDate.getTime())) continue;

    const daysSince = (Date.now() - sentDate.getTime()) / 86400000;
    const rem1Sent = rem1Col >= 0 ? String(row[rem1Col] || '').toLowerCase() === 'yes' : false;
    const rem2Sent = rem2Col >= 0 ? String(row[rem2Col] || '').toLowerCase() === 'yes' : false;

    let sendReminder = false;
    let reminderNum = 0;

    if (!rem1Sent && daysSince >= rem1Days) {
      sendReminder = true;
      reminderNum = 1;
    } else if (rem1Sent && !rem2Sent && daysSince >= rem2Days) {
      sendReminder = true;
      reminderNum = 2;
    }

    if (!sendReminder) continue;

    const html = buildReminderEmail_(name, reviewUrl, settings.schoolName, reminderNum);

    try {
      MailApp.sendEmail({
        to: email,
        subject: (reminderNum === 2 ? 'Last chance โ€” ' : 'Quick reminder โ€” ') + 'we\'d love your feedback! โญ',
        body: 'Hi ' + name.split(' ')[0] + '! Quick reminder to leave us a review: ' + reviewUrl,
        htmlBody: html,
        name: CONFIG.SCHOOL_NAME
      });

      const r = i + 1;
      if (reminderNum === 1 && rem1Col >= 0) tracker.getRange(r, rem1Col + 1).setValue('Yes');
      if (reminderNum === 2 && rem2Col >= 0) tracker.getRange(r, rem2Col + 1).setValue('Yes');
      if (statusCol >= 0) tracker.getRange(r, statusCol + 1).setValue('Reminder ' + reminderNum);
      sent++;
    } catch (e) {
      Logger.log('Reminder failed for ' + email + ': ' + e.message);
    }
  }

  return sent;
}

/* ================================================================
   THANK YOU EMAIL (call when status manually set to "Reviewed")
   ================================================================ */

function sendThankYouEmails() {
  if (CONFIG.DEMO_MODE) { Logger.log('๐ŸŽญ DEMO MODE'); return; }

  const ss = SpreadsheetApp.openById(CONFIG.REVIEW_SHEET_ID);
  const tracker = ss.getSheetByName('Review Tracker');
  if (!tracker || tracker.getLastRow() < 2) return;

  const data = tracker.getDataRange().getValues();
  const h = data[0].map(c => String(c || '').toLowerCase().trim());
  const nameCol   = findCol_(h, ['student name', 'name']);
  const emailCol  = findCol_(h, ['email']);
  const statusCol = findCol_(h, ['status']);
  const revDtCol  = findCol_(h, ['review date']);
  const settings  = getSettings_(ss);

  for (let i = 1; i < data.length; i++) {
    const status = statusCol >= 0 ? String(data[i][statusCol] || '').toLowerCase() : '';
    if (status !== 'reviewed') continue;

    const name  = String(data[i][nameCol] || '').trim();
    const email = String(data[i][emailCol] || '').trim();
    if (!email.includes('@')) continue;

    const firstName = esc_(name.split(' ')[0]);
    const sch = esc_(settings.schoolName);
    const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe&body=Remove%20' + encodeURIComponent(email);

    const html = emailWrap_(
      '<div style="text-align:center;">'
      + '<div style="font-size:48px;margin-bottom:12px;">๐Ÿ™โค๏ธ</div>'
      + '<div style="font-size:22px;font-weight:800;color:#fff;">Thank You, ' + firstName + '!</div></div>',
      '<p style="font-size:14px;color:rgba(255,255,255,0.6);line-height:1.7;text-align:center;">'
      + 'Your review means the world to us! It helps other students find quality driving instruction and motivates our team to keep doing great work.</p>'
      + '<p style="font-size:13px;color:rgba(255,255,255,0.5);text-align:center;margin-top:16px;">'
      + esc_(CONFIG.INCENTIVE_TEXT) + '</p>',
      unsub
    );

    try {
      MailApp.sendEmail({
        to: email,
        subject: 'Thank you for your review! โค๏ธ โ€” ' + settings.schoolName,
        body: 'Thank you for leaving a review, ' + name.split(' ')[0] + '! We really appreciate it.',
        htmlBody: html, name: CONFIG.SCHOOL_NAME
      });

      const r = i + 1;
      if (statusCol >= 0) tracker.getRange(r, statusCol + 1).setValue('Thanked');
      if (revDtCol >= 0 && !data[i][revDtCol]) tracker.getRange(r, revDtCol + 1).setValue(new Date());
    } catch (e) {
      Logger.log('Thank you email failed for ' + email + ': ' + e.message);
    }
  }
}

/* ================================================================
   REVIEW STATS
   ================================================================ */

function updateStats_(ss) {
  let statsSheet = ss.getSheetByName('Review Stats');
  if (!statsSheet) return;

  const tracker = ss.getSheetByName('Review Tracker');
  if (!tracker || tracker.getLastRow() < 2) return;

  const data = tracker.getDataRange().getValues();
  const h = data[0].map(c => String(c || '').toLowerCase().trim());
  const statusCol = findCol_(h, ['status']);
  const sentDtCol = findCol_(h, ['sent date']);
  const revDtCol  = findCol_(h, ['review date']);

  let requested = 0, reviewed = 0, pending = 0, totalDays = 0;

  for (let i = 1; i < data.length; i++) {
    const status = statusCol >= 0 ? String(data[i][statusCol] || '').toLowerCase() : '';
    if (status === 'requested' || status.includes('reminder') || status === 'reviewed' || status === 'thanked') requested++;
    if (status === 'reviewed' || status === 'thanked') {
      reviewed++;
      if (sentDtCol >= 0 && revDtCol >= 0 && data[i][sentDtCol] instanceof Date && data[i][revDtCol] instanceof Date) {
        totalDays += (data[i][revDtCol] - data[i][sentDtCol]) / 86400000;
      }
    }
    if (status === 'pending') pending++;
  }

  const rate = requested > 0 ? (reviewed / requested * 100).toFixed(1) + '%' : '0%';
  const avgDays = reviewed > 0 ? (totalDays / reviewed).toFixed(1) : 'N/A';

  const rows = [
    ['Metric', 'Value'],
    ['๐Ÿ“ฌ Total Requested', requested],
    ['โญ Total Reviewed', reviewed],
    ['๐Ÿ“Š Conversion Rate', rate],
    ['โฑ๏ธ Avg Days to Review', avgDays],
    ['โณ Pending (not yet sent)', pending],
    ['', ''],
    ['Last Updated', Utilities.formatDate(new Date(), CONFIG.TIMEZONE, 'M/d/yyyy h:mm a')]
  ];

  statsSheet.clear();
  statsSheet.getRange(1, 1, rows.length, 2).setValues(rows);
  styleHeader_(statsSheet, 2);
  statsSheet.getRange(1, 1, rows.length, 2).setBackground('#0a0a0a').setFontColor('#cccccc');
  statsSheet.getRange(2, 2, 5, 1).setFontColor('#fff').setFontWeight('bold').setFontSize(13);
  statsSheet.getRange(4, 2).setFontColor('#22c55e');
  statsSheet.setColumnWidth(1, 250);
  statsSheet.setColumnWidth(2, 200);
}

/** Get review stats for BI Dashboard integration */
function getReviewStats() {
  if (CONFIG.DEMO_MODE) return DEMO.stats;

  const ss = SpreadsheetApp.openById(CONFIG.REVIEW_SHEET_ID);
  const tracker = ss.getSheetByName('Review Tracker');
  if (!tracker || tracker.getLastRow() < 2) return { totalRequested: 0, totalReviewed: 0, conversionRate: '0%' };

  const data = tracker.getDataRange().getValues();
  const h = data[0].map(c => String(c || '').toLowerCase().trim());
  const statusCol = findCol_(h, ['status']);
  let requested = 0, reviewed = 0;

  for (let i = 1; i < data.length; i++) {
    const status = statusCol >= 0 ? String(data[i][statusCol] || '').toLowerCase() : '';
    if (status !== 'pending' && status) requested++;
    if (status === 'reviewed' || status === 'thanked') reviewed++;
  }

  return {
    totalRequested: requested,
    totalReviewed: reviewed,
    conversionRate: requested > 0 ? (reviewed / requested * 100).toFixed(1) + '%' : '0%'
  };
}

/* ================================================================
   DATA FUNCTIONS
   ================================================================ */

function getRegisteredStudents_() {
  const students = [];
  try {
    const regSS = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
    const regSheet = CONFIG.REGISTRATION_SHEET_TAB
      ? (regSS.getSheetByName(CONFIG.REGISTRATION_SHEET_TAB) || regSS.getSheets()[0])
      : regSS.getSheets()[0];
    if (!regSheet || regSheet.getLastRow() < 2) return students;

    const data = regSheet.getDataRange().getValues();
    const h = data[0].map(c => String(c || '').toLowerCase().trim());
    const nameCol  = findCol_(h, ['student name', 'full name', 'name']);
    const emailCol = findCol_(h, ['email', 'student email']);
    const phoneCol = findCol_(h, ['phone', 'phone number', 'mobile']);
    const pkgCol   = findCol_(h, ['package', 'lesson package', 'program']);

    const seen = new Set();
    for (let i = 1; i < data.length; i++) {
      const name = sanitize_(String(data[i][nameCol] || '').trim());
      if (!name) continue;
      const email = emailCol >= 0 ? String(data[i][emailCol] || '').trim().toLowerCase() : '';
      const phone = phoneCol >= 0 ? String(data[i][phoneCol] || '').trim() : '';
      const pkgRaw = pkgCol >= 0 ? String(data[i][pkgCol] || '') : '';

      // Dedup by email first, then name
      const key = email || name.toLowerCase();
      if (seen.has(key)) continue;
      seen.add(key);

      const { pkg, count } = resolvePackage_(pkgRaw);
      students.push({ name, email, phone, pkg, lessonCount: count });
    }
  } catch (e) {
    Logger.log('Registration read error: ' + e.message);
  }
  return students;
}

function resolvePackage_(pkgRaw) {
  const raw = String(pkgRaw || '').toLowerCase().trim();
  if (!raw) return { pkg: 'Unknown', count: 0 };

  const keys = Object.keys(CONFIG.PACKAGES).sort((a, b) => b.length - a.length);
  for (const key of keys) {
    if (raw.includes(key.toLowerCase())) return { pkg: key, count: CONFIG.PACKAGES[key] };
  }

  const numMatch = raw.match(/(\d+)/);
  if (numMatch) {
    const num = numMatch[1];
    for (const key of keys) {
      if (key.includes(num)) return { pkg: key, count: CONFIG.PACKAGES[key] };
    }
  }

  return { pkg: pkgRaw || 'Unknown', count: 0 };
}

function getAttendance_() {
  const byEmail = {};
  const byName = {};

  try {
    const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
    const sheet = CONFIG.BOOKINGS_SHEET_TAB
      ? (ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB) || ss.getSheets()[0])
      : ss.getSheets()[0];
    if (!sheet || sheet.getLastRow() < 2) return { byEmail, byName };

    const data = sheet.getDataRange().getValues();
    const h = data[0].map(c => String(c || '').toLowerCase().trim());
    const nameCol   = findCol_(h, ['student name', 'student', 'name']);
    const emailCol  = findCol_(h, ['student email', 'email']);
    const statusCol = findCol_(h, ['status', 'attendance', 'booking status']);
    const dateCol   = findCol_(h, ['date', 'lesson date']);
    const now = new Date();

    for (let i = 1; i < data.length; i++) {
      const row = data[i];
      const status = statusCol >= 0 ? String(row[statusCol] || '').toLowerCase().trim() : '';

      if (!isConfirmedStatus_(status)) continue;

      // Only past lessons
      if (dateCol >= 0 && row[dateCol]) {
        const d = row[dateCol] instanceof Date ? row[dateCol] : new Date(row[dateCol]);
        if (!isNaN(d.getTime()) && d > now) continue;
      }

      const name = nameCol >= 0 ? String(row[nameCol] || '').trim() : '';
      const email = emailCol >= 0 ? String(row[emailCol] || '').trim().toLowerCase() : '';
      const lessonDate = dateCol >= 0 ? (row[dateCol] instanceof Date ? row[dateCol] : new Date(row[dateCol])) : null;
      const validDate = lessonDate && !isNaN(lessonDate.getTime()) ? lessonDate : null;

      if (email) {
        if (!byEmail[email]) byEmail[email] = { count: 0, lastDate: null };
        byEmail[email].count++;
        if (validDate && (!byEmail[email].lastDate || validDate > byEmail[email].lastDate)) byEmail[email].lastDate = validDate;
      }
      if (name) {
        const nk = name.toLowerCase();
        if (!byName[nk]) byName[nk] = { count: 0, lastDate: null };
        byName[nk].count++;
        if (validDate && (!byName[nk].lastDate || validDate > byName[nk].lastDate)) byName[nk].lastDate = validDate;
      }
    }
  } catch (e) {
    Logger.log('Attendance read error: ' + e.message);
  }

  return { byEmail, byName };
}

function isConfirmedStatus_(status) {
  const s = String(status || '').toLowerCase().trim();
  if (CONFIG.SKIP_STATUSES.some(skip => s === skip)) return false;
  return CONFIG.CONFIRMED_STATUSES.some(ok => s === ok);
}

/* ================================================================
   SETTINGS READER
   ================================================================ */

function getSettings_(ss) {
  const defaults = {
    reviewUrl: CONFIG.GOOGLE_REVIEW_URL,
    schoolName: CONFIG.SCHOOL_NAME,
    delayHours: CONFIG.DELAY_HOURS,
    reminder1Days: CONFIG.REMINDER_1_DAYS,
    reminder2Days: CONFIG.REMINDER_2_DAYS,
    incentiveText: CONFIG.INCENTIVE_TEXT,
    adminEmail: CONFIG.ADMIN_EMAIL,
    includeFiveHour: !CONFIG.LESSON_PACKAGES_ONLY
  };

  const sheet = ss.getSheetByName('Settings');
  if (!sheet || sheet.getLastRow() < 2) return defaults;

  const data = sheet.getDataRange().getValues();
  for (let i = 1; i < data.length; i++) {
    const key = String(data[i][0] || '').toLowerCase();
    const val = String(data[i][1] || '').trim();
    if (!key || !val) continue;

    if (key.includes('review url'))    defaults.reviewUrl = val;
    else if (key.includes('school name'))   defaults.schoolName = val;
    else if (key.includes('delay hours'))   defaults.delayHours = parseInt(val, 10) || 24;
    else if (key.includes('5-hour'))        defaults.includeFiveHour = val.toLowerCase() === 'yes';
    else if (key.includes('reminder 1'))    defaults.reminder1Days = parseInt(val, 10) || 7;
    else if (key.includes('reminder 2'))    defaults.reminder2Days = parseInt(val, 10) || 14;
    else if (key.includes('incentive'))     defaults.incentiveText = val;
    else if (key.includes('admin'))         defaults.adminEmail = val;
  }
  return defaults;
}

/* ================================================================
   EMAIL TEMPLATES (Mission Control theme)
   ================================================================ */

function buildReviewEmail_(name, reviewUrl, schoolName, incentiveText) {
  const f = esc_(name.split(' ')[0]);
  const sch = esc_(schoolName);
  const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe%20Review%20Emails&body=Please%20remove%20me.';

  return emailWrap_(
    '<div style="text-align:center;">'
    + '<div style="font-size:40px;letter-spacing:4px;">โญโญโญโญโญ</div>'
    + '<h1 style="margin:16px 0 0;color:#fff;font-size:22px;">Congratulations, ' + f + '!</h1>'
    + '<p style="margin:8px 0 0;color:#888;font-size:14px;">You\'ve completed your driving lessons at <span style="color:#ff2d2d;font-weight:600;">' + sch + '</span></p></div>',
    '<p style="color:rgba(255,255,255,0.6);font-size:14px;line-height:1.7;text-align:center;">'
    + 'We hope you had an amazing experience! Your feedback helps other students find great instruction and means the world to our team.</p>'
    + '<p style="color:rgba(255,255,255,0.6);font-size:14px;text-align:center;">Would you take 30 seconds to share your experience?</p>'
    + '<div style="text-align:center;margin:24px 0;">'
    + '<a href="' + reviewUrl + '" style="display:inline-block;background:#ff2d2d;color:#fff;font-size:16px;font-weight:700;padding:14px 36px;border-radius:12px;text-decoration:none;">โญ Leave a Review</a>'
    + '<p style="margin:8px 0 0;color:rgba(255,255,255,0.3);font-size:12px;">Takes less than 30 seconds</p></div>'
    + (incentiveText ? '<p style="font-size:12px;color:rgba(255,255,255,0.4);text-align:center;margin-top:16px;padding:12px;background:rgba(255,45,45,0.06);border:1px solid rgba(255,45,45,0.1);border-radius:8px;">๐Ÿ’ก ' + esc_(incentiveText) + '</p>' : ''),
    unsub
  );
}

function buildReminderEmail_(name, reviewUrl, schoolName, reminderNum) {
  const f = esc_(name.split(' ')[0]);
  const sch = esc_(schoolName);
  const isLast = reminderNum === 2;
  const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe%20Review%20Emails&body=Please%20remove%20me.';

  const emoji = isLast ? '๐Ÿ™' : '๐Ÿ’ฌ';
  const title = isLast ? 'Last chance, ' + f + '!' : 'Hey ' + f + ', quick reminder!';
  const text = isLast
    ? 'This is our last nudge โ€” we promise! If you have 30 seconds, we\'d really appreciate a quick review.'
    : 'We\'d really appreciate it if you could take a moment to leave us a review. Your feedback helps future students find us!';

  return emailWrap_(
    '<div style="text-align:center;"><div style="font-size:32px;">' + emoji + '</div>'
    + '<h1 style="margin:12px 0 0;color:#fff;font-size:20px;">' + title + '</h1></div>',
    '<p style="color:rgba(255,255,255,0.6);font-size:14px;line-height:1.7;text-align:center;">' + text + '</p>'
    + '<div style="text-align:center;margin:24px 0;">'
    + '<a href="' + reviewUrl + '" style="display:inline-block;background:#ff2d2d;color:#fff;font-size:15px;font-weight:700;padding:12px 32px;border-radius:12px;text-decoration:none;">โญ Leave a Review</a></div>',
    unsub
  );
}

function emailWrap_(header, body, unsubLink) {
  const sch = esc_(CONFIG.SCHOOL_NAME);
  const tag = esc_(CONFIG.SCHOOL_TAGLINE);
  return '<!DOCTYPE html><html><head><meta charset="utf-8"></head>'
    + '<body style="margin:0;padding:0;background:#000;font-family:Arial,Helvetica,sans-serif;">'
    + '<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#000;"><tr><td align="center" style="padding:20px;">'
    + '<table width="560" cellpadding="0" cellspacing="0" border="0" style="background:#0d0d0d;border-radius:16px;border:1px solid rgba(255,255,255,0.06);overflow:hidden;">'
    + '<tr><td style="padding:32px 40px 16px;">' + header + '</td></tr>'
    + '<tr><td style="padding:8px 40px 24px;">' + body + '</td></tr>'
    + '<tr><td style="padding:16px 40px;border-top:1px solid rgba(255,255,255,0.06);text-align:center;">'
    + '<p style="margin:0;color:rgba(255,255,255,0.2);font-size:12px;">' + sch + ' โ€” ' + tag + '</p>'
    + (unsubLink ? '<p style="font-size:10px;margin:6px 0 0;"><a href="' + unsubLink + '" style="color:rgba(255,255,255,0.15);text-decoration:underline;">Unsubscribe</a></p>' : '')
    + '</td></tr></table></td></tr></table></body></html>';
}

/* ================================================================
   SHARED HELPERS
   ================================================================ */

function findCol_(headers, candidates) {
  for (let i = 0; i < headers.length; i++) {
    const h = String(headers[i] || '').toLowerCase().trim();
    for (const kw of candidates) {
      if (kw && h.includes(String(kw).toLowerCase().trim())) return i;
    }
  }
  return -1;
}

function safeUrl_(url) {
  const u = String(url || '').trim();
  return (u.startsWith('https://') || u.startsWith('http://')) ? u : '';
}

function esc_(str) {
  if (str == null) return '';
  return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function sanitize_(str) {
  return String(str || '').replace(/[<>{}()\[\]\\\/]/g, '').substring(0, 200).trim();
}

function fuzzyFind_(target, keys) {
  if (!target) return null;
  if (keys.includes(target)) return target;
  const tParts = target.split(/\s+/);
  const tFirst = tParts[0] || '';
  const tLast = tParts[tParts.length - 1] || '';

  for (const key of keys) {
    if (levenshtein_(target, key) <= 2) return key;
    const kParts = key.split(/\s+/);
    const kFirst = kParts[0] || '';
    const kLast = kParts[kParts.length - 1] || '';
    if (tFirst.length >= 3 && kFirst.length >= 3 && tFirst.substring(0, 3) === kFirst.substring(0, 3) && tLast === kLast) return key;
    if (tFirst === kLast && tLast === kFirst) return key;
  }
  return null;
}

function levenshtein_(a, b) {
  const m = a.length, n = b.length;
  const dp = Array.from({ length: m + 1 }, (_, i) => i);
  for (let j = 1; j <= n; j++) {
    let prev = dp[0]; dp[0] = j;
    for (let i = 1; i <= m; i++) {
      const temp = dp[i];
      dp[i] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[i], dp[i - 1]);
      prev = temp;
    }
  }
  return dp[m];
}

function notifyAdmin_(subject, body) {
  if (!CONFIG.ADMIN_EMAIL) return;
  try { MailApp.sendEmail(CONFIG.ADMIN_EMAIL, subject, body, { name: CONFIG.SCHOOL_NAME }); } catch (_) {}
}

/* ================================================================
   MANUAL TOOLS
   ================================================================ */

function testDemoReview() {
  Logger.log('๐ŸŽญ Demo review stats: ' + JSON.stringify(DEMO.stats));
  Logger.log('Would send to: ' + DEMO.completedStudents.map(s => s.name).join(', '));
}

function testReviewEmail() {
  if (CONFIG.DEMO_MODE) { Logger.log('๐ŸŽญ DEMO MODE'); return; }
  const ss = SpreadsheetApp.openById(CONFIG.REVIEW_SHEET_ID);
  const settings = getSettings_(ss);
  const url = safeUrl_(settings.reviewUrl) || CONFIG.GOOGLE_REVIEW_URL;
  const html = buildReviewEmail_('Test Student', url, settings.schoolName, settings.incentiveText);
  MailApp.sendEmail({ to: CONFIG.ADMIN_EMAIL, subject: 'โญ [TEST] Review Request', body: 'Test', htmlBody: html, name: CONFIG.SCHOOL_NAME });
  Logger.log('Test email sent to ' + CONFIG.ADMIN_EMAIL);
}
๐Ÿ“… Instructor Schedule Board 50KB ยท 1,250 linesGSโ–ถ
/**
 * =========================================================
 *  INSTRUCTOR SCHEDULE BOARD
 *  Flavors Driving School
 * =========================================================
 *  Automated scheduling system with:
 *  - Double-booking prevention
 *  - Booking confirmation emails (Mission Control theme)
 *  - Weekly visual grid (auto-refresh)
 *  - Cancellation + reschedule flows
 *  - Daily instructor schedule emails
 *  - Waitlist auto-fill on cancellation
 *  - Student booking history API
 *  - Utilization stats
 *  - Past-date + daily booking limit validation
 *  - Demo mode for safe presentations
 *
 *  SHEETS:
 *    "Bookings"      โ€” Master booking log
 *    "Instructors"   โ€” Instructor info
 *    "Weekly View"   โ€” Auto-generated visual board
 *    "Booking Log"   โ€” Audit trail
 * =========================================================
 */

const CONFIG = {
  // โ”€โ”€ Sheet ID โ”€โ”€
  SCHEDULE_BOARD_ID: '1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE',
  REGISTRATION_ID:   '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',

  // โ”€โ”€ Sheet tabs โ”€โ”€
  BOOKINGS_SHEET_TAB: 'Bookings',
  INSTRUCTORS_TAB:    'Instructors',
  WEEKLY_VIEW_TAB:    'Weekly View',
  WAITLIST_TAB:       'Waitlist',

  // โ”€โ”€ Branding โ”€โ”€
  SCHOOL_NAME:    'Flavors Driving School',
  SCHOOL_TAGLINE: 'Your Road to Freedom Starts Here',
  ADMIN_EMAIL:    '[email protected]',
  SCHOOL_PHONE:   '(718) 555-0100',

  // โ”€โ”€ Instructors โ”€โ”€
  INSTRUCTORS: [
    { name: 'Anisha', car: 'Car 1 (update model)', email: '' },
    { name: 'Carlos', car: 'Car 2 (update model)', email: '' },
    { name: 'Nick',   car: 'Car 3 (update model)', email: '' }
  ],

  // โ”€โ”€ Schedule settings โ”€โ”€
  START_HOUR: 9,
  END_HOUR:   18,
  SLOT_DURATION_MIN: 60,
  OPERATING_DAYS: [0, 1, 2, 3, 4, 5, 6],
  TIMEZONE: 'America/New_York',
  MAX_BOOKINGS_PER_STUDENT_PER_DAY: 2,

  TIME_SLOTS: [
    '9:00 AM', '10:00 AM', '11:00 AM', '12:00 PM',
    '1:00 PM', '2:00 PM', '3:00 PM', '4:00 PM', '5:00 PM'
  ],

  FORM_RESPONSE_SHEET: 'Form Responses 1',

  // โ”€โ”€ Demo mode โ”€โ”€
  DEMO_MODE: true
};

/* ================================================================
   DEMO DATA
   ================================================================ */
const DEMO = {
  booking: {
    id: 'BK-0042',
    instructor: 'Anisha',
    student: 'Sarah Johnson',
    email: '[email protected]',
    phone: '917-555-0123',
    date: '02/22/2026',
    time: '10:00 AM',
    car: 'Honda Civic 2023',
    status: 'Confirmed'
  },
  weekSummary: {
    totalBookings: 18,
    utilization: { Anisha: 67, Carlos: 56, Nick: 44 },
    cancelledThisWeek: 2
  }
};

/* ================================================================
   SETUP & AUTH
   ================================================================ */

function forceAuth() {
  SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID).getSheetByName('test_auth_ignore');
  SpreadsheetApp.openById(CONFIG.REGISTRATION_ID).getSheetByName('test_auth_ignore');
  MailApp.getRemainingDailyQuota();
  FormApp.getActiveForm();
  Logger.log('โœ… All permissions authorized. You can close this now.');
}

function fullSetup() {
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);

  // โ”€โ”€ Bookings sheet (create if missing, don't wipe if exists) โ”€โ”€
  let bookSheet = ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB);
  if (!bookSheet) {
    bookSheet = ss.insertSheet(CONFIG.BOOKINGS_SHEET_TAB);
    bookSheet.appendRow([
      'Booking ID', 'Timestamp', 'Instructor', 'Student Name', 'Student Email',
      'Student Phone', 'Date', 'Time Slot', 'Status', 'Car', 'Notes', 'Confirmed By'
    ]);
    styleHeader_(bookSheet, 12);
    Logger.log('โœ… Created "' + CONFIG.BOOKINGS_SHEET_TAB + '" sheet.');
  }

  // โ”€โ”€ Instructors sheet (create if missing, don't wipe if exists) โ”€โ”€
  let instSheet = ss.getSheetByName(CONFIG.INSTRUCTORS_TAB);
  if (!instSheet) {
    instSheet = ss.insertSheet(CONFIG.INSTRUCTORS_TAB);
    instSheet.appendRow(['Instructor Name', 'Car / Vehicle', 'Email', 'Phone', 'Notes']);
    for (const inst of CONFIG.INSTRUCTORS) {
      instSheet.appendRow([inst.name, inst.car, inst.email, '', '']);
    }
    styleHeader_(instSheet, 5);
    Logger.log('โœ… Created "' + CONFIG.INSTRUCTORS_TAB + '" sheet.');
  }

  // โ”€โ”€ Weekly View โ”€โ”€
  let weekSheet = ss.getSheetByName(CONFIG.WEEKLY_VIEW_TAB);
  if (!weekSheet) {
    ss.insertSheet(CONFIG.WEEKLY_VIEW_TAB);
    Logger.log('โœ… Created "' + CONFIG.WEEKLY_VIEW_TAB + '" sheet.');
  }

  // โ”€โ”€ Booking Log (audit trail) โ”€โ”€
  let logSheet = ss.getSheetByName('Booking Log');
  if (!logSheet) {
    logSheet = ss.insertSheet('Booking Log');
    logSheet.appendRow([
      'Timestamp', 'Action', 'Booking ID', 'Student', 'Instructor',
      'Date', 'Time', 'Status', 'Details'
    ]);
    styleHeader_(logSheet, 9);
    Logger.log('โœ… Created "Booking Log" sheet.');
  }

  // โ”€โ”€ Triggers (clean old first) โ”€โ”€
  ScriptApp.getProjectTriggers().forEach(t => {
    const fn = t.getHandlerFunction();
    if (['onFormSubmit', 'generateWeeklyView', 'sendDailyInstructorSchedules'].includes(fn)) {
      ScriptApp.deleteTrigger(t);
    }
  });

  // Daily midnight โ€” refresh weekly view
  ScriptApp.newTrigger('generateWeeklyView')
    .timeBased().everyDays(1).atHour(0).nearMinute(5).create();

  // Daily 7 AM โ€” email instructors their schedule
  ScriptApp.newTrigger('sendDailyInstructorSchedules')
    .timeBased().everyDays(1).atHour(7).create();

  Logger.log('โœ… Triggers set: Weekly View midnight, Instructor Schedules 7 AM.');
  Logger.log('โœ… Instructor Schedule Board setup complete.');
  Logger.log('๐Ÿ“Œ Next: Update instructor car models and emails in the Instructors sheet.');
}

function styleHeader_(sheet, numCols) {
  const range = sheet.getRange(1, 1, 1, numCols);
  range.setBackground('#1a1a1a').setFontColor('#ff2d2d').setFontWeight('bold');
  sheet.setFrozenRows(1);
}

/* ================================================================
   BOOKING FUNCTION
   ================================================================ */

function bookLesson(instructor, studentName, studentEmail, studentPhone, dateStr, timeSlot, notes) {
  if (CONFIG.DEMO_MODE) {
    Logger.log('๐ŸŽญ DEMO MODE โ€” would book: ' + studentName + ' with ' + instructor + ' on ' + dateStr + ' at ' + timeSlot);
    return { success: true, bookingId: DEMO.booking.id, message: '๐ŸŽญ Demo booking created.' };
  }

  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const bookSheet = ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB) || ss.getSheets()[0];
  const tz = CONFIG.TIMEZONE;

  // Sanitize inputs
  studentName = sanitize_(studentName);
  studentEmail = String(studentEmail || '').trim().toLowerCase();
  studentPhone = String(studentPhone || '').trim();
  notes = sanitize_(notes || '');
  instructor = sanitize_(instructor);

  // Normalize date
  const parsedDate = parseDate_(dateStr);
  const normalizedDateStr = parsedDate ? fmtDate_(parsedDate) : String(dateStr).trim();

  // โ”€โ”€ Validate operating day โ”€โ”€
  if (parsedDate && !CONFIG.OPERATING_DAYS.includes(parsedDate.getDay())) {
    return { success: false, message: 'We are closed on that day. Please choose an operating day.' };
  }

  // โ”€โ”€ Validate not in the past โ”€โ”€
  if (parsedDate) {
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    if (parsedDate < today) {
      return { success: false, message: 'Cannot book lessons in the past. Please choose a future date.' };
    }
  }

  // โ”€โ”€ Conflict check โ”€โ”€
  const conflict = checkConflict_(ss, instructor, normalizedDateStr, timeSlot);
  if (conflict) {
    const available = getAvailableSlots(normalizedDateStr, instructor);
    const altSlots = (available[instructor] || []).slice(0, 3).join(', ');
    const altMsg = altSlots ? ' Available slots: ' + altSlots + '.' : '';
    return {
      success: false,
      message: 'CONFLICT: ' + instructor + ' is already booked at ' + timeSlot + ' on ' + normalizedDateStr + ' with ' + conflict.student + '.' + altMsg
    };
  }

  // โ”€โ”€ Daily booking limit โ”€โ”€
  if (studentEmail && CONFIG.MAX_BOOKINGS_PER_STUDENT_PER_DAY > 0) {
    const todayCount = countStudentBookingsOnDate_(ss, studentEmail, studentName, normalizedDateStr);
    if (todayCount >= CONFIG.MAX_BOOKINGS_PER_STUDENT_PER_DAY) {
      return { success: false, message: 'Booking limit reached: max ' + CONFIG.MAX_BOOKINGS_PER_STUDENT_PER_DAY + ' lessons per day.' };
    }
  }

  // โ”€โ”€ Generate booking ID โ”€โ”€
  const bookingId = generateBookingId_(bookSheet);
  const car = getInstructorCar_(ss, instructor);

  try {
    bookSheet.appendRow([
      bookingId, new Date(), instructor, studentName, studentEmail,
      studentPhone, normalizedDateStr, timeSlot, 'Confirmed', car, notes, 'System'
    ]);

    sendBookingConfirmation_(bookingId, instructor, studentName, studentEmail, normalizedDateStr, timeSlot, car);
    logAction_('BOOK', bookingId, studentName, instructor, normalizedDateStr, timeSlot, 'Confirmed', '');

    // Refresh weekly view (async-safe)
    try { generateWeeklyView(); } catch (_) {}

    return {
      success: true,
      bookingId: bookingId,
      message: 'Lesson booked! ' + studentName + ' with ' + instructor + ' on ' + normalizedDateStr + ' at ' + timeSlot + '. Booking ID: ' + bookingId
    };
  } catch (err) {
    Logger.log('bookLesson error: ' + (err.message || err));
    return { success: false, message: 'Booking could not be saved: ' + (err.message || String(err)) };
  }
}

/* ================================================================
   BOOKING ID GENERATOR (short, memorable)
   ================================================================ */

function generateBookingId_(bookSheet) {
  let maxNum = 0;
  if (bookSheet && bookSheet.getLastRow() > 1) {
    const ids = bookSheet.getRange(2, 1, bookSheet.getLastRow() - 1, 1).getValues();
    for (const row of ids) {
      const match = String(row[0] || '').match(/BK-(\d+)/);
      if (match) {
        const num = parseInt(match[1], 10);
        if (num > maxNum) maxNum = num;
      }
    }
  }
  return 'BK-' + String(maxNum + 1).padStart(4, '0');
}

/* ================================================================
   CONFLICT CHECK
   ================================================================ */

function checkConflict_(ss, instructor, dateStr, timeSlot) {
  const bookSheet = ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB) || ss.getSheets()[0];
  if (!bookSheet || bookSheet.getLastRow() < 2) return null;

  const data = bookSheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const instCol   = findCol_(headers, ['instructor', 'instructor name']);
  const dateCol   = findCol_(headers, ['date', 'lesson date']);
  const timeCol   = findCol_(headers, ['time slot', 'time']);
  const statusCol = findCol_(headers, ['status', 'booking status']);
  const nameCol   = findCol_(headers, ['student name', 'student', 'name']);

  if (instCol < 0 || dateCol < 0 || timeCol < 0) return null;

  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    const status = String(row[statusCol] != null ? row[statusCol] : '').toLowerCase();
    if (status === 'cancelled' || status === 'canceled') continue;

    const rowInst = String(row[instCol] || '').trim();
    const rowDate = fmtDate_(row[dateCol]);
    const rowTime = String(row[timeCol] || '').trim();

    if (rowInst === instructor && rowDate === dateStr && rowTime === timeSlot) {
      return { student: nameCol >= 0 ? String(row[nameCol] || '') : 'Unknown' };
    }
  }
  return null;
}

/* ================================================================
   DAILY BOOKING LIMIT
   ================================================================ */

function countStudentBookingsOnDate_(ss, email, name, dateStr) {
  const bookSheet = ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB);
  if (!bookSheet || bookSheet.getLastRow() < 2) return 0;

  const data = bookSheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const emailCol  = findCol_(headers, ['student email', 'email']);
  const nameCol   = findCol_(headers, ['student name', 'student', 'name']);
  const dateCol   = findCol_(headers, ['date', 'lesson date']);
  const statusCol = findCol_(headers, ['status']);

  let count = 0;
  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    const status = String(row[statusCol] != null ? row[statusCol] : '').toLowerCase();
    if (status === 'cancelled' || status === 'canceled') continue;

    const rowDate = fmtDate_(row[dateCol]);
    if (rowDate !== dateStr) continue;

    // Email-first matching
    if (email && emailCol >= 0 && String(row[emailCol] || '').toLowerCase().trim() === email) { count++; continue; }
    // Fuzzy name fallback
    if (name && nameCol >= 0) {
      const rowName = String(row[nameCol] || '').toLowerCase().trim();
      if (rowName === name.toLowerCase() || levenshtein_(rowName, name.toLowerCase()) <= 2) count++;
    }
  }
  return count;
}

/* ================================================================
   AVAILABLE SLOTS
   ================================================================ */

function getAvailableSlots(dateStr, instructor) {
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const allSlots = CONFIG.TIME_SLOTS;
  const instructors = instructor ? [instructor] : CONFIG.INSTRUCTORS.map(i => i.name);
  const available = {};

  for (const inst of instructors) {
    available[inst] = allSlots.filter(slot => !checkConflict_(ss, inst, dateStr, slot));
  }
  return available;
}

/* ================================================================
   CANCEL BOOKING
   ================================================================ */

function cancelBooking(bookingId) {
  if (CONFIG.DEMO_MODE) {
    Logger.log('๐ŸŽญ DEMO MODE โ€” would cancel booking: ' + bookingId);
    return true;
  }

  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const bookSheet = ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB);
  if (!bookSheet || bookSheet.getLastRow() < 2) return false;

  const data = bookSheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const idCol     = findCol_(headers, ['booking id', 'id']);
  const statusCol = findCol_(headers, ['status']);
  const nameCol   = findCol_(headers, ['student name', 'student']);
  const emailCol  = findCol_(headers, ['student email', 'email']);
  const instCol   = findCol_(headers, ['instructor']);
  const dateCol   = findCol_(headers, ['date']);
  const timeCol   = findCol_(headers, ['time slot', 'time']);

  for (let i = 1; i < data.length; i++) {
    if (String(data[i][idCol] || '').trim() === bookingId) {
      bookSheet.getRange(i + 1, statusCol + 1).setValue('Cancelled');

      const row = data[i];
      const studentName = String(row[nameCol] || '');
      const studentEmail = emailCol >= 0 ? String(row[emailCol] || '') : '';
      const instructor = String(row[instCol] || '');
      const dateStr = fmtDate_(row[dateCol]);
      const timeSlot = String(row[timeCol] || '');

      sendCancellationEmail_(bookingId, studentName, studentEmail, instructor, dateStr, timeSlot);
      logAction_('CANCEL', bookingId, studentName, instructor, dateStr, timeSlot, 'Cancelled', '');

      // โ”€โ”€ Auto-fill from waitlist โ”€โ”€
      tryFillFromWaitlist_(ss, instructor, dateStr, timeSlot);

      try { generateWeeklyView(); } catch (_) {}
      Logger.log('โœ… Booking ' + bookingId + ' cancelled.');
      return true;
    }
  }

  Logger.log('Booking ' + bookingId + ' not found.');
  return false;
}

/* ================================================================
   RESCHEDULE BOOKING
   ================================================================ */

function rescheduleBooking(bookingId, newDateStr, newTimeSlot) {
  if (CONFIG.DEMO_MODE) {
    Logger.log('๐ŸŽญ DEMO MODE โ€” would reschedule ' + bookingId + ' to ' + newDateStr + ' ' + newTimeSlot);
    return { success: true, message: '๐ŸŽญ Demo reschedule.' };
  }

  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const bookSheet = ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB);
  if (!bookSheet || bookSheet.getLastRow() < 2) return { success: false, message: 'No bookings found.' };

  const data = bookSheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const idCol     = findCol_(headers, ['booking id', 'id']);
  const instCol   = findCol_(headers, ['instructor']);
  const nameCol   = findCol_(headers, ['student name', 'student']);
  const emailCol  = findCol_(headers, ['student email', 'email']);
  const phoneCol  = findCol_(headers, ['student phone', 'phone']);
  const dateCol   = findCol_(headers, ['date']);
  const timeCol   = findCol_(headers, ['time slot', 'time']);
  const statusCol = findCol_(headers, ['status']);
  const notesCol  = findCol_(headers, ['notes']);

  for (let i = 1; i < data.length; i++) {
    if (String(data[i][idCol] || '').trim() !== bookingId) continue;

    const row = data[i];
    const oldStatus = String(row[statusCol] || '').toLowerCase();
    if (oldStatus === 'cancelled' || oldStatus === 'canceled') {
      return { success: false, message: 'Cannot reschedule a cancelled booking.' };
    }

    const instructor = String(row[instCol] || '');
    const studentName = String(row[nameCol] || '');
    const studentEmail = emailCol >= 0 ? String(row[emailCol] || '') : '';
    const studentPhone = phoneCol >= 0 ? String(row[phoneCol] || '') : '';
    const oldDate = fmtDate_(row[dateCol]);
    const oldTime = String(row[timeCol] || '');
    const notes = notesCol >= 0 ? String(row[notesCol] || '') : '';

    const newDate = parseDate_(newDateStr);
    const normalizedNew = newDate ? fmtDate_(newDate) : newDateStr;

    // Validate new date not in past
    if (newDate) {
      const today = new Date();
      today.setHours(0, 0, 0, 0);
      if (newDate < today) return { success: false, message: 'Cannot reschedule to a past date.' };
    }

    // Check conflict on new slot
    const conflict = checkConflict_(ss, instructor, normalizedNew, newTimeSlot);
    if (conflict) {
      return { success: false, message: 'CONFLICT: ' + instructor + ' is already booked at ' + newTimeSlot + ' on ' + normalizedNew + '.' };
    }

    // Cancel old
    bookSheet.getRange(i + 1, statusCol + 1).setValue('Rescheduled');
    logAction_('RESCHEDULE', bookingId, studentName, instructor, oldDate, oldTime, 'Rescheduled โ†’ ' + normalizedNew + ' ' + newTimeSlot, '');

    // Create new booking
    const result = bookLesson(instructor, studentName, studentEmail, studentPhone, normalizedNew, newTimeSlot, notes + ' (rescheduled from ' + oldDate + ' ' + oldTime + ')');

    // Notify student about reschedule
    if (studentEmail) {
      sendRescheduleEmail_(studentName, studentEmail, instructor, oldDate, oldTime, normalizedNew, newTimeSlot, result.bookingId || '');
    }

    // Try fill old slot from waitlist
    tryFillFromWaitlist_(ss, instructor, oldDate, oldTime);

    return result;
  }

  return { success: false, message: 'Booking ' + bookingId + ' not found.' };
}

/* ================================================================
   WAITLIST AUTO-FILL
   ================================================================ */

function tryFillFromWaitlist_(ss, instructor, dateStr, timeSlot) {
  try {
    const waitSheet = ss.getSheetByName(CONFIG.WAITLIST_TAB);
    if (!waitSheet || waitSheet.getLastRow() < 2) return;

    const data = waitSheet.getDataRange().getValues();
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const wNameCol  = findCol_(headers, ['student', 'student name', 'name']);
    const wEmailCol = findCol_(headers, ['email', 'student email']);
    const wInstCol  = findCol_(headers, ['instructor', 'preferred instructor']);
    const wDateCol  = findCol_(headers, ['date', 'preferred date']);
    const wTimeCol  = findCol_(headers, ['time', 'preferred time', 'time slot']);
    const wStatusCol = findCol_(headers, ['status', 'waitlist status']);

    for (let i = 1; i < data.length; i++) {
      const row = data[i];
      const status = wStatusCol >= 0 ? String(row[wStatusCol] || '').toLowerCase() : '';
      if (status === 'filled' || status === 'cancelled') continue;

      const wInst = wInstCol >= 0 ? String(row[wInstCol] || '').trim() : '';
      const wDate = wDateCol >= 0 ? fmtDate_(row[wDateCol]) : '';
      const wTime = wTimeCol >= 0 ? String(row[wTimeCol] || '').trim() : '';

      // Match instructor + date + time (or any instructor if blank)
      if ((!wInst || wInst === instructor) && (!wDate || wDate === dateStr) && (!wTime || wTime === timeSlot)) {
        const name = wNameCol >= 0 ? String(row[wNameCol] || '') : '';
        const email = wEmailCol >= 0 ? String(row[wEmailCol] || '') : '';

        if (name && email) {
          // Notify waitlisted student about the opening
          sendWaitlistOfferEmail_(name, email, instructor, dateStr, timeSlot);
          if (wStatusCol >= 0) {
            waitSheet.getRange(i + 1, wStatusCol + 1).setValue('Offered');
          }
          logAction_('WAITLIST_OFFER', '', name, instructor, dateStr, timeSlot, 'Offered open slot', '');
          Logger.log('๐Ÿ“‹ Waitlist offer sent to ' + name + ' for ' + instructor + ' ' + dateStr + ' ' + timeSlot);
          return; // Only offer to first match
        }
      }
    }
  } catch (e) {
    Logger.log('Waitlist auto-fill error: ' + e.message);
  }
}

/* ================================================================
   STUDENT BOOKING HISTORY (API for Student Portal)
   ================================================================ */

function getStudentBookings(email) {
  if (CONFIG.DEMO_MODE) {
    return [
      { bookingId: 'BK-0040', instructor: 'Anisha', date: '02/20/2026', time: '10:00 AM', status: 'Confirmed', car: 'Honda Civic 2023' },
      { bookingId: 'BK-0038', instructor: 'Carlos', date: '02/18/2026', time: '2:00 PM',  status: 'Completed', car: 'Toyota Corolla 2022' },
      { bookingId: 'BK-0035', instructor: 'Anisha', date: '02/15/2026', time: '11:00 AM', status: 'Completed', car: 'Honda Civic 2023' }
    ];
  }

  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const bookSheet = ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB);
  if (!bookSheet || bookSheet.getLastRow() < 2) return [];

  const data = bookSheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const idCol     = findCol_(headers, ['booking id', 'id']);
  const instCol   = findCol_(headers, ['instructor']);
  const nameCol   = findCol_(headers, ['student name', 'student']);
  const emailCol  = findCol_(headers, ['student email', 'email']);
  const dateCol   = findCol_(headers, ['date']);
  const timeCol   = findCol_(headers, ['time slot', 'time']);
  const statusCol = findCol_(headers, ['status']);
  const carCol    = findCol_(headers, ['car', 'vehicle']);

  const emailLower = String(email || '').toLowerCase().trim();
  const results = [];

  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    const rowEmail = emailCol >= 0 ? String(row[emailCol] || '').toLowerCase().trim() : '';
    if (rowEmail !== emailLower) continue;

    results.push({
      bookingId:  idCol >= 0 ? String(row[idCol] || '') : '',
      instructor: instCol >= 0 ? String(row[instCol] || '') : '',
      date:       dateCol >= 0 ? fmtDate_(row[dateCol]) : '',
      time:       timeCol >= 0 ? String(row[timeCol] || '') : '',
      status:     statusCol >= 0 ? String(row[statusCol] || '') : '',
      car:        carCol >= 0 ? String(row[carCol] || '') : ''
    });
  }

  // Sort by date descending
  results.sort((a, b) => {
    const da = new Date(a.date), db = new Date(b.date);
    return db - da;
  });

  return results;
}

/* ================================================================
   UTILIZATION STATS
   ================================================================ */

function getWeeklyUtilization() {
  if (CONFIG.DEMO_MODE) return DEMO.weekSummary.utilization;

  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const bookSheet = ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB);
  if (!bookSheet || bookSheet.getLastRow() < 2) return {};

  const today = new Date();
  const dayOfWeek = today.getDay();
  const monday = new Date(today);
  monday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1));
  monday.setHours(0, 0, 0, 0);
  const sunday = new Date(monday);
  sunday.setDate(monday.getDate() + 6);
  sunday.setHours(23, 59, 59, 999);

  const data = bookSheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const instCol   = findCol_(headers, ['instructor']);
  const dateCol   = findCol_(headers, ['date']);
  const statusCol = findCol_(headers, ['status']);

  const counts = {};
  CONFIG.INSTRUCTORS.forEach(i => { counts[i.name] = 0; });

  const maxSlotsPerWeek = CONFIG.TIME_SLOTS.length * 7;

  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    const status = String(row[statusCol] != null ? row[statusCol] : '').toLowerCase();
    if (status === 'cancelled' || status === 'canceled') continue;

    const rowDate = row[dateCol] instanceof Date ? row[dateCol] : new Date(row[dateCol]);
    if (isNaN(rowDate.getTime()) || rowDate < monday || rowDate > sunday) continue;

    const inst = String(row[instCol] || '').trim();
    if (counts[inst] !== undefined) counts[inst]++;
  }

  const util = {};
  for (const [name, count] of Object.entries(counts)) {
    util[name] = Math.round((count / maxSlotsPerWeek) * 100);
  }
  return util;
}

/* ================================================================
   DAILY INSTRUCTOR SCHEDULE EMAILS
   ================================================================ */

function sendDailyInstructorSchedules() {
  if (CONFIG.DEMO_MODE) {
    Logger.log('๐ŸŽญ DEMO MODE โ€” would send daily schedules to instructors.');
    return;
  }

  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const bookSheet = ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB);
  if (!bookSheet || bookSheet.getLastRow() < 2) return;

  const tz = CONFIG.TIMEZONE;
  const today = new Date();
  const todayStr = Utilities.formatDate(today, tz, 'MM/dd/yyyy');
  const todayNice = Utilities.formatDate(today, tz, 'EEEE, MMMM d');

  const data = bookSheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const instCol   = findCol_(headers, ['instructor']);
  const nameCol   = findCol_(headers, ['student name', 'student']);
  const dateCol   = findCol_(headers, ['date']);
  const timeCol   = findCol_(headers, ['time slot', 'time']);
  const statusCol = findCol_(headers, ['status']);
  const carCol    = findCol_(headers, ['car', 'vehicle']);
  const phoneCol  = findCol_(headers, ['student phone', 'phone']);

  // Group by instructor
  const schedules = {};
  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    const status = String(row[statusCol] != null ? row[statusCol] : '').toLowerCase();
    if (status === 'cancelled' || status === 'canceled') continue;

    const rowDate = fmtDate_(row[dateCol]);
    if (rowDate !== todayStr) continue;

    const inst = String(row[instCol] || '').trim();
    if (!schedules[inst]) schedules[inst] = [];
    schedules[inst].push({
      time:    timeCol >= 0 ? String(row[timeCol] || '') : '',
      student: nameCol >= 0 ? String(row[nameCol] || '') : '',
      phone:   phoneCol >= 0 ? String(row[phoneCol] || '') : '',
      car:     carCol >= 0 ? String(row[carCol] || '') : ''
    });
  }

  // Send to each instructor
  const instSheet = ss.getSheetByName(CONFIG.INSTRUCTORS_TAB);
  const instData = instSheet ? instSheet.getDataRange().getValues() : [];

  for (const inst of CONFIG.INSTRUCTORS) {
    const email = getInstructorEmail_(ss, inst.name);
    const lessons = schedules[inst.name] || [];

    // Sort by time
    lessons.sort((a, b) => {
      const ta = parseTimeSlot_(a.time), tb = parseTimeSlot_(b.time);
      return ta - tb;
    });

    // Always send to admin, and to instructor if they have email
    const recipients = [CONFIG.ADMIN_EMAIL];
    if (email && email.includes('@')) recipients.push(email);

    const subject = '๐Ÿ“… ' + inst.name + '\'s Schedule โ€” ' + todayNice + ' (' + lessons.length + ' lessons)';
    const htmlBody = buildDailyScheduleEmail_(inst.name, todayNice, lessons);

    for (const to of recipients) {
      try {
        MailApp.sendEmail({ to: to, subject: subject, body: plainSchedule_(inst.name, todayNice, lessons), htmlBody: htmlBody, name: CONFIG.SCHOOL_NAME });
      } catch (e) {
        Logger.log('Failed to send schedule to ' + to + ': ' + e.message);
      }
    }
  }

  Logger.log('โœ… Daily schedules sent for ' + todayNice);
}

function plainSchedule_(instName, date, lessons) {
  let text = instName + '\'s Schedule โ€” ' + date + '\n\n';
  if (lessons.length === 0) { text += 'No lessons scheduled today. Enjoy your day off!\n'; return text; }
  for (const l of lessons) {
    text += l.time + ' โ€” ' + l.student + (l.phone ? ' (' + l.phone + ')' : '') + '\n';
  }
  return text;
}

function parseTimeSlot_(timeStr) {
  const match = String(timeStr || '').match(/(\d+):(\d+)\s*(AM|PM)/i);
  if (!match) return 0;
  let h = parseInt(match[1], 10);
  const m = parseInt(match[2], 10);
  if (match[3].toUpperCase() === 'PM' && h !== 12) h += 12;
  if (match[3].toUpperCase() === 'AM' && h === 12) h = 0;
  return h * 60 + m;
}

/* ================================================================
   WEEKLY VIEW GENERATOR (batch writes)
   ================================================================ */

function generateWeeklyView() {
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  let weekSheet = ss.getSheetByName(CONFIG.WEEKLY_VIEW_TAB);
  if (!weekSheet) weekSheet = ss.insertSheet(CONFIG.WEEKLY_VIEW_TAB);
  else weekSheet.clear();

  const tz = CONFIG.TIMEZONE;
  const today = new Date();
  const dayOfWeek = today.getDay();
  const monday = new Date(today);
  monday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1));

  const days = [];
  for (let i = 0; i < 7; i++) {
    const d = new Date(monday);
    d.setDate(monday.getDate() + i);
    days.push(d);
  }

  const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
  const instructors = CONFIG.INSTRUCTORS;
  const timeSlots = CONFIG.TIME_SLOTS;

  // Get all bookings for this week
  const bookings = getWeekBookings_(ss, monday);

  // Build all rows in memory
  const allRows = [];

  // Header row 1: day labels
  const headerRow1 = [''];
  for (let d = 0; d < days.length; d++) {
    const dateStr = Utilities.formatDate(days[d], tz, 'MM/dd');
    for (let j = 0; j < instructors.length; j++) {
      headerRow1.push(j === 0 ? dayNames[d] + ' ' + dateStr : '');
    }
  }
  allRows.push(headerRow1);

  // Header row 2: instructor names
  const headerRow2 = ['Time'];
  for (let d = 0; d < days.length; d++) {
    for (const inst of instructors) headerRow2.push(inst.name);
  }
  allRows.push(headerRow2);

  // Data rows
  for (const slot of timeSlots) {
    const row = [slot];
    for (const day of days) {
      const dateStr = Utilities.formatDate(day, tz, 'MM/dd/yyyy');
      for (const inst of instructors) {
        const booking = bookings.find(b => b.instructor === inst.name && b.date === dateStr && b.time === slot);
        row.push(booking ? booking.student : '');
      }
    }
    allRows.push(row);
  }

  // Batch write
  const totalCols = 1 + (7 * instructors.length);
  weekSheet.getRange(1, 1, allRows.length, totalCols).setValues(allRows);

  // โ”€โ”€ Styling โ”€โ”€
  const totalRows = allRows.length;

  weekSheet.getRange(1, 1, 1, totalCols)
    .setBackground('#0d0d0d').setFontColor('#ff2d2d').setFontWeight('bold').setFontSize(11).setHorizontalAlignment('center');
  weekSheet.getRange(2, 1, 1, totalCols)
    .setBackground('#1a1a1a').setFontColor('#ffffff').setFontWeight('bold').setFontSize(10).setHorizontalAlignment('center');
  weekSheet.getRange(3, 1, totalRows - 2, 1)
    .setBackground('#111111').setFontColor('#888888').setFontWeight('bold').setHorizontalAlignment('center');
  weekSheet.getRange(3, 2, totalRows - 2, totalCols - 1)
    .setBackground('#0a0a0a').setFontColor('#cccccc').setHorizontalAlignment('center').setFontSize(9);

  // Highlight booked cells
  for (let r = 3; r <= totalRows; r++) {
    for (let c = 2; c <= totalCols; c++) {
      const cell = weekSheet.getRange(r, c);
      if (String(cell.getValue()).trim()) {
        cell.setBackground('#1a0000').setFontColor('#ff4444').setFontWeight('bold');
        cell.setBorder(true, true, true, true, false, false, '#ff2d2d', SpreadsheetApp.BorderStyle.SOLID);
      }
    }
  }

  weekSheet.getRange(1, 1, totalRows, totalCols).setBorder(true, true, true, true, true, true, '#333333', SpreadsheetApp.BorderStyle.SOLID);
  weekSheet.setColumnWidth(1, 80);
  for (let c = 2; c <= totalCols; c++) weekSheet.setColumnWidth(c, 90);
  weekSheet.setFrozenRows(2);
  weekSheet.setFrozenColumns(1);

  Logger.log('โœ… Weekly view generated for week of ' + Utilities.formatDate(monday, tz, 'MM/dd/yyyy'));
}

function getWeekBookings_(ss, mondayDate) {
  const bookSheet = ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB);
  if (!bookSheet || bookSheet.getLastRow() < 2) return [];

  const data = bookSheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const instCol   = findCol_(headers, ['instructor']);
  const dateCol   = findCol_(headers, ['date']);
  const timeCol   = findCol_(headers, ['time slot', 'time']);
  const statusCol = findCol_(headers, ['status']);
  const nameCol   = findCol_(headers, ['student name', 'student']);

  const mondayStart = new Date(mondayDate);
  mondayStart.setHours(0, 0, 0, 0);
  const sundayEnd = new Date(mondayDate);
  sundayEnd.setDate(mondayDate.getDate() + 6);
  sundayEnd.setHours(23, 59, 59, 999);

  const bookings = [];
  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    const status = String(row[statusCol] != null ? row[statusCol] : '').toLowerCase();
    if (status === 'cancelled' || status === 'canceled') continue;

    const rowDate = row[dateCol] instanceof Date ? row[dateCol] : new Date(row[dateCol]);
    if (isNaN(rowDate.getTime()) || rowDate < mondayStart || rowDate > sundayEnd) continue;

    bookings.push({
      instructor: String(row[instCol] || '').trim(),
      date: fmtDate_(row[dateCol]),
      time: String(row[timeCol] || '').trim(),
      student: String(row[nameCol] || '').trim()
    });
  }
  return bookings;
}

/* ================================================================
   FORM SUBMISSION HANDLER
   ================================================================ */

function onFormSubmit(e) {
  try {
    if (CONFIG.DEMO_MODE) {
      Logger.log('๐ŸŽญ DEMO MODE โ€” form submit ignored.');
      return;
    }

    if (!e || !e.namedValues) {
      Logger.log('onFormSubmit: no event data.');
      return;
    }

    const val = (keys) => {
      for (const k of keys) {
        const v = e.namedValues[k];
        if (v && v[0]) return sanitize_(v[0].trim());
      }
      return '';
    };

    const instructor   = val(['Instructor', 'instructor']);
    const studentName  = val(['Student Name', 'student name', 'Full Name']);
    const studentEmail = val(['Student Email', 'Email', 'email']);
    const studentPhone = val(['Phone', 'phone', 'Student Phone']);
    const dateRaw      = val(['Lesson Date', 'Date', 'date']);
    const timeSlot     = val(['Time Slot', 'Time', 'time slot']);
    const notes        = val(['Notes', 'notes']);

    const dateStr = dateRaw ? fmtDate_(new Date(dateRaw)) : '';

    const result = bookLesson(instructor, studentName, studentEmail, studentPhone, dateStr, timeSlot, notes);

    if (!result.success) {
      // Send conflict notification
      const conflictHtml = buildConflictEmail_(instructor, studentName, dateStr, timeSlot, result.message);
      MailApp.sendEmail({
        to: CONFIG.ADMIN_EMAIL,
        subject: 'โš ๏ธ Booking Conflict โ€” ' + instructor + ' on ' + dateStr,
        body: result.message,
        htmlBody: conflictHtml,
        name: CONFIG.SCHOOL_NAME
      });
      Logger.log('CONFLICT: ' + result.message);
    } else {
      Logger.log('BOOKED: ' + result.message);
    }
  } catch (err) {
    Logger.log('onFormSubmit error: ' + (err.message || err));
    notifyAdmin_('Schedule Board โ€” Form Submit Error', 'onFormSubmit failed: ' + String(err.message || err).substring(0, 500));
    throw err;
  }
}

/* ================================================================
   EMAIL TEMPLATES (Mission Control theme)
   ================================================================ */

function sendBookingConfirmation_(bookingId, instructor, studentName, studentEmail, dateStr, timeSlot, car) {
  const sch = esc_(CONFIG.SCHOOL_NAME);
  const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe&body=Please%20remove%20' + encodeURIComponent(studentEmail) + '%20from%20booking%20emails.';

  const html = emailWrap_(
    iconHeader_('โœ…', '#22c55e', 'Lesson Confirmed'),
    '<table width="100%" cellpadding="0" cellspacing="0" border="0">'
    + detailRow_('Booking ID', esc_(bookingId))
    + detailRow_('Student', esc_(studentName))
    + detailRow_('Instructor', esc_(instructor), '#ff2d2d')
    + detailRow_('Date', esc_(dateStr))
    + detailRow_('Time', esc_(timeSlot))
    + detailRow_('Vehicle', esc_(car))
    + '</table>'
    + '<p style="font-size:11px;color:rgba(255,255,255,0.3);margin:16px 0 0;text-align:center;">Please arrive 10 minutes early. Bring your learner permit and valid photo ID.</p>',
    unsub
  );

  // Admin
  MailApp.sendEmail({
    to: CONFIG.ADMIN_EMAIL,
    subject: 'โœ… Lesson Booked โ€” ' + studentName + ' with ' + instructor + ' on ' + dateStr,
    body: 'Booking confirmed: ' + studentName + ' with ' + instructor + ' on ' + dateStr + ' at ' + timeSlot + '. ID: ' + bookingId,
    htmlBody: html,
    name: CONFIG.SCHOOL_NAME
  });

  // Student
  if (studentEmail && studentEmail.includes('@')) {
    MailApp.sendEmail({
      to: studentEmail,
      subject: 'Your Driving Lesson is Confirmed! โ€” ' + dateStr + ' at ' + timeSlot,
      body: 'Hi ' + studentName + '! Your lesson with ' + instructor + ' on ' + dateStr + ' at ' + timeSlot + ' is confirmed. Booking ID: ' + bookingId,
      htmlBody: html,
      name: CONFIG.SCHOOL_NAME
    });
  }

  // Instructor
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const instEmail = getInstructorEmail_(ss, instructor);
  if (instEmail && instEmail.includes('@')) {
    MailApp.sendEmail({
      to: instEmail,
      subject: '๐Ÿ“… New Lesson โ€” ' + studentName + ' on ' + dateStr + ' at ' + timeSlot,
      body: 'New lesson: ' + studentName + ' on ' + dateStr + ' at ' + timeSlot + '. ID: ' + bookingId,
      htmlBody: html,
      name: CONFIG.SCHOOL_NAME
    });
  }
}

function sendCancellationEmail_(bookingId, studentName, studentEmail, instructor, dateStr, timeSlot) {
  const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe&body=Please%20remove%20me%20from%20booking%20emails.';

  const html = emailWrap_(
    iconHeader_('โŒ', '#ff2d2d', 'Lesson Cancelled'),
    '<table width="100%" cellpadding="0" cellspacing="0" border="0">'
    + detailRow_('Booking ID', esc_(bookingId))
    + detailRow_('Student', esc_(studentName))
    + detailRow_('Instructor', esc_(instructor), '#ff2d2d')
    + detailRow_('Date', esc_(dateStr))
    + detailRow_('Time', esc_(timeSlot))
    + '</table>'
    + '<p style="font-size:13px;color:rgba(255,255,255,0.5);margin:16px 0 0;text-align:center;">Please contact us to rebook when you\'re ready.</p>',
    unsub
  );

  if (studentEmail && studentEmail.includes('@')) {
    MailApp.sendEmail({ to: studentEmail, subject: 'Driving Lesson Cancelled โ€” ' + dateStr + ' at ' + timeSlot,
      body: 'Your lesson on ' + dateStr + ' at ' + timeSlot + ' has been cancelled. Contact us to rebook.', htmlBody: html, name: CONFIG.SCHOOL_NAME });
  }

  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const instEmail = getInstructorEmail_(ss, instructor);
  if (instEmail && instEmail.includes('@')) {
    MailApp.sendEmail({ to: instEmail, subject: 'Lesson Cancelled โ€” ' + studentName + ' on ' + dateStr,
      body: 'Cancelled: ' + studentName + ' on ' + dateStr + ' at ' + timeSlot, htmlBody: html, name: CONFIG.SCHOOL_NAME });
  }
}

function sendRescheduleEmail_(studentName, studentEmail, instructor, oldDate, oldTime, newDate, newTime, newBookingId) {
  const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe&body=Please%20remove%20me%20from%20booking%20emails.';

  const html = emailWrap_(
    iconHeader_('๐Ÿ”„', '#f59e0b', 'Lesson Rescheduled'),
    '<p style="font-size:13px;color:rgba(255,255,255,0.5);margin:0 0 16px;text-align:center;">Your lesson has been moved to a new time.</p>'
    + '<table width="100%" cellpadding="0" cellspacing="0" border="0">'
    + detailRow_('Old Date/Time', esc_(oldDate + ' at ' + oldTime))
    + detailRow_('New Date/Time', esc_(newDate + ' at ' + newTime), '#22c55e')
    + detailRow_('Instructor', esc_(instructor), '#ff2d2d')
    + (newBookingId ? detailRow_('New Booking ID', esc_(newBookingId)) : '')
    + '</table>',
    unsub
  );

  if (studentEmail && studentEmail.includes('@')) {
    MailApp.sendEmail({ to: studentEmail, subject: 'Lesson Rescheduled โ€” Now ' + newDate + ' at ' + newTime,
      body: 'Your lesson has been rescheduled from ' + oldDate + ' ' + oldTime + ' to ' + newDate + ' ' + newTime + ' with ' + instructor + '.',
      htmlBody: html, name: CONFIG.SCHOOL_NAME });
  }
}

function sendWaitlistOfferEmail_(studentName, email, instructor, dateStr, timeSlot) {
  const firstName = (studentName || '').split(' ')[0] || studentName;
  const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe&body=Please%20remove%20me%20from%20waitlist%20emails.';

  const html = emailWrap_(
    iconHeader_('๐ŸŽ‰', '#22c55e', 'A Slot Opened Up!'),
    '<p style="font-size:14px;color:rgba(255,255,255,0.6);line-height:1.7;text-align:center;margin:0 0 16px;">'
    + 'Hi <strong style="color:#fff;">' + esc_(firstName) + '</strong>! A slot you were waiting for just opened up.</p>'
    + '<table width="100%" cellpadding="0" cellspacing="0" border="0">'
    + detailRow_('Instructor', esc_(instructor), '#ff2d2d')
    + detailRow_('Date', esc_(dateStr))
    + detailRow_('Time', esc_(timeSlot))
    + '</table>'
    + '<p style="font-size:13px;color:rgba(255,255,255,0.5);margin:16px 0 0;text-align:center;">Reply to this email or call ' + esc_(CONFIG.SCHOOL_PHONE) + ' to grab this slot before someone else does!</p>',
    unsub
  );

  MailApp.sendEmail({ to: email, subject: '๐ŸŽ‰ Open Slot Available โ€” ' + instructor + ' on ' + dateStr + ' at ' + timeSlot,
    body: 'Hi ' + firstName + '! A slot opened up: ' + instructor + ' on ' + dateStr + ' at ' + timeSlot + '. Contact us to book it!',
    htmlBody: html, name: CONFIG.SCHOOL_NAME });
}

function buildConflictEmail_(instructor, studentName, dateStr, timeSlot, message) {
  return emailWrap_(
    iconHeader_('โš ๏ธ', '#ff2d2d', 'Double-Booking Detected'),
    '<p style="font-size:14px;color:rgba(255,255,255,0.6);line-height:1.5;margin:0 0 16px;">' + esc_(message) + '</p>'
    + '<p style="font-size:13px;color:rgba(255,255,255,0.4);">Please contact <strong style="color:#fff;">' + esc_(studentName) + '</strong> to reschedule.</p>',
    ''
  );
}

function buildDailyScheduleEmail_(instName, dateNice, lessons) {
  let body = '';
  if (lessons.length === 0) {
    body = '<div style="text-align:center;padding:20px;">'
      + '<div style="font-size:48px;margin-bottom:12px;">โ˜€๏ธ</div>'
      + '<p style="font-size:16px;color:#fff;font-weight:700;">No lessons today!</p>'
      + '<p style="font-size:13px;color:rgba(255,255,255,0.4);">Enjoy your day off.</p></div>';
  } else {
    body = '<table width="100%" cellpadding="0" cellspacing="4" border="0">';
    for (const l of lessons) {
      body += '<tr><td style="padding:12px;background:rgba(255,255,255,0.03);border-radius:8px;border-left:3px solid #ff2d2d;">'
        + '<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr>'
        + '<td style="width:80px;vertical-align:top;"><div style="font-size:14px;color:#ff2d2d;font-weight:700;">' + esc_(l.time) + '</div></td>'
        + '<td><div style="font-size:14px;color:#fff;font-weight:600;">' + esc_(l.student) + '</div>'
        + (l.phone ? '<div style="font-size:11px;color:rgba(255,255,255,0.4);margin-top:2px;">๐Ÿ“ž ' + esc_(l.phone) + '</div>' : '')
        + (l.car ? '<div style="font-size:11px;color:rgba(255,255,255,0.4);margin-top:2px;">๐Ÿš— ' + esc_(l.car) + '</div>' : '')
        + '</td></tr></table></td></tr>';
    }
    body += '</table>';
  }

  return emailWrap_(
    '<div style="text-align:center;padding-bottom:8px;">'
    + '<div style="font-size:18px;font-weight:800;color:#fff;">' + esc_(instName) + '\'s Schedule</div>'
    + '<div style="font-size:12px;color:rgba(255,255,255,0.4);margin-top:4px;">' + esc_(dateNice) + ' โ€” ' + lessons.length + ' lesson' + (lessons.length !== 1 ? 's' : '') + '</div></div>',
    body, ''
  );
}

/* ================================================================
   EMAIL BUILDING BLOCKS
   ================================================================ */

function emailWrap_(header, body, unsubLink) {
  const sch = esc_(CONFIG.SCHOOL_NAME);
  const tag = esc_(CONFIG.SCHOOL_TAGLINE);
  return '<!DOCTYPE html><html><head><meta charset="utf-8"></head>'
    + '<body style="margin:0;padding:0;background:#000;font-family:Arial,Helvetica,sans-serif;">'
    + '<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#000;"><tr><td align="center" style="padding:20px;">'
    + '<table width="560" cellpadding="0" cellspacing="0" border="0" style="background:#0d0d0d;border-radius:16px;border:1px solid rgba(255,255,255,0.06);overflow:hidden;">'
    + '<tr><td style="padding:24px 30px;border-bottom:1px solid rgba(255,255,255,0.06);">' + header + '</td></tr>'
    + '<tr><td style="padding:24px 30px;">' + body + '</td></tr>'
    + '<tr><td style="padding:16px 30px;border-top:1px solid rgba(255,255,255,0.06);text-align:center;">'
    + '<p style="font-size:11px;color:rgba(255,255,255,0.2);margin:0;">' + sch + ' โ€” ' + tag + '</p>'
    + (unsubLink ? '<p style="font-size:10px;margin:6px 0 0;"><a href="' + unsubLink + '" style="color:rgba(255,255,255,0.15);text-decoration:underline;">Unsubscribe</a></p>' : '')
    + '</td></tr></table></td></tr></table></body></html>';
}

function iconHeader_(emoji, color, title) {
  return '<div style="display:inline-block;width:40px;height:40px;background:' + color + ';border-radius:12px;line-height:40px;font-size:20px;text-align:center;">' + emoji + '</div>'
    + '<span style="color:#fff;font-size:18px;font-weight:700;margin-left:12px;">' + esc_(title) + '</span>';
}

function detailRow_(label, value, color) {
  return '<tr><td style="padding:8px 0;color:#666;font-size:13px;width:130px;">' + label + '</td>'
    + '<td style="padding:8px 0;color:' + (color || '#fff') + ';font-size:13px;font-weight:600;">' + value + '</td></tr>';
}

/* ================================================================
   AUDIT LOG
   ================================================================ */

function logAction_(action, bookingId, student, instructor, date, time, status, details) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
    const logSheet = ss.getSheetByName('Booking Log');
    if (!logSheet) return;
    logSheet.appendRow([new Date(), action, bookingId, student, instructor, date, time, status, details]);
  } catch (_) {}
}

/* ================================================================
   INSTRUCTOR HELPERS
   ================================================================ */

function getInstructorCar_(ss, name) {
  const instSheet = ss.getSheetByName(CONFIG.INSTRUCTORS_TAB);
  if (!instSheet || instSheet.getLastRow() < 2) return 'N/A';
  const data = instSheet.getDataRange().getValues();
  for (let i = 1; i < data.length; i++) {
    if (String(data[i][0]).trim() === name) return String(data[i][1] || '').trim() || 'N/A';
  }
  return 'N/A';
}

function getInstructorEmail_(ss, name) {
  const instSheet = ss.getSheetByName(CONFIG.INSTRUCTORS_TAB);
  if (!instSheet || instSheet.getLastRow() < 2) return '';
  const data = instSheet.getDataRange().getValues();
  for (let i = 1; i < data.length; i++) {
    if (String(data[i][0]).trim() === name) return String(data[i][2] || '').trim();
  }
  return '';
}

/* ================================================================
   SHARED HELPERS
   ================================================================ */

function findCol_(headers, candidates) {
  for (let i = 0; i < headers.length; i++) {
    const h = String(headers[i] || '').toLowerCase().trim();
    for (const kw of candidates) {
      if (kw && h.includes(String(kw).toLowerCase().trim())) return i;
    }
  }
  return -1;
}

function esc_(str) {
  if (str == null) return '';
  return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function sanitize_(str) {
  return String(str || '').replace(/[<>{}()\[\]\\\/]/g, '').substring(0, 200).trim();
}

function parseDate_(val) {
  if (val instanceof Date) return isNaN(val.getTime()) ? null : val;
  if (val == null || val === '') return null;
  const d = new Date(val);
  return isNaN(d.getTime()) ? null : d;
}

function fmtDate_(val) {
  const tz = CONFIG.TIMEZONE;
  if (val instanceof Date) return isNaN(val.getTime()) ? '' : Utilities.formatDate(val, tz, 'MM/dd/yyyy');
  const d = new Date(val);
  return isNaN(d.getTime()) ? String(val || '').trim() : Utilities.formatDate(d, tz, 'MM/dd/yyyy');
}

function levenshtein_(a, b) {
  const m = a.length, n = b.length;
  const dp = Array.from({ length: m + 1 }, (_, i) => i);
  for (let j = 1; j <= n; j++) {
    let prev = dp[0]; dp[0] = j;
    for (let i = 1; i <= m; i++) {
      const temp = dp[i];
      dp[i] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[i], dp[i - 1]);
      prev = temp;
    }
  }
  return dp[m];
}

function notifyAdmin_(subject, body) {
  if (!CONFIG.ADMIN_EMAIL) return;
  try { MailApp.sendEmail(CONFIG.ADMIN_EMAIL, subject, body, { name: CONFIG.SCHOOL_NAME }); } catch (_) {}
}

/* ================================================================
   MANUAL TOOLS
   ================================================================ */

function testDemoBooking() {
  const d = DEMO.booking;
  Logger.log('๐ŸŽญ Demo booking:');
  Logger.log('  ' + d.student + ' with ' + d.instructor + ' on ' + d.date + ' at ' + d.time);
  Logger.log('  Car: ' + d.car + ' | Booking ID: ' + d.id);
  Logger.log('  Utilization: ' + JSON.stringify(DEMO.weekSummary.utilization));
}

function manualBook() {
  const result = bookLesson('Anisha', 'Test Student', '[email protected]', '555-0123', '03/01/2026', '10:00 AM', 'Test booking');
  Logger.log(JSON.stringify(result));
}
๐ŸŽฏ Lead Follow-Up System 33KB ยท 788 linesGSโ–ถ
/**
 * =========================================================
 * LEAD FOLLOW-UP SYSTEM
 * Flavors Driving School
 * =========================================================
 * Auto follows up with unconfirmed 5-Hour Class signups.
 *
 * - 1st follow-up: 48 hours after signup (friendly reminder)
 * - 2nd follow-up: 5 days after signup (urgency nudge)
 * - 3rd follow-up: 10 days after signup (last chance + offer)
 * - Auto-confirm: cross-checks Registration sheet
 * - Multi-carrier SMS (16 carriers) + email fallback
 * - Mission Control themed HTML emails
 * - Admin summary after each run
 * - Demo mode for safe presentations
 * =========================================================
 */

const CONFIG = {
  // โ”€โ”€ Sheet IDs โ”€โ”€
  SIGNUPS_SHEET_ID: '1lE2_amcNj3-ao3TiP_U0VpaWHqmx4R-0rkF3n3xl7fM',
  REGISTRATION_ID: '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',

  // โ”€โ”€ Sheet tabs โ”€โ”€
  SIGNUPS_TAB: 'Form Responses 1',
  REGISTRATION_TAB: '',  // first sheet

  // โ”€โ”€ Admin โ”€โ”€
  ADMIN_EMAIL: '[email protected]',
  SCHOOL_NAME: 'Flavors Driving School',
  SCHOOL_PHONE: '',  // fill in when available

  // โ”€โ”€ Demo Mode โ”€โ”€
  DEMO_MODE: true,

  // โ”€โ”€ Timing โ”€โ”€
  FOLLOWUP_1_HOURS: 48,
  FOLLOWUP_2_DAYS: 5,
  FOLLOWUP_3_DAYS: 10,

  // โ”€โ”€ Cooldown (prevent double-sends if trigger fires twice) โ”€โ”€
  COOLDOWN_HOURS: 12,

  // โ”€โ”€ SMS Carrier Gateways โ”€โ”€
  CARRIER_GATEWAYS: {
    'verizon':    'vtext.com',
    'att':        'txt.att.net',
    'at&t':       'txt.att.net',
    'tmobile':    'tmomail.net',
    't-mobile':   'tmomail.net',
    'sprint':     'messaging.sprintpcs.com',
    'metro':      'mymetropcs.com',
    'metropcs':   'mymetropcs.com',
    'boost':      'sms.myboostmobile.com',
    'cricket':    'sms.cricketwireless.net',
    'uscellular': 'email.uscc.net',
    'mint':       'mailmymobile.net',
    'visible':    'visible.com',
    'xfinity':    'vtext.com',
    'googlefi':   'msg.fi.google.com',
    'google fi':  'msg.fi.google.com',
    'spectrum':   'vtext.com',
    'consumer':   'mailmymobile.net'
  },
  DEFAULT_GATEWAY: 'vtext.com',

  TIMEZONE: 'America/New_York'
};

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   AUTH & SETUP
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function forceAuth() {
  const ss = SpreadsheetApp.openById(CONFIG.SIGNUPS_SHEET_ID);
  Logger.log('Signups sheet: ' + ss.getName());
  const reg = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  Logger.log('Registration sheet: ' + reg.getName());
  MailApp.getRemainingDailyQuota();
  Logger.log('MailApp authorized. Remaining quota: ' + MailApp.getRemainingDailyQuota());
  Logger.log('forceAuth complete โœ…');
}

function fullSetup() {
  const ss = SpreadsheetApp.openById(CONFIG.SIGNUPS_SHEET_ID);
  const sheet = ss.getSheetByName(CONFIG.SIGNUPS_TAB);
  if (!sheet) {
    Logger.log('ERROR: Tab "' + CONFIG.SIGNUPS_TAB + '" not found');
    return;
  }

  // Ensure tracking columns exist
  const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
  const neededCols = ['Follow-Up Stage', 'Status', 'Follow-Up 1 Date', 'Follow-Up 2 Date', 'Follow-Up 3 Date', 'Carrier', 'Unsubscribe'];
  let nextCol = headers.length + 1;
  for (const col of neededCols) {
    if (findCol_(headers, col) === -1) {
      sheet.getRange(1, nextCol).setValue(col).setFontWeight('bold');
      Logger.log('Added column: ' + col + ' at column ' + nextCol);
      nextCol++;
    }
  }

  // โ”€โ”€ Follow-Up Log sheet โ”€โ”€
  let logSheet = ss.getSheetByName('Follow-Up Log');
  if (!logSheet) {
    logSheet = ss.insertSheet('Follow-Up Log');
    logSheet.getRange(1, 1, 1, 8).setValues([[
      'Timestamp', 'Student Name', 'Email', 'Phone', 'Stage', 'Channel', 'Status', 'Notes'
    ]]);
    logSheet.getRange(1, 1, 1, 8).setFontWeight('bold').setBackground('#0d0d0d').setFontColor('#ff2d2d');
    logSheet.setFrozenRows(1);
    Logger.log('Created Follow-Up Log sheet โœ…');
  }

  // โ”€โ”€ Settings sheet โ”€โ”€
  let settingsSheet = ss.getSheetByName('Settings');
  if (!settingsSheet) {
    settingsSheet = ss.insertSheet('Settings');
    settingsSheet.getRange(1, 1, 1, 3).setValues([['Setting', 'Value', 'Description']]);
    settingsSheet.getRange(1, 1, 1, 3).setFontWeight('bold').setBackground('#0d0d0d').setFontColor('#ff2d2d');
    const settings = [
      ['DEMO_MODE', 'true', 'Set to false for production'],
      ['ADMIN_EMAIL', CONFIG.ADMIN_EMAIL, 'Admin receives run summaries'],
      ['FOLLOWUP_1_HOURS', '48', 'Hours before 1st follow-up'],
      ['FOLLOWUP_2_DAYS', '5', 'Days before 2nd follow-up'],
      ['FOLLOWUP_3_DAYS', '10', 'Days before 3rd follow-up (last chance)'],
      ['COOLDOWN_HOURS', '12', 'Minimum hours between sends to same student'],
      ['SCHOOL_PHONE', '', 'School phone number for emails'],
      ['NEXT_CLASS_DATE', '', 'Next Saturday class date (shows in emails)']
    ];
    settingsSheet.getRange(2, 1, settings.length, 3).setValues(settings);
    settingsSheet.autoResizeColumns(1, 3);
    Logger.log('Created Settings sheet โœ…');
  }

  // โ”€โ”€ Triggers (clean duplicates first) โ”€โ”€
  const triggers = ScriptApp.getProjectTriggers();
  const wantedFns = ['sendLeadFollowUps'];
  for (const t of triggers) {
    if (wantedFns.includes(t.getHandlerFunction())) {
      ScriptApp.deleteTrigger(t);
      Logger.log('Removed old trigger: ' + t.getHandlerFunction());
    }
  }

  // Daily at 9 AM
  ScriptApp.newTrigger('sendLeadFollowUps')
    .timeBased()
    .everyDays(1)
    .atHour(9)
    .create();
  Logger.log('Created trigger: sendLeadFollowUps (daily 9 AM) โœ…');

  Logger.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
  Logger.log('fullSetup complete โœ…');
  Logger.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   HELPERS
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function findCol_(headers, label) {
  for (let i = 0; i < headers.length; i++) {
    if (String(headers[i]).toLowerCase().includes(String(label).toLowerCase())) return i;
  }
  return -1;
}

function escHtml_(s) {
  return String(s || '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

function sanitize_(s, maxLen) {
  return String(s || '').replace(/[^\w\s@.\-\/,#$()&+:;'"!?]/g, '').substring(0, maxLen || 200);
}

function getSetting_(key, fallback) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.SIGNUPS_SHEET_ID);
    const sheet = ss.getSheetByName('Settings');
    if (!sheet) return fallback;
    const data = sheet.getDataRange().getValues();
    for (let i = 1; i < data.length; i++) {
      if (String(data[i][0]).trim().toUpperCase() === key.toUpperCase()) {
        return String(data[i][1]).trim();
      }
    }
  } catch (e) { /* ignore */ }
  return fallback;
}

function isDemoMode_() {
  return getSetting_('DEMO_MODE', 'true').toLowerCase() === 'true';
}

function getAdminEmail_() {
  return getSetting_('ADMIN_EMAIL', CONFIG.ADMIN_EMAIL);
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   AUTO-CONFIRM FROM REGISTRATION
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function getRegisteredEmails_() {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
    const sheet = ss.getSheets()[0];
    if (sheet.getLastRow() < 2) return new Set();
    const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    const cEmail = findCol_(headers, 'email');
    if (cEmail === -1) return new Set();
    const data = sheet.getRange(2, cEmail + 1, sheet.getLastRow() - 1, 1).getValues();
    const emails = new Set();
    for (const row of data) {
      const e = String(row[0] || '').trim().toLowerCase();
      if (e && e.includes('@')) emails.add(e);
    }
    return emails;
  } catch (e) {
    Logger.log('Could not read Registration sheet: ' + e.message);
    return new Set();
  }
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   MAIN FOLLOW-UP ENGINE
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function sendLeadFollowUps() {
  const demo = isDemoMode_();
  const now = new Date();

  if (demo) {
    Logger.log('โ•โ• DEMO MODE โ€” no real emails will be sent โ•โ•');
  }

  const ss = SpreadsheetApp.openById(CONFIG.SIGNUPS_SHEET_ID);
  const sheet = ss.getSheetByName(CONFIG.SIGNUPS_TAB);
  if (!sheet || sheet.getLastRow() < 2) {
    Logger.log('No signups to process');
    return;
  }

  const allData = sheet.getDataRange().getValues();
  const headers = allData[0];

  // Find columns dynamically
  const cTimestamp = findCol_(headers, 'timestamp');
  const cName = findCol_(headers, 'name');
  const cPhone = findCol_(headers, 'phone');
  const cEmail = findCol_(headers, 'email');
  const cStage = findCol_(headers, 'follow-up stage');
  const cStatus = findCol_(headers, 'status');
  const cFU1 = findCol_(headers, 'follow-up 1 date');
  const cFU2 = findCol_(headers, 'follow-up 2 date');
  const cFU3 = findCol_(headers, 'follow-up 3 date');
  const cCarrier = findCol_(headers, 'carrier');
  const cUnsub = findCol_(headers, 'unsubscribe');

  if (cTimestamp === -1 || cName === -1) {
    Logger.log('ERROR: Missing required columns (Timestamp, Name)');
    return;
  }

  // Get timing settings
  const fu1Ms = parseInt(getSetting_('FOLLOWUP_1_HOURS', '48'), 10) * 60 * 60 * 1000;
  const fu2Ms = parseInt(getSetting_('FOLLOWUP_2_DAYS', '5'), 10) * 24 * 60 * 60 * 1000;
  const fu3Ms = parseInt(getSetting_('FOLLOWUP_3_DAYS', '10'), 10) * 24 * 60 * 60 * 1000;
  const cooldownMs = parseInt(getSetting_('COOLDOWN_HOURS', '12'), 10) * 60 * 60 * 1000;

  // Get registered emails for auto-confirm
  const registeredEmails = getRegisteredEmails_();

  // Get next class date for emails
  const nextClassDate = getSetting_('NEXT_CLASS_DATE', '');

  // Track stats
  const stats = { processed: 0, fu1Sent: 0, fu2Sent: 0, fu3Sent: 0, autoConfirmed: 0, skipped: 0, errors: 0 };

  // Get Follow-Up Log for cooldown check
  const logSheet = ss.getSheetByName('Follow-Up Log');
  const recentSends = new Map();
  if (logSheet && logSheet.getLastRow() > 1) {
    const logData = logSheet.getRange(2, 1, logSheet.getLastRow() - 1, 3).getValues();
    for (const row of logData) {
      const logTime = row[0] instanceof Date ? row[0] : new Date(row[0]);
      const logEmail = String(row[2] || '').trim().toLowerCase();
      if (logEmail && logTime > new Date(now.getTime() - cooldownMs)) {
        recentSends.set(logEmail, logTime);
      }
    }
  }

  // Batch updates array
  const updates = [];

  for (let i = 1; i < allData.length; i++) {
    const row = allData[i];
    const name = sanitize_(String(row[cName] || '').trim(), 100);
    if (!name) continue;

    const timestamp = row[cTimestamp] instanceof Date ? row[cTimestamp] : new Date(row[cTimestamp]);
    if (isNaN(timestamp.getTime())) continue;

    const email = cEmail > -1 ? String(row[cEmail] || '').trim().toLowerCase() : '';
    const phone = cPhone > -1 ? String(row[cPhone] || '').replace(/[^\d]/g, '') : '';
    const carrier = cCarrier > -1 ? String(row[cCarrier] || '').trim().toLowerCase() : '';
    const stage = cStage > -1 ? String(row[cStage] || '').trim() : '';
    const status = cStatus > -1 ? String(row[cStatus] || '').trim() : '';
    const unsub = cUnsub > -1 ? String(row[cUnsub] || '').trim().toUpperCase() : '';

    // Skip if confirmed, unsubscribed, or completed all follow-ups
    if (status === 'Confirmed' || unsub === 'YES' || unsub === 'TRUE' || stage === 'DONE') continue;

    stats.processed++;
    const timeDiff = now.getTime() - timestamp.getTime();
    const rowNum = i + 1;

    // Auto-confirm if they registered
    if (email && registeredEmails.has(email)) {
      updates.push({ row: rowNum, col: cStatus > -1 ? cStatus + 1 : null, val: 'Confirmed (Auto-Detected)' });
      updates.push({ row: rowNum, col: cStage > -1 ? cStage + 1 : null, val: 'DONE' });
      stats.autoConfirmed++;
      logFollowUp_(logSheet, now, name, email, phone, 'Auto-Confirm', 'success', 'Found in Registration sheet');
      continue;
    }

    // Cooldown check
    if (email && recentSends.has(email)) {
      stats.skipped++;
      continue;
    }

    // No contact info
    if (!email && phone.length < 10) {
      if (cStatus > -1) updates.push({ row: rowNum, col: cStatus + 1, val: 'No Contact Info' });
      stats.skipped++;
      continue;
    }

    const firstName = escHtml_(name.split(' ')[0]);
    const safeName = escHtml_(name);

    // โ”€โ”€ 1st Follow-Up: 48 hours โ”€โ”€
    if (!stage && timeDiff >= fu1Ms) {
      const subject = '๐ŸŽ“ Confirm Your 5-Hour Class Spot โ€” ' + CONFIG.SCHOOL_NAME;
      const body = buildFollowUp1_(firstName, safeName, nextClassDate);

      const result = demo ? 'demo' : sendFollowUp_(email, phone, carrier, subject, body);

      if (result) {
        if (cStage > -1) updates.push({ row: rowNum, col: cStage + 1, val: '1ST' });
        if (cStatus > -1) updates.push({ row: rowNum, col: cStatus + 1, val: 'Followed Up โ€” Awaiting Response' });
        if (cFU1 > -1) updates.push({ row: rowNum, col: cFU1 + 1, val: now });
        stats.fu1Sent++;
        logFollowUp_(logSheet, now, name, email, phone, '1st', result === 'demo' ? 'demo' : 'sent', '');
      } else {
        if (cStatus > -1) updates.push({ row: rowNum, col: cStatus + 1, val: 'Send Failed โ€” Will Retry' });
        stats.errors++;
        logFollowUp_(logSheet, now, name, email, phone, '1st', 'failed', 'MailApp error');
      }
      continue;
    }

    // โ”€โ”€ 2nd Follow-Up: 5 days โ”€โ”€
    if (stage === '1ST' && timeDiff >= fu2Ms) {
      const subject = 'โฐ Last Few Spots โ€” Your 5-Hour Class';
      const body = buildFollowUp2_(firstName, nextClassDate);

      const result = demo ? 'demo' : sendFollowUp_(email, phone, carrier, subject, body);

      if (result) {
        if (cStage > -1) updates.push({ row: rowNum, col: cStage + 1, val: '2ND' });
        if (cStatus > -1) updates.push({ row: rowNum, col: cStatus + 1, val: '2nd Follow-Up โ€” Final Nudge' });
        if (cFU2 > -1) updates.push({ row: rowNum, col: cFU2 + 1, val: now });
        stats.fu2Sent++;
        logFollowUp_(logSheet, now, name, email, phone, '2nd', result === 'demo' ? 'demo' : 'sent', '');
      } else {
        stats.errors++;
        logFollowUp_(logSheet, now, name, email, phone, '2nd', 'failed', 'MailApp error');
      }
      continue;
    }

    // โ”€โ”€ 3rd Follow-Up: 10 days (last chance) โ”€โ”€
    if (stage === '2ND' && timeDiff >= fu3Ms) {
      const subject = '๐Ÿš— Last Chance โ€” Special Offer on 5-Hour Class';
      const body = buildFollowUp3_(firstName, nextClassDate);

      const result = demo ? 'demo' : sendFollowUp_(email, phone, carrier, subject, body);

      if (result) {
        if (cStage > -1) updates.push({ row: rowNum, col: cStage + 1, val: 'DONE' });
        if (cStatus > -1) updates.push({ row: rowNum, col: cStatus + 1, val: 'All Follow-Ups Complete' });
        if (cFU3 > -1) updates.push({ row: rowNum, col: cFU3 + 1, val: now });
        stats.fu3Sent++;
        logFollowUp_(logSheet, now, name, email, phone, '3rd', result === 'demo' ? 'demo' : 'sent', 'Last chance offer');
      } else {
        stats.errors++;
        logFollowUp_(logSheet, now, name, email, phone, '3rd', 'failed', 'MailApp error');
      }
      continue;
    }
  }

  // Apply all updates
  for (const u of updates) {
    if (u.col) sheet.getRange(u.row, u.col).setValue(u.val);
  }

  // Send admin summary
  sendAdminSummary_(stats, demo);

  Logger.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
  Logger.log('Lead follow-up complete');
  Logger.log('Processed: ' + stats.processed + ' | 1st: ' + stats.fu1Sent + ' | 2nd: ' + stats.fu2Sent +
    ' | 3rd: ' + stats.fu3Sent + ' | Auto-confirmed: ' + stats.autoConfirmed +
    ' | Skipped: ' + stats.skipped + ' | Errors: ' + stats.errors);
  Logger.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   SEND FOLLOW-UP (EMAIL + SMS)
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function sendFollowUp_(email, phone, carrier, subject, htmlBody) {
  let sent = false;
  try {
    // Email
    if (email && email.includes('@')) {
      MailApp.sendEmail({
        to: email,
        subject: subject,
        htmlBody: buildEmailHtml_(subject, htmlBody)
      });
      sent = true;
    }

    // SMS via carrier gateway
    if (phone && phone.length >= 10) {
      const gateway = CONFIG.CARRIER_GATEWAYS[carrier] || CONFIG.DEFAULT_GATEWAY;
      const smsAddress = phone + '@' + gateway;
      // SMS gets plain text version (strip HTML)
      const plainText = htmlBody.replace(/<[^>]*>/g, '').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim().substring(0, 160);
      MailApp.sendEmail({
        to: smsAddress,
        subject: '',
        body: plainText
      });
      sent = true;
    }

    return sent ? 'sent' : false;
  } catch (e) {
    Logger.log('Send error: ' + e.message);
    return false;
  }
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   FOLLOW-UP EMAIL TEMPLATES
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function buildFollowUp1_(firstName, fullName, nextDate) {
  const dateInfo = nextDate
    ? '<p style="color:#ff2d2d;font-weight:bold;text-align:center;font-size:16px;">๐Ÿ“… Next class: ' + escHtml_(nextDate) + '</p>'
    : '';

  return '<div style="text-align:center;margin-bottom:20px;">' +
      '<span style="font-size:48px;">๐ŸŽ“</span>' +
    '</div>' +
    '<p style="color:#ccc;">Hi ' + firstName + '!</p>' +
    '<p style="color:#ccc;">We noticed you signed up for the <strong style="color:#fff;">5-Hour Pre-Licensing Class</strong> but haven\'t confirmed your spot yet.</p>' +
    '<p style="color:#ccc;">This class is <strong style="color:#fff;">required by NY State</strong> before you can take your road test โ€” and our Saturday classes fill up fast!</p>' +
    dateInfo +
    '<div style="text-align:center;margin:24px 0;">' +
      '<div style="display:inline-block;background:#ff2d2d;color:#fff;padding:12px 32px;border-radius:8px;font-weight:bold;font-size:16px;">' +
        'Reply to confirm your spot โ†’' +
      '</div>' +
    '</div>' +
    '<p style="color:#999;">Just reply to this email or give us a call to lock in your date. We\'d love to have you!</p>' +
    buildUnsubscribeText_();
}

function buildFollowUp2_(firstName, nextDate) {
  const dateInfo = nextDate
    ? '<p style="color:#ffaa00;font-weight:bold;text-align:center;">๐Ÿ“… Next available: ' + escHtml_(nextDate) + ' โ€” only a few seats left</p>'
    : '';

  return '<div style="text-align:center;margin-bottom:20px;">' +
      '<span style="font-size:48px;">โฐ</span>' +
    '</div>' +
    '<p style="color:#ccc;">Hey ' + firstName + '!</p>' +
    '<p style="color:#ccc;">Quick check-in โ€” we still have a spot open for you in our <strong style="color:#fff;">5-Hour Pre-Licensing Class</strong>.</p>' +
    '<p style="color:#ccc;">Just a heads up: this is a <strong style="color:#fff;">required course</strong> to get your license, and we typically have <strong style="color:#ffaa00;">limited seats</strong> each Saturday.</p>' +
    dateInfo +
    '<table style="width:100%;border-collapse:collapse;margin:20px 0;background:#1a1a1a;border-radius:8px;">' +
      '<tr><td style="padding:16px;text-align:center;">' +
        '<div style="color:#ff2d2d;font-size:20px;font-weight:bold;">$65</div>' +
        '<div style="color:#999;font-size:12px;">One-time โ€ข Certificate included</div>' +
      '</td></tr>' +
    '</table>' +
    '<p style="color:#999;">Let us know if you\'d like to lock in your date โ€” just hit reply!</p>' +
    buildUnsubscribeText_();
}

function buildFollowUp3_(firstName, nextDate) {
  return '<div style="text-align:center;margin-bottom:20px;">' +
      '<span style="font-size:48px;">๐Ÿš—</span>' +
    '</div>' +
    '<p style="color:#ccc;">Hey ' + firstName + '!</p>' +
    '<p style="color:#ccc;">This is our last reach-out โ€” we don\'t want to bug you, but we also don\'t want you to miss out.</p>' +
    '<p style="color:#ccc;">You signed up for the 5-Hour Pre-Licensing Class and your spot is still available. Once you complete this class, you\'re one step closer to your license! ๐ŸŽ‰</p>' +

    '<table style="width:100%;border-collapse:collapse;margin:20px 0;background:linear-gradient(135deg,#1a0000,#0d0d0d);border:1px solid rgba(255,45,45,0.3);border-radius:12px;">' +
      '<tr><td style="padding:24px;text-align:center;">' +
        '<div style="color:#ff2d2d;font-size:14px;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;">Special Offer</div>' +
        '<div style="color:#fff;font-size:18px;font-weight:bold;">Book your 5-Hour Class + first driving lesson</div>' +
        '<div style="color:#ffaa00;font-size:14px;margin-top:8px;">and get <strong>$10 off</strong> your first lesson package ๐ŸŽ</div>' +
        '<div style="color:#666;font-size:11px;margin-top:12px;">Just mention this email when you call</div>' +
      '</td></tr>' +
    '</table>' +

    '<p style="color:#999;">Reply to this email or call us anytime. Either way โ€” good luck on your driving journey!</p>' +
    buildUnsubscribeText_();
}

function buildUnsubscribeText_() {
  return '<p style="color:#444;font-size:10px;text-align:center;margin-top:24px;border-top:1px solid rgba(255,255,255,0.05);padding-top:12px;">' +
    'Don\'t want to hear from us? Reply "STOP" and we\'ll remove you from follow-ups.</p>';
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   FOLLOW-UP LOG
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function logFollowUp_(logSheet, timestamp, name, email, phone, stage, status, notes) {
  if (!logSheet) return;
  try {
    logSheet.appendRow([timestamp, name, email, phone, stage, status === 'demo' ? 'Demo โ€” not sent' : status, status, notes]);
  } catch (e) { /* silent */ }
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   MARK CONFIRMED
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

/** Mark a student as confirmed by name (case-insensitive) or email */
function markConfirmed(studentNameOrEmail) {
  const ss = SpreadsheetApp.openById(CONFIG.SIGNUPS_SHEET_ID);
  const sheet = ss.getSheetByName(CONFIG.SIGNUPS_TAB);
  const allData = sheet.getDataRange().getValues();
  const headers = allData[0];
  const cName = findCol_(headers, 'name');
  const cEmail = findCol_(headers, 'email');
  const cStatus = findCol_(headers, 'status');
  const cStage = findCol_(headers, 'follow-up stage');

  const search = String(studentNameOrEmail || '').trim().toLowerCase();
  let found = 0;

  for (let i = 1; i < allData.length; i++) {
    const name = cName > -1 ? String(allData[i][cName] || '').trim().toLowerCase() : '';
    const email = cEmail > -1 ? String(allData[i][cEmail] || '').trim().toLowerCase() : '';

    if (name === search || email === search) {
      const rowNum = i + 1;
      if (cStatus > -1) sheet.getRange(rowNum, cStatus + 1).setValue('Confirmed');
      if (cStage > -1) sheet.getRange(rowNum, cStage + 1).setValue('DONE');
      Logger.log('Confirmed: ' + allData[i][cName]);
      found++;
    }
  }

  Logger.log(found > 0 ? 'Marked ' + found + ' row(s) as confirmed' : 'Student not found: ' + studentNameOrEmail);
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   VIEW STATS
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function viewLeadStatuses() {
  const ss = SpreadsheetApp.openById(CONFIG.SIGNUPS_SHEET_ID);
  const sheet = ss.getSheetByName(CONFIG.SIGNUPS_TAB);
  const allData = sheet.getDataRange().getValues();
  const headers = allData[0];
  const cName = findCol_(headers, 'name');
  const cStatus = findCol_(headers, 'status');
  const cStage = findCol_(headers, 'follow-up stage');

  const counts = { 'Pending': 0, 'Followed Up': 0, '2nd Follow-Up': 0, 'All Complete': 0, 'Confirmed': 0, 'No Contact': 0, 'Failed': 0, 'Other': 0 };

  Logger.log('โ•โ•โ• LEAD STATUS REPORT โ•โ•โ•');
  for (let i = 1; i < allData.length; i++) {
    const name = cName > -1 ? String(allData[i][cName] || '').trim() : '';
    if (!name) continue;
    const status = cStatus > -1 ? String(allData[i][cStatus] || 'Pending').trim() : 'Pending';

    let bucket = 'Other';
    if (status.includes('Confirmed')) bucket = 'Confirmed';
    else if (status.includes('All Follow')) bucket = 'All Complete';
    else if (status.includes('2nd')) bucket = '2nd Follow-Up';
    else if (status.includes('Followed Up') || status.includes('Awaiting')) bucket = 'Followed Up';
    else if (status.includes('No Contact')) bucket = 'No Contact';
    else if (status.includes('Failed') || status.includes('Retry')) bucket = 'Failed';
    else if (status === 'Pending' || status === '') bucket = 'Pending';

    counts[bucket] = (counts[bucket] || 0) + 1;
    Logger.log(name + ' โ†’ ' + status);
  }

  Logger.log('โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€');
  for (const [k, v] of Object.entries(counts)) {
    if (v > 0) Logger.log(k + ': ' + v);
  }
}

/** API for BI Dashboard */
function getLeadStats() {
  const demo = isDemoMode_();

  if (demo) {
    return {
      demo: true,
      total: 15,
      pending: 3,
      followedUp: 4,
      confirmed: 6,
      noContact: 1,
      complete: 1,
      conversionRate: '40%'
    };
  }

  const ss = SpreadsheetApp.openById(CONFIG.SIGNUPS_SHEET_ID);
  const sheet = ss.getSheetByName(CONFIG.SIGNUPS_TAB);
  if (!sheet || sheet.getLastRow() < 2) return { total: 0 };

  const allData = sheet.getDataRange().getValues();
  const headers = allData[0];
  const cName = findCol_(headers, 'name');
  const cStatus = findCol_(headers, 'status');

  let total = 0, confirmed = 0, pending = 0, followedUp = 0, noContact = 0;
  for (let i = 1; i < allData.length; i++) {
    if (cName > -1 && !String(allData[i][cName] || '').trim()) continue;
    total++;
    const status = cStatus > -1 ? String(allData[i][cStatus] || '').trim() : '';
    if (status.includes('Confirmed')) confirmed++;
    else if (status.includes('Followed') || status.includes('Awaiting') || status.includes('2nd') || status.includes('All Follow')) followedUp++;
    else if (status.includes('No Contact')) noContact++;
    else pending++;
  }

  return {
    demo: false,
    total: total,
    pending: pending,
    followedUp: followedUp,
    confirmed: confirmed,
    noContact: noContact,
    conversionRate: total > 0 ? Math.round(confirmed / total * 100) + '%' : '0%'
  };
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   ADMIN SUMMARY EMAIL
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function sendAdminSummary_(stats, demo) {
  const total = stats.fu1Sent + stats.fu2Sent + stats.fu3Sent;
  if (total === 0 && stats.autoConfirmed === 0 && stats.errors === 0) {
    Logger.log('No actions taken โ€” skipping admin summary');
    return;
  }

  const body =
    '<div style="text-align:center;margin-bottom:20px;">' +
      '<span style="font-size:48px;">๐Ÿ“‹</span>' +
      '<h2 style="color:#ff2d2d;margin:12px 0 4px;">Lead Follow-Up Summary</h2>' +
      '<p style="color:#999;margin:0;">' + Utilities.formatDate(new Date(), CONFIG.TIMEZONE, 'EEEE, MMM d, yyyy โ€” h:mm a') + '</p>' +
    '</div>' +

    '<table style="width:100%;border-collapse:collapse;margin:16px 0;">' +
      buildRow_('Leads Processed', stats.processed) +
      buildRow_('1st Follow-Up Sent', stats.fu1Sent > 0 ? '<span style="color:#4CAF50;">' + stats.fu1Sent + '</span>' : '0') +
      buildRow_('2nd Follow-Up Sent', stats.fu2Sent > 0 ? '<span style="color:#ffaa00;">' + stats.fu2Sent + '</span>' : '0') +
      buildRow_('3rd Follow-Up Sent', stats.fu3Sent > 0 ? '<span style="color:#ff2d2d;">' + stats.fu3Sent + '</span>' : '0') +
      buildRow_('Auto-Confirmed', stats.autoConfirmed > 0 ? '<span style="color:#4CAF50;">โœ… ' + stats.autoConfirmed + '</span>' : '0') +
      buildRow_('Skipped (cooldown/no info)', stats.skipped) +
      buildRow_('Errors', stats.errors > 0 ? '<span style="color:#ff2d2d;">โš ๏ธ ' + stats.errors + '</span>' : '0') +
    '</table>';

  const subject = (demo ? '[DEMO] ' : '') + '๐Ÿ“‹ Lead Follow-Up: ' + total + ' sent, ' + stats.autoConfirmed + ' auto-confirmed';

  if (!demo) {
    try {
      MailApp.sendEmail({
        to: getAdminEmail_(),
        subject: subject,
        htmlBody: buildEmailHtml_('Lead Follow-Up Summary', body)
      });
    } catch (e) {
      Logger.log('Admin summary email failed: ' + e.message);
    }
  }
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   DEMO DATA
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function generateDemoLeads() {
  const ss = SpreadsheetApp.openById(CONFIG.SIGNUPS_SHEET_ID);
  const logSheet = ss.getSheetByName('Follow-Up Log');
  if (!logSheet) {
    Logger.log('Run fullSetup() first');
    return;
  }

  const now = new Date();
  const demoData = [
    { name: 'Sarah Johnson', email: '[email protected]', phone: '9175551001', daysAgo: 1 },   // too early
    { name: 'Mike Rivera', email: '[email protected]', phone: '7185551002', daysAgo: 3 },       // ready for 1st
    { name: 'Emily Chen', email: '[email protected]', phone: '3475551003', daysAgo: 6 },       // ready for 2nd
    { name: 'James Wilson', email: '[email protected]', phone: '6465551004', daysAgo: 11 },    // ready for 3rd
    { name: 'Ana Martinez', email: '[email protected]', phone: '9295551005', daysAgo: 4 },       // ready for 1st
  ];

  const demoRows = demoData.map(d => {
    const ts = new Date(now.getTime() - d.daysAgo * 24 * 60 * 60 * 1000);
    return [ts, d.name, d.phone, d.email, '', '', ts];
  });

  Logger.log('Generated ' + demoRows.length + ' demo leads');
  Logger.log('Run sendLeadFollowUps() to see them process (in demo mode โ€” no real emails)');
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   EMAIL TEMPLATE โ€” Mission Control Theme
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function buildEmailHtml_(title, bodyContent) {
  return '<!DOCTYPE html><html><head><meta charset="utf-8"></head>' +
    '<body style="margin:0;padding:0;background:#000;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;">' +
    '<table width="100%" cellpadding="0" cellspacing="0" style="background:#000;"><tr><td align="center" style="padding:20px;">' +
      '<table width="600" cellpadding="0" cellspacing="0" style="background:#0d0d0d;border:1px solid rgba(255,45,45,0.2);border-radius:12px;overflow:hidden;">' +

        // Header
        '<tr><td style="background:linear-gradient(135deg,#1a0000,#0d0d0d);padding:24px 32px;border-bottom:1px solid rgba(255,45,45,0.15);">' +
          '<table width="100%"><tr>' +
            '<td style="color:#ff2d2d;font-size:20px;font-weight:bold;">๐Ÿซ ' + escHtml_(CONFIG.SCHOOL_NAME) + '</td>' +
          '</tr></table>' +
        '</td></tr>' +

        // Title bar
        '<tr><td style="padding:20px 32px 0;">' +
          '<h1 style="color:#fff;font-size:22px;margin:0 0 4px;font-weight:600;">' + title + '</h1>' +
          '<div style="width:40px;height:3px;background:#ff2d2d;border-radius:2px;"></div>' +
        '</td></tr>' +

        // Body
        '<tr><td style="padding:20px 32px 32px;">' + bodyContent + '</td></tr>' +

        // Footer
        '<tr><td style="padding:20px 32px;border-top:1px solid rgba(255,255,255,0.05);text-align:center;">' +
          '<p style="color:#444;font-size:11px;margin:0;">' + CONFIG.SCHOOL_NAME + ' โ€” Lead Follow-Up System</p>' +
          '<p style="color:#333;font-size:10px;margin:4px 0 0;">Automated by Mission Control</p>' +
        '</td></tr>' +

      '</table>' +
    '</td></tr></table></body></html>';
}

function buildRow_(label, value) {
  return '<tr>' +
    '<td style="padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#999;width:50%;">' + label + '</td>' +
    '<td style="padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.05);color:#fff;font-weight:bold;">' + value + '</td>' +
  '</tr>';
}
๐Ÿ“ˆ Monthly Revenue Report 46KB ยท 961 linesGSโ–ถ
/**
 * =========================================================
 * MONTHLY REVENUE REPORT
 * Flavors Driving School
 * =========================================================
 * Comprehensive monthly business report pulling from all
 * data sources: Signups, Registration, Payments, Expenses.
 *
 * - Revenue from actual payments (Payment Tracker) + estimates
 * - Expense breakdown by category with budget comparison
 * - Net profit with month-over-month trends
 * - Student activity summary
 * - YTD running totals
 * - 3-month trend indicators
 * - Report History sheet for lookback
 * - Demo mode for safe presentations
 * =========================================================
 */

const CONFIG = {
  // โ”€โ”€ Sheet IDs โ”€โ”€
  SIGNUPS_SHEET_ID:      '1lE2_amcNj3-ao3TiP_U0VpaWHqmx4R-0rkF3n3xl7fM',
  REGISTRATION_ID:       '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',
  EXPENSE_SHEET_ID:      '1QyC39fjuslDXk-a_u09XACyHSq782H7NUJajV3vBp8M',
  PAYMENT_TRACKER_ID:    '1XWNU7PygQX9jV6mFUUrgHYU_vrhS7NP1OHwNUNWRoOk',

  // โ”€โ”€ Sheet tabs โ”€โ”€
  SIGNUPS_TAB:           'Form Responses 1',
  REGISTRATION_TAB:      '',  // first sheet
  EXPENSE_LOG_TAB:       'Expense Log',
  PAYMENTS_TAB:          'Payments',

  // โ”€โ”€ Admin โ”€โ”€
  ADMIN_EMAIL: '[email protected]',
  SCHOOL_NAME: 'Flavors Driving School',

  // โ”€โ”€ Demo Mode โ”€โ”€
  DEMO_MODE: true,

  // โ”€โ”€ Pricing โ”€โ”€
  FIVE_HOUR_CLASS_PRICE: 65,
  LESSON_PACKAGES: {
    '3 Lessons':  250,
    '5 Lessons':  445,
    '10 Lessons': 710,
    '15 Lessons': 950,
    '25 Lessons': 1500
  },

  TIMEZONE: 'America/New_York'
};

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   AUTH & SETUP
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function forceAuth() {
  const ss1 = SpreadsheetApp.openById(CONFIG.SIGNUPS_SHEET_ID);
  Logger.log('Signups sheet: ' + ss1.getName());
  const ss2 = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  Logger.log('Registration sheet: ' + ss2.getName());
  const ss3 = SpreadsheetApp.openById(CONFIG.EXPENSE_SHEET_ID);
  Logger.log('Expense sheet: ' + ss3.getName());
  const ss4 = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  Logger.log('Payment Tracker: ' + ss4.getName());
  MailApp.getRemainingDailyQuota();
  Logger.log('MailApp authorized. Remaining quota: ' + MailApp.getRemainingDailyQuota());
  Logger.log('forceAuth complete โœ…');
}

function fullSetup() {
  // โ”€โ”€ Report History sheet (in Registration spreadsheet) โ”€โ”€
  const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  let histSheet = ss.getSheetByName('Report History');
  if (!histSheet) {
    histSheet = ss.insertSheet('Report History');
    histSheet.getRange(1, 1, 1, 10).setValues([[
      'Report Month', 'Generated Date', 'Total Revenue', 'Total Expenses',
      'Net Profit', 'New Students', '5-Hour Signups', 'Registrations',
      'Payments Received', 'YTD Revenue'
    ]]);
    histSheet.getRange(1, 1, 1, 10).setFontWeight('bold').setBackground('#0d0d0d').setFontColor('#ff2d2d');
    histSheet.setFrozenRows(1);
    Logger.log('Created Report History sheet โœ…');
  } else {
    Logger.log('Report History sheet already exists โœ…');
  }

  // โ”€โ”€ Settings sheet โ”€โ”€
  let settingsSheet = ss.getSheetByName('Revenue Report Settings');
  if (!settingsSheet) {
    settingsSheet = ss.insertSheet('Revenue Report Settings');
    settingsSheet.getRange(1, 1, 1, 3).setValues([['Setting', 'Value', 'Description']]);
    settingsSheet.getRange(1, 1, 1, 3).setFontWeight('bold').setBackground('#0d0d0d').setFontColor('#ff2d2d');
    const settings = [
      ['DEMO_MODE', 'true', 'Set to false for production'],
      ['ADMIN_EMAIL', CONFIG.ADMIN_EMAIL, 'Report recipient(s) โ€” comma-separated for multiple'],
      ['REPORT_DAY', '1', 'Day of month to send report (1-28)'],
      ['REPORT_HOUR', '8', 'Hour to send report (0-23)'],
      ['INCLUDE_STUDENT_LIST', 'true', 'Show individual student names in report'],
      ['INCLUDE_YTD', 'true', 'Include year-to-date totals']
    ];
    settingsSheet.getRange(2, 1, settings.length, 3).setValues(settings);
    settingsSheet.autoResizeColumns(1, 3);
    Logger.log('Created Revenue Report Settings sheet โœ…');
  } else {
    Logger.log('Revenue Report Settings already exists โœ…');
  }

  // โ”€โ”€ Triggers (clean duplicates) โ”€โ”€
  const triggers = ScriptApp.getProjectTriggers();
  for (const t of triggers) {
    if (['generateMonthlyReport', 'setupMonthlyTrigger'].includes(t.getHandlerFunction())) {
      ScriptApp.deleteTrigger(t);
      Logger.log('Removed old trigger: ' + t.getHandlerFunction());
    }
  }

  ScriptApp.newTrigger('generateMonthlyReport')
    .timeBased()
    .onMonthDay(1)
    .atHour(8)
    .create();
  Logger.log('Created trigger: generateMonthlyReport (1st of month 8 AM) โœ…');

  Logger.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
  Logger.log('fullSetup complete โœ…');
  Logger.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   HELPERS
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function findCol_(headers, label) {
  for (let i = 0; i < headers.length; i++) {
    if (String(headers[i]).toLowerCase().includes(String(label).toLowerCase())) return i;
  }
  return -1;
}

function escHtml_(s) {
  return String(s || '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

function formatMoney_(n) {
  const num = Number(n) || 0;
  const abs = Math.abs(num);
  const formatted = abs.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  return (num < 0 ? '-$' : '$') + formatted;
}

function pctChange_(current, previous) {
  if (previous === 0) return current > 0 ? '+100%' : 'โ€”';
  const pct = Math.round(((current - previous) / Math.abs(previous)) * 100);
  return (pct >= 0 ? '+' : '') + pct + '%';
}

function getSetting_(key, fallback) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
    const sheet = ss.getSheetByName('Revenue Report Settings');
    if (!sheet) return fallback;
    const data = sheet.getDataRange().getValues();
    for (let i = 1; i < data.length; i++) {
      if (String(data[i][0]).trim().toUpperCase() === key.toUpperCase()) {
        return String(data[i][1]).trim();
      }
    }
  } catch (e) { /* ignore */ }
  return fallback;
}

function isDemoMode_() {
  return getSetting_('DEMO_MODE', 'true').toLowerCase() === 'true';
}

function getAdminEmail_() {
  return getSetting_('ADMIN_EMAIL', CONFIG.ADMIN_EMAIL);
}

function getSheetByTabOrFirst_(ss, tabName) {
  if (tabName) {
    const sheet = ss.getSheetByName(tabName);
    if (sheet) return sheet;
  }
  return ss.getSheets()[0];
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   DATA COLLECTION
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function get5HourClassData_(startDate, endDate) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.SIGNUPS_SHEET_ID);
    const sheet = getSheetByTabOrFirst_(ss, CONFIG.SIGNUPS_TAB);
    if (sheet.getLastRow() < 2) return { count: 0, students: [] };

    const allData = sheet.getDataRange().getValues();
    const headers = allData[0];
    const cTs = findCol_(headers, 'timestamp');
    const cName = findCol_(headers, 'name');

    let count = 0;
    const students = [];

    for (let i = 1; i < allData.length; i++) {
      const date = cTs > -1 ? new Date(allData[i][cTs]) : null;
      if (!date || isNaN(date.getTime())) continue;
      if (date >= startDate && date <= endDate) {
        count++;
        if (cName > -1) {
          const name = String(allData[i][cName] || '').trim();
          if (name) students.push(name);
        }
      }
    }

    return { count, students };
  } catch (e) {
    Logger.log('Error reading 5-Hour signups: ' + e.message);
    return { count: 0, students: [], error: e.message };
  }
}

function getRegistrationData_(startDate, endDate) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
    const sheet = getSheetByTabOrFirst_(ss, CONFIG.REGISTRATION_TAB);
    if (sheet.getLastRow() < 2) return { count: 0, totalRevenue: 0, packages: {}, students: [] };

    const allData = sheet.getDataRange().getValues();
    const headers = allData[0];
    const cTs = findCol_(headers, 'timestamp');
    const cName = findCol_(headers, 'name');
    const cPackage = findCol_(headers, 'package');

    let count = 0;
    let totalRevenue = 0;
    const packages = {};
    const students = [];

    for (let i = 1; i < allData.length; i++) {
      const date = cTs > -1 ? new Date(allData[i][cTs]) : null;
      if (!date || isNaN(date.getTime())) continue;
      if (date < startDate || date > endDate) continue;

      count++;
      if (cName > -1) {
        const name = String(allData[i][cName] || '').trim();
        if (name) students.push(name);
      }

      if (cPackage > -1) {
        const pkg = String(allData[i][cPackage] || '').trim();
        let matched = false;

        for (const [key, price] of Object.entries(CONFIG.LESSON_PACKAGES)) {
          if (pkg.toLowerCase().includes(key.toLowerCase()) ||
              (key.match(/\d+/) && pkg.includes(key.match(/\d+/)[0]))) {
            packages[key] = (packages[key] || 0) + 1;
            totalRevenue += price;
            matched = true;
            break;
          }
        }

        if (!matched) {
          const numMatch = pkg.match(/(\d+)/);
          if (numMatch) {
            const lessonCount = parseInt(numMatch[1], 10);
            const matchKey = Object.keys(CONFIG.LESSON_PACKAGES).find(k => k.includes(String(lessonCount)));
            if (matchKey) {
              packages[matchKey] = (packages[matchKey] || 0) + 1;
              totalRevenue += CONFIG.LESSON_PACKAGES[matchKey];
            } else {
              packages['Unknown: ' + pkg] = (packages['Unknown: ' + pkg] || 0) + 1;
            }
          } else {
            packages['Unknown: ' + pkg] = (packages['Unknown: ' + pkg] || 0) + 1;
          }
        }
      }
    }

    return { count, totalRevenue, packages, students };
  } catch (e) {
    Logger.log('Error reading registrations: ' + e.message);
    return { count: 0, totalRevenue: 0, packages: {}, students: [], error: e.message };
  }
}

function getExpenseData_(startDate, endDate) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.EXPENSE_SHEET_ID);
    const sheet = getSheetByTabOrFirst_(ss, CONFIG.EXPENSE_LOG_TAB);
    if (sheet.getLastRow() < 2) return { totalExpenses: 0, categories: {} };

    const allData = sheet.getDataRange().getValues();
    const headers = allData[0];
    const cDate = findCol_(headers, 'date of expense');
    const cTs = cDate > -1 ? cDate : findCol_(headers, 'timestamp');
    const cAmount = findCol_(headers, 'amount');
    const cCategory = findCol_(headers, 'category');
    const cStatus = findCol_(headers, 'status');

    let totalExpenses = 0;
    const categories = {};

    for (let i = 1; i < allData.length; i++) {
      // Skip voided/duplicate
      if (cStatus > -1) {
        const status = String(allData[i][cStatus] || '').toLowerCase();
        if (['void', 'voided', 'duplicate', 'cancelled', 'canceled'].includes(status)) continue;
      }

      const date = cTs > -1 ? new Date(allData[i][cTs]) : null;
      if (!date || isNaN(date.getTime())) continue;
      if (date < startDate || date > endDate) continue;

      const amount = cAmount > -1 ? (parseFloat(String(allData[i][cAmount]).replace(/[$,]/g, '')) || 0) : 0;
      totalExpenses += amount;

      const cat = cCategory > -1 ? String(allData[i][cCategory] || 'Uncategorized').trim() : 'Uncategorized';
      categories[cat] = (categories[cat] || 0) + amount;
    }

    return { totalExpenses, categories };
  } catch (e) {
    Logger.log('Error reading expenses: ' + e.message);
    return { totalExpenses: 0, categories: {}, error: e.message };
  }
}

function getPaymentData_(startDate, endDate) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
    const sheet = getSheetByTabOrFirst_(ss, CONFIG.PAYMENTS_TAB);
    if (sheet.getLastRow() < 2) return { totalReceived: 0, count: 0, methods: {} };

    const allData = sheet.getDataRange().getValues();
    const headers = allData[0];
    const cDate = findCol_(headers, 'date');
    const cTs = cDate > -1 ? cDate : findCol_(headers, 'timestamp');
    const cAmount = findCol_(headers, 'amount');
    const cMethod = findCol_(headers, 'method');
    const cStatus = findCol_(headers, 'status');
    const cType = findCol_(headers, 'type');

    let totalReceived = 0;
    let count = 0;
    const methods = {};

    for (let i = 1; i < allData.length; i++) {
      // Skip refunds and voided
      if (cStatus > -1) {
        const status = String(allData[i][cStatus] || '').toLowerCase();
        if (['voided', 'refunded', 'cancelled'].includes(status)) continue;
      }
      if (cType > -1) {
        const type = String(allData[i][cType] || '').toLowerCase();
        if (type === 'refund') continue;
      }

      const date = cTs > -1 ? new Date(allData[i][cTs]) : null;
      if (!date || isNaN(date.getTime())) continue;
      if (date < startDate || date > endDate) continue;

      const amount = cAmount > -1 ? (parseFloat(String(allData[i][cAmount]).replace(/[$,]/g, '')) || 0) : 0;
      totalReceived += amount;
      count++;

      if (cMethod > -1) {
        const method = String(allData[i][cMethod] || 'Other').trim();
        methods[method] = (methods[method] || 0) + amount;
      }
    }

    return { totalReceived, count, methods };
  } catch (e) {
    Logger.log('Error reading payments: ' + e.message);
    return { totalReceived: 0, count: 0, methods: {}, error: e.message };
  }
}

/** Get YTD totals */
function getYTDData_(year) {
  const ytdStart = new Date(year, 0, 1);
  const ytdEnd = new Date(year, 11, 31, 23, 59, 59);

  const signups = get5HourClassData_(ytdStart, ytdEnd);
  const regs = getRegistrationData_(ytdStart, ytdEnd);
  const expenses = getExpenseData_(ytdStart, ytdEnd);
  const payments = getPaymentData_(ytdStart, ytdEnd);

  const estimatedRevenue = (signups.count * CONFIG.FIVE_HOUR_CLASS_PRICE) + regs.totalRevenue;

  return {
    revenue: payments.totalReceived > 0 ? payments.totalReceived : estimatedRevenue,
    expenses: expenses.totalExpenses,
    profit: (payments.totalReceived > 0 ? payments.totalReceived : estimatedRevenue) - expenses.totalExpenses,
    students: signups.count + regs.count,
    payments: payments.count
  };
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   DEMO DATA
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function getDemoData_() {
  return {
    monthName: 'January 2026',
    signupData: { count: 8, students: ['Sarah Johnson', 'Mike Rivera', 'Emily Chen', 'James Wilson', 'Ana Martinez', 'David Park', 'Lisa Thompson', 'Kevin Brown'] },
    registrationData: {
      count: 12,
      totalRevenue: 8440,
      packages: { '3 Lessons': 2, '5 Lessons': 3, '10 Lessons': 4, '15 Lessons': 2, '25 Lessons': 1 },
      students: ['Carlos Mendez', 'Jenny Liu', 'Omar Hassan', 'Priya Patel', 'Alex Kim', 'Rosa Garcia', 'Tyler Jones', 'Nina Popov', 'Derek Chang', 'Maria Santos', 'Ryan O\'Brien', 'Fatima Al-Said']
    },
    expenseData: {
      totalExpenses: 5430.25,
      categories: { 'Instructor Pay': 3000, 'Fuel / Gas': 645.50, 'Insurance': 600, 'Vehicle Maintenance': 425, 'Rent / Utilities': 500, 'Marketing / Advertising': 150, 'Office Supplies': 59.75, 'Miscellaneous': 50 }
    },
    paymentData: { totalReceived: 9285.00, count: 18, methods: { 'Cash': 4500, 'Zelle': 3785, 'Credit Card': 1000 } },
    prevSignup: { count: 6 },
    prevReg: { totalRevenue: 6500 },
    prevExp: { totalExpenses: 4800 },
    prevPayments: { totalReceived: 7100 },
    ytd: { revenue: 9285, expenses: 5430.25, profit: 3854.75, students: 20, payments: 18 },
    dataErrors: []
  };
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   MAIN REPORT GENERATOR
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function generateMonthlyReport() {
  const demo = isDemoMode_();
  const now = new Date();

  try {
    let d;

    if (demo) {
      Logger.log('โ•โ• DEMO MODE โ€” using sample data โ•โ•');
      d = getDemoData_();
    } else {
      // Report on previous month
      const reportMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
      const monthStart = new Date(reportMonth.getFullYear(), reportMonth.getMonth(), 1);
      const monthEnd = new Date(reportMonth.getFullYear(), reportMonth.getMonth() + 1, 0, 23, 59, 59);
      const monthName = Utilities.formatDate(reportMonth, CONFIG.TIMEZONE, 'MMMM yyyy');

      // Previous month for comparison
      const prevStart = new Date(reportMonth.getFullYear(), reportMonth.getMonth() - 1, 1);
      const prevEnd = new Date(reportMonth.getFullYear(), reportMonth.getMonth(), 0, 23, 59, 59);

      // Collect data
      const signupData = get5HourClassData_(monthStart, monthEnd);
      const registrationData = getRegistrationData_(monthStart, monthEnd);
      const expenseData = getExpenseData_(monthStart, monthEnd);
      const paymentData = getPaymentData_(monthStart, monthEnd);

      const prevSignup = get5HourClassData_(prevStart, prevEnd);
      const prevReg = getRegistrationData_(prevStart, prevEnd);
      const prevExp = getExpenseData_(prevStart, prevEnd);
      const prevPayments = getPaymentData_(prevStart, prevEnd);

      const ytd = getYTDData_(reportMonth.getFullYear());

      const dataErrors = [];
      if (signupData.error) dataErrors.push('5-Hour Signups: ' + signupData.error);
      if (registrationData.error) dataErrors.push('Registrations: ' + registrationData.error);
      if (expenseData.error) dataErrors.push('Expenses: ' + expenseData.error);
      if (paymentData.error) dataErrors.push('Payments: ' + paymentData.error);

      d = { monthName, signupData, registrationData, expenseData, paymentData, prevSignup, prevReg, prevExp, prevPayments, ytd, dataErrors };
    }

    // Calculate totals
    const classRevenue = d.signupData.count * CONFIG.FIVE_HOUR_CLASS_PRICE;
    const lessonRevenue = d.registrationData.totalRevenue;
    const estimatedRevenue = classRevenue + lessonRevenue;
    const actualRevenue = d.paymentData.totalReceived;
    const totalRevenue = actualRevenue > 0 ? actualRevenue : estimatedRevenue;
    const totalExpenses = d.expenseData.totalExpenses;
    const netProfit = totalRevenue - totalExpenses;

    const prevEstRevenue = (d.prevSignup.count * CONFIG.FIVE_HOUR_CLASS_PRICE) + (d.prevReg.totalRevenue || 0);
    const prevRevenue = (d.prevPayments && d.prevPayments.totalReceived > 0) ? d.prevPayments.totalReceived : prevEstRevenue;
    const prevExpenses = d.prevExp.totalExpenses || 0;
    const prevProfit = prevRevenue - prevExpenses;

    // Build trend (3 bars)
    const trend = buildTrend_(prevRevenue, totalRevenue);

    // Build email
    const html = buildReportEmail_({
      monthName: d.monthName,
      signupData: d.signupData,
      registrationData: d.registrationData,
      expenseData: d.expenseData,
      paymentData: d.paymentData,
      classRevenue,
      lessonRevenue,
      estimatedRevenue,
      actualRevenue,
      totalRevenue,
      totalExpenses,
      netProfit,
      prevRevenue,
      prevExpenses,
      prevProfit,
      ytd: d.ytd,
      trend,
      dataErrors: d.dataErrors || [],
      demo
    });

    // Send email
    const recipients = getAdminEmail_();
    const subject = (demo ? '[DEMO] ' : '') + '๐Ÿ“Š Monthly Revenue Report โ€” ' + d.monthName;

    if (!demo) {
      const toList = recipients.split(',').map(e => e.trim()).filter(Boolean);
      for (const to of toList) {
        MailApp.sendEmail({ to, subject, htmlBody: html });
      }
      Logger.log('Report sent to ' + toList.length + ' recipient(s)');
    } else {
      Logger.log('DEMO โ€” email not sent. Subject: ' + subject);
    }

    // Save to Report History
    saveReportHistory_(d.monthName, now, totalRevenue, totalExpenses, netProfit,
      d.signupData.count + d.registrationData.count, d.signupData.count, d.registrationData.count,
      d.paymentData ? d.paymentData.count : 0, d.ytd ? d.ytd.revenue : totalRevenue);

    Logger.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
    Logger.log('Revenue: ' + formatMoney_(totalRevenue) + ' | Expenses: ' + formatMoney_(totalExpenses) + ' | Net: ' + formatMoney_(netProfit));
    Logger.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');

  } catch (err) {
    Logger.log('ERROR in generateMonthlyReport: ' + err.message);
    if (!demo) {
      try {
        MailApp.sendEmail({
          to: getAdminEmail_(),
          subject: 'โš ๏ธ Monthly Revenue Report Error',
          htmlBody: buildEmailHtml_('Report Error',
            '<p style="color:#ccc;">Failed to generate the monthly report:</p>' +
            '<p style="color:#ff2d2d;font-family:monospace;">' + escHtml_(err.message) + '</p>' +
            '<p style="color:#999;">Check the Apps Script execution log for details.</p>')
        });
      } catch (e2) { /* silent */ }
    }
  }
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   TREND BUILDER
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function buildTrend_(prev, current) {
  // Simple 2-bar trend using block chars
  const max = Math.max(prev, current, 1);
  const bars = ['โ–', 'โ–‚', 'โ–ƒ', 'โ–„', 'โ–…', 'โ–†', 'โ–‡', 'โ–ˆ'];
  const prevBar = bars[Math.min(Math.floor((prev / max) * 7), 7)];
  const currBar = bars[Math.min(Math.floor((current / max) * 7), 7)];
  const direction = current > prev ? '๐Ÿ“ˆ' : current < prev ? '๐Ÿ“‰' : 'โžก๏ธ';
  return prevBar + currBar + ' ' + direction;
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   SAVE REPORT HISTORY
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function saveReportHistory_(monthName, date, revenue, expenses, profit, students, signups, regs, payments, ytdRevenue) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
    const sheet = ss.getSheetByName('Report History');
    if (!sheet) return;
    sheet.appendRow([monthName, date, revenue, expenses, profit, students, signups, regs, payments, ytdRevenue]);
    Logger.log('Saved to Report History โœ…');
  } catch (e) {
    Logger.log('Could not save to Report History: ' + e.message);
  }
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   API โ€” For BI Dashboard
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function getRevenueStats(monthYear) {
  const demo = isDemoMode_();

  if (demo) {
    return {
      demo: true,
      monthYear: 'January 2026',
      totalRevenue: 9285,
      totalExpenses: 5430.25,
      netProfit: 3854.75,
      newStudents: 20,
      fiveHourSignups: 8,
      registrations: 12,
      paymentsReceived: 18,
      topExpenseCategory: 'Instructor Pay',
      ytdRevenue: 9285,
      ytdProfit: 3854.75
    };
  }

  const now = new Date();
  const targetMonth = monthYear
    ? new Date(monthYear + ' 1')
    : new Date(now.getFullYear(), now.getMonth() - 1, 1);
  const monthStart = new Date(targetMonth.getFullYear(), targetMonth.getMonth(), 1);
  const monthEnd = new Date(targetMonth.getFullYear(), targetMonth.getMonth() + 1, 0, 23, 59, 59);

  const signups = get5HourClassData_(monthStart, monthEnd);
  const regs = getRegistrationData_(monthStart, monthEnd);
  const expenses = getExpenseData_(monthStart, monthEnd);
  const payments = getPaymentData_(monthStart, monthEnd);
  const ytd = getYTDData_(targetMonth.getFullYear());

  const totalRevenue = payments.totalReceived > 0
    ? payments.totalReceived
    : (signups.count * CONFIG.FIVE_HOUR_CLASS_PRICE) + regs.totalRevenue;

  const sortedCats = Object.entries(expenses.categories).sort((a, b) => b[1] - a[1]);

  return {
    demo: false,
    monthYear: Utilities.formatDate(targetMonth, CONFIG.TIMEZONE, 'MMMM yyyy'),
    totalRevenue,
    totalExpenses: expenses.totalExpenses,
    netProfit: totalRevenue - expenses.totalExpenses,
    newStudents: signups.count + regs.count,
    fiveHourSignups: signups.count,
    registrations: regs.count,
    paymentsReceived: payments.count,
    topExpenseCategory: sortedCats.length > 0 ? sortedCats[0][0] : 'N/A',
    ytdRevenue: ytd.revenue,
    ytdProfit: ytd.profit
  };
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   TEST / MANUAL RUN
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

/** Send a test report for the CURRENT month (not previous). Prefixed [TEST]. */
function testReport() {
  const demo = isDemoMode_();
  const now = new Date();
  const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
  const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
  const monthName = Utilities.formatDate(now, CONFIG.TIMEZONE, 'MMMM yyyy');

  let signupData, registrationData, expenseData, paymentData;

  if (demo) {
    const d = getDemoData_();
    signupData = d.signupData;
    registrationData = d.registrationData;
    expenseData = d.expenseData;
    paymentData = d.paymentData;
  } else {
    signupData = get5HourClassData_(monthStart, monthEnd);
    registrationData = getRegistrationData_(monthStart, monthEnd);
    expenseData = getExpenseData_(monthStart, monthEnd);
    paymentData = getPaymentData_(monthStart, monthEnd);
  }

  const classRevenue = signupData.count * CONFIG.FIVE_HOUR_CLASS_PRICE;
  const totalRevenue = paymentData.totalReceived > 0 ? paymentData.totalReceived : classRevenue + registrationData.totalRevenue;
  const netProfit = totalRevenue - expenseData.totalExpenses;

  Logger.log('=== TEST REPORT ===');
  Logger.log('5-Hour: ' + signupData.count + ' (' + formatMoney_(classRevenue) + ')');
  Logger.log('Registrations: ' + registrationData.count + ' (' + formatMoney_(registrationData.totalRevenue) + ')');
  Logger.log('Actual Payments: ' + formatMoney_(paymentData.totalReceived) + ' (' + paymentData.count + ' payments)');
  Logger.log('Revenue: ' + formatMoney_(totalRevenue) + ' | Expenses: ' + formatMoney_(expenseData.totalExpenses) + ' | Net: ' + formatMoney_(netProfit));

  const html = buildReportEmail_({
    monthName, signupData, registrationData, expenseData, paymentData,
    classRevenue, lessonRevenue: registrationData.totalRevenue,
    estimatedRevenue: classRevenue + registrationData.totalRevenue,
    actualRevenue: paymentData.totalReceived,
    totalRevenue, totalExpenses: expenseData.totalExpenses, netProfit,
    prevRevenue: 0, prevExpenses: 0, prevProfit: 0,
    ytd: { revenue: totalRevenue, expenses: expenseData.totalExpenses, profit: netProfit, students: signupData.count + registrationData.count, payments: paymentData.count },
    trend: 'โ–…โ–ˆ ๐Ÿ“ˆ', dataErrors: [], demo
  });

  if (!demo) {
    MailApp.sendEmail({
      to: getAdminEmail_(),
      subject: '[TEST] ๐Ÿ“Š Monthly Revenue Report โ€” ' + monthName,
      htmlBody: html
    });
    Logger.log('Test email sent โœ…');
  } else {
    Logger.log('DEMO โ€” test email not sent. HTML length: ' + html.length);
  }
}

/** Dry run โ€” log only, no email */
function testReportDryRun() {
  const now = new Date();
  const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
  const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);

  Logger.log('=== DRY RUN ===');
  const signups = get5HourClassData_(monthStart, monthEnd);
  const regs = getRegistrationData_(monthStart, monthEnd);
  const expenses = getExpenseData_(monthStart, monthEnd);
  const payments = getPaymentData_(monthStart, monthEnd);

  Logger.log('5-Hour: ' + signups.count + ' | Regs: ' + regs.count);
  Logger.log('Payments received: ' + formatMoney_(payments.totalReceived) + ' (' + payments.count + ')');
  Logger.log('Estimated revenue: ' + formatMoney_((signups.count * CONFIG.FIVE_HOUR_CLASS_PRICE) + regs.totalRevenue));
  Logger.log('Expenses: ' + formatMoney_(expenses.totalExpenses));
  if (signups.error) Logger.log('โš ๏ธ Signup error: ' + signups.error);
  if (regs.error) Logger.log('โš ๏ธ Registration error: ' + regs.error);
  if (expenses.error) Logger.log('โš ๏ธ Expense error: ' + expenses.error);
  if (payments.error) Logger.log('โš ๏ธ Payment error: ' + payments.error);
  Logger.log('No email sent.');
}

/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
   EMAIL BUILDER โ€” Mission Control Theme
   (Keeping Boss's sidebar design + upgrading)
   โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */

function buildReportEmail_(d) {
  const revenueChange = pctChange_(d.totalRevenue, d.prevRevenue);
  const expenseChange = pctChange_(d.totalExpenses, d.prevExpenses);
  const profitChange = pctChange_(d.netProfit, d.prevProfit);
  const profitPositive = d.netProfit >= 0;
  const revColor = revenueChange.startsWith('+') ? '#22c55e' : '#ff4444';
  const expColor = expenseChange.startsWith('+') ? '#ff4444' : '#22c55e';
  const profitColor = profitChange.startsWith('+') ? '#22c55e' : '#ff4444';

  // Revenue breakdown rows
  let packageRows = '';
  for (const [pkg, count] of Object.entries(d.registrationData.packages || {})) {
    const price = CONFIG.LESSON_PACKAGES[pkg] || 0;
    packageRows += '<tr><td style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.06);color:#ccc;font-size:14px;">' + escHtml_(pkg) + '</td>' +
      '<td style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.06);color:#ccc;text-align:center;">' + count + '</td>' +
      '<td style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.06);color:#fff;text-align:right;font-weight:600;">' + formatMoney_(price * count) + '</td></tr>';
  }
  if (d.signupData.count > 0) {
    packageRows += '<tr><td style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.06);color:#ccc;">5-Hour Pre-Licensing</td>' +
      '<td style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.06);color:#ccc;text-align:center;">' + d.signupData.count + '</td>' +
      '<td style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.06);color:#fff;text-align:right;font-weight:600;">' + formatMoney_(d.classRevenue) + '</td></tr>';
  }
  if (!packageRows) {
    packageRows = '<tr><td colspan="3" style="padding:16px;text-align:center;color:#666;font-style:italic;">No revenue recorded</td></tr>';
  }

  // Payment methods row (NEW)
  let paymentMethodRows = '';
  if (d.paymentData && d.paymentData.methods) {
    for (const [method, amount] of Object.entries(d.paymentData.methods)) {
      paymentMethodRows += '<tr><td style="padding:8px 14px;border-bottom:1px solid rgba(255,255,255,0.04);color:#ccc;font-size:13px;">' + escHtml_(method) + '</td>' +
        '<td style="padding:8px 14px;border-bottom:1px solid rgba(255,255,255,0.04);color:#22c55e;text-align:right;font-weight:600;">' + formatMoney_(amount) + '</td></tr>';
    }
  }

  // Expense breakdown rows
  const sortedExpenses = Object.entries(d.expenseData.categories || {}).sort((a, b) => b[1] - a[1]);
  let expenseRows = '';
  for (const [cat, amount] of sortedExpenses) {
    expenseRows += '<tr><td style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.06);color:#ccc;">' + escHtml_(cat) + '</td>' +
      '<td style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.06);color:#ff4444;text-align:right;font-weight:600;">' + formatMoney_(amount) + '</td></tr>';
  }
  if (!expenseRows) {
    expenseRows = '<tr><td colspan="2" style="padding:16px;text-align:center;color:#666;font-style:italic;">No expenses recorded</td></tr>';
  }

  // Student list
  const allStudents = [...new Set([...(d.signupData.students || []), ...(d.registrationData.students || [])])];
  let studentList = '';
  if (allStudents.length > 0) {
    for (const name of allStudents) {
      studentList += '<tr><td style="padding:6px 14px;border-bottom:1px solid rgba(255,255,255,0.04);">' +
        '<span style="display:inline-block;width:8px;height:8px;background:#ff2d2d;border-radius:50%;margin-right:10px;"></span>' +
        '<span style="color:#ddd;font-size:13px;">' + escHtml_(name) + '</span></td></tr>';
    }
  } else {
    studentList = '<tr><td style="padding:16px;text-align:center;color:#666;font-style:italic;">No new students</td></tr>';
  }

  // Data alerts
  let alertHtml = '';
  if (d.dataErrors && d.dataErrors.length > 0) {
    alertHtml = '<div style="padding:12px 24px;background:rgba(255,200,0,0.1);border:1px solid rgba(255,200,0,0.3);border-radius:8px;margin:8px 24px;">' +
      '<span style="color:#e6b800;font-size:12px;">โš ๏ธ ' + d.dataErrors.map(e => escHtml_(e)).join('<br/>') + '</span></div>';
  }

  // YTD section
  let ytdHtml = '';
  if (d.ytd) {
    ytdHtml = '<tr><td style="padding:8px 24px 16px;">' +
      '<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07);border-radius:16px;padding:16px;">' +
      '<div style="color:#fff;font-size:14px;font-weight:600;margin-bottom:12px;">๐Ÿ“ˆ Year-to-Date</div>' +
      '<table width="100%" cellpadding="0" cellspacing="0"><tr>' +
      '<td style="text-align:center;"><div style="color:#22c55e;font-size:18px;font-weight:700;">' + formatMoney_(d.ytd.revenue) + '</div><div style="color:#666;font-size:10px;">Revenue</div></td>' +
      '<td style="text-align:center;"><div style="color:#ff4444;font-size:18px;font-weight:700;">' + formatMoney_(d.ytd.expenses) + '</div><div style="color:#666;font-size:10px;">Expenses</div></td>' +
      '<td style="text-align:center;"><div style="color:' + (d.ytd.profit >= 0 ? '#ff2d2d' : '#ff4444') + ';font-size:18px;font-weight:700;">' + formatMoney_(d.ytd.profit) + '</div><div style="color:#666;font-size:10px;">Net Profit</div></td>' +
      '<td style="text-align:center;"><div style="color:#fff;font-size:18px;font-weight:700;">' + d.ytd.students + '</div><div style="color:#666;font-size:10px;">Students</div></td>' +
      '</tr></table></div></td></tr>';
  }

  // Sidebar
  const sidebarItems = [
    { icon: '๐Ÿ“Š', label: 'Overview', active: true },
    { icon: '๐Ÿ’ฐ', label: 'Revenue' },
    { icon: '๐Ÿ“‹', label: 'Expenses' },
    { icon: '๐Ÿ‘ฅ', label: 'Students' },
    { icon: '๐Ÿ“ˆ', label: 'Trends' },
    { icon: 'โš™๏ธ', label: 'Settings' }
  ];
  let sidebarHtml = '';
  for (const item of sidebarItems) {
    const bg = item.active ? 'background:rgba(255,45,45,0.15);border:1px solid rgba(255,45,45,0.25);' : 'background:transparent;border:1px solid transparent;';
    const color = item.active ? '#ff2d2d' : '#555';
    const weight = item.active ? '600' : '400';
    sidebarHtml += '<tr><td style="padding:4px 0;"><div style="padding:10px 14px;border-radius:12px;' + bg + 'text-align:center;">' +
      '<div style="font-size:20px;line-height:1;">' + item.icon + '</div>' +
      '<div style="font-size:9px;color:' + color + ';margin-top:4px;font-weight:' + weight + ';">' + item.label + '</div></div></td></tr>';
  }

  // Payment source indicator
  const revenueSourceNote = d.actualRevenue > 0
    ? '<div style="color:#22c55e;font-size:10px;margin-top:4px;">โœ… Actual payments received</div>'
    : '<div style="color:#ffaa00;font-size:10px;margin-top:4px;">โšก Estimated from package prices</div>';

  // Build full email
  return '<!DOCTYPE html><html><head><meta charset="utf-8"></head>' +
    '<body style="margin:0;padding:0;background:#000;font-family:-apple-system,BlinkMacSystemFont,\'SF Pro Display\',\'Helvetica Neue\',Arial,sans-serif;">' +
    '<table width="100%" cellpadding="0" cellspacing="0" style="background:#000;padding:16px;"><tr><td align="center">' +
    '<table width="700" cellpadding="0" cellspacing="0" style="background:#0d0d0d;border-radius:20px;overflow:hidden;border:1px solid rgba(255,255,255,0.06);">' +

    // Sidebar + Content
    '<tr>' +
    // Sidebar
    '<td width="72" valign="top" style="background:#0a0a0a;border-right:1px solid rgba(255,255,255,0.06);padding:16px 8px;">' +
    '<table width="100%" cellpadding="0" cellspacing="0">' +
    '<tr><td style="text-align:center;padding:8px 0 20px;"><div style="width:42px;height:42px;background:linear-gradient(135deg,#ff2d2d,#cc0000);border-radius:14px;line-height:42px;font-size:18px;margin:0 auto;font-weight:bold;color:#fff;">๐Ÿซ</div></td></tr>' +
    '<tr><td style="padding:0 10px 12px;"><div style="height:1px;background:rgba(255,255,255,0.06);"></div></td></tr>' +
    sidebarHtml +
    '<tr><td style="padding:16px 10px 8px;"><div style="height:1px;background:rgba(255,255,255,0.06);"></div></td></tr>' +
    '<tr><td style="text-align:center;padding:4px 0;"><div style="width:8px;height:8px;background:#22c55e;border-radius:50%;margin:0 auto;"></div><div style="font-size:8px;color:#444;margin-top:4px;">LIVE</div></td></tr>' +
    '</table></td>' +

    // Main content
    '<td valign="top" style="padding:0;"><table width="100%" cellpadding="0" cellspacing="0">' +

    // Header bar
    '<tr><td style="padding:18px 24px;border-bottom:1px solid rgba(255,255,255,0.06);"><table width="100%"><tr>' +
    '<td><span style="color:#fff;font-size:18px;font-weight:700;">Mission Control</span> <span style="color:#333;">/</span> <span style="color:#888;font-size:15px;">Revenue Report</span></td>' +
    '<td style="text-align:right;"><span style="display:inline-block;background:rgba(255,45,45,0.12);border:1px solid rgba(255,45,45,0.25);color:#ff2d2d;font-size:11px;font-weight:600;padding:4px 12px;border-radius:20px;">' + escHtml_(d.monthName) + (d.demo ? ' โ€ข DEMO' : '') + '</span></td>' +
    '</tr></table></td></tr>' +

    // Data alerts
    (alertHtml ? '<tr><td>' + alertHtml + '</td></tr>' : '') +

    // Summary cards
    '<tr><td style="padding:20px 24px 12px;"><table width="100%" cellpadding="0" cellspacing="0"><tr>' +
    // Revenue card
    '<td width="33%" style="padding-right:8px;"><div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07);border-radius:16px;padding:18px 16px;">' +
    '<table width="100%"><tr><td><span style="font-size:11px;color:#666;text-transform:uppercase;">Revenue</span></td><td style="text-align:right;">๐Ÿ’ฐ</td></tr></table>' +
    '<div style="font-size:24px;font-weight:700;color:#22c55e;margin:8px 0 4px;">' + formatMoney_(d.totalRevenue) + '</div>' +
    '<span style="font-size:11px;color:' + revColor + ';">' + revenueChange + '</span> <span style="font-size:11px;color:#444;">vs prior</span>' +
    revenueSourceNote +
    '</div></td>' +
    // Expenses card
    '<td width="33%" style="padding:0 4px;"><div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07);border-radius:16px;padding:18px 16px;">' +
    '<table width="100%"><tr><td><span style="font-size:11px;color:#666;text-transform:uppercase;">Expenses</span></td><td style="text-align:right;">๐Ÿ”ฅ</td></tr></table>' +
    '<div style="font-size:24px;font-weight:700;color:#ff4444;margin:8px 0 4px;">' + formatMoney_(d.totalExpenses) + '</div>' +
    '<span style="font-size:11px;color:' + expColor + ';">' + expenseChange + '</span> <span style="font-size:11px;color:#444;">vs prior</span>' +
    '</div></td>' +
    // Net Profit card
    '<td width="33%" style="padding-left:8px;"><div style="background:' + (profitPositive ? 'rgba(255,45,45,0.06)' : 'rgba(255,255,255,0.03)') + ';border:1px solid ' + (profitPositive ? 'rgba(255,45,45,0.18)' : 'rgba(255,255,255,0.07)') + ';border-radius:16px;padding:18px 16px;">' +
    '<table width="100%"><tr><td><span style="font-size:11px;color:#666;text-transform:uppercase;">Net Profit</span></td><td style="text-align:right;">โšก</td></tr></table>' +
    '<div style="font-size:24px;font-weight:700;color:' + (profitPositive ? '#ff2d2d' : '#ff4444') + ';margin:8px 0 4px;">' + formatMoney_(d.netProfit) + '</div>' +
    '<span style="font-size:11px;color:' + profitColor + ';">' + profitChange + '</span> <span style="font-size:11px;color:#444;">vs prior</span>' +
    '<div style="color:#888;font-size:12px;margin-top:4px;">' + (d.trend || '') + '</div>' +
    '</div></td>' +
    '</tr></table></td></tr>' +

    // Revenue breakdown + Students
    '<tr><td style="padding:8px 24px 12px;"><table width="100%" cellpadding="0" cellspacing="0"><tr>' +
    // Revenue table
    '<td width="60%" valign="top" style="padding-right:8px;"><div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07);border-radius:16px;overflow:hidden;">' +
    '<div style="padding:14px 16px 10px;border-bottom:1px solid rgba(255,255,255,0.05);">๐Ÿ’ฐ <span style="color:#fff;font-size:14px;font-weight:600;">Revenue Breakdown</span></div>' +
    '<table width="100%"><tr style="background:rgba(255,255,255,0.02);"><th style="padding:8px 12px;text-align:left;font-size:10px;color:#555;">Service</th><th style="padding:8px 12px;text-align:center;font-size:10px;color:#555;">Qty</th><th style="padding:8px 12px;text-align:right;font-size:10px;color:#555;">Amount</th></tr>' +
    packageRows +
    '<tr style="background:rgba(255,255,255,0.02);"><td style="padding:10px 12px;color:#fff;font-weight:700;" colspan="2">Total (Estimated)</td><td style="padding:10px 12px;text-align:right;color:#22c55e;font-weight:700;">' + formatMoney_(d.estimatedRevenue) + '</td></tr></table></div>' +

    // Payment methods (if available)
    (paymentMethodRows ? '<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07);border-radius:16px;overflow:hidden;margin-top:8px;">' +
    '<div style="padding:14px 16px 10px;border-bottom:1px solid rgba(255,255,255,0.05);">๐Ÿ’ณ <span style="color:#fff;font-size:14px;font-weight:600;">Payments Received</span> <span style="display:inline-block;background:#22c55e;color:#fff;font-size:10px;font-weight:700;padding:2px 7px;border-radius:8px;margin-left:6px;">' + (d.paymentData ? d.paymentData.count : 0) + '</span></div>' +
    '<table width="100%">' + paymentMethodRows +
    '<tr style="background:rgba(255,255,255,0.02);"><td style="padding:10px 14px;color:#fff;font-weight:700;">Total Received</td><td style="padding:10px 14px;text-align:right;color:#22c55e;font-weight:700;">' + formatMoney_(d.actualRevenue) + '</td></tr></table></div>' : '') +

    '</td>' +

    // Students list
    '<td width="40%" valign="top" style="padding-left:8px;"><div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07);border-radius:16px;overflow:hidden;">' +
    '<div style="padding:14px 16px 10px;border-bottom:1px solid rgba(255,255,255,0.05);">๐Ÿ‘ฅ <span style="color:#fff;font-size:14px;font-weight:600;">New Students</span> <span style="display:inline-block;background:#ff2d2d;color:#fff;font-size:10px;font-weight:700;padding:2px 7px;border-radius:8px;margin-left:6px;">' + allStudents.length + '</span></div>' +
    '<table width="100%">' + studentList + '</table></div></td>' +
    '</tr></table></td></tr>' +

    // Expense breakdown
    '<tr><td style="padding:0 24px 12px;"><div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07);border-radius:16px;overflow:hidden;">' +
    '<div style="padding:14px 16px 10px;border-bottom:1px solid rgba(255,255,255,0.05);">๐Ÿ“‹ <span style="color:#fff;font-size:14px;font-weight:600;">Expense Breakdown</span></div>' +
    '<table width="100%"><tr style="background:rgba(255,255,255,0.02);"><th style="padding:8px 12px;text-align:left;font-size:10px;color:#555;">Category</th><th style="padding:8px 12px;text-align:right;font-size:10px;color:#555;">Amount</th></tr>' +
    expenseRows +
    '<tr style="background:rgba(255,255,255,0.02);"><td style="padding:10px 12px;color:#fff;font-weight:700;">Total Expenses</td><td style="padding:10px 12px;text-align:right;color:#ff4444;font-weight:700;">' + formatMoney_(d.totalExpenses) + '</td></tr></table></div></td></tr>' +

    // YTD
    ytdHtml +

    // Footer
    '<tr><td style="padding:12px 24px 18px;border-top:1px solid rgba(255,255,255,0.04);"><table width="100%"><tr>' +
    '<td><span style="font-size:11px;color:#333;">Powered by </span><span style="font-size:11px;color:#ff2d2d;font-weight:600;">' + escHtml_(CONFIG.SCHOOL_NAME) + ' โ€” Mission Control</span></td>' +
    '<td style="text-align:right;"><span style="font-size:11px;color:#444;">All systems operational</span></td>' +
    '</tr></table></td></tr>' +

    // Unsubscribe
    '<tr><td style="padding:0 24px 16px;text-align:center;"><span style="font-size:10px;color:#333;">To stop receiving this report, reply "STOP" or update Settings.</span></td></tr>' +

    '</table></td></tr></table></td></tr></table></body></html>';
}

/** Generic Mission Control email wrapper (for error emails) */
function buildEmailHtml_(title, bodyContent) {
  return '<!DOCTYPE html><html><head><meta charset="utf-8"></head>' +
    '<body style="margin:0;padding:0;background:#000;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;">' +
    '<table width="100%" cellpadding="0" cellspacing="0" style="background:#000;"><tr><td align="center" style="padding:20px;">' +
    '<table width="600" cellpadding="0" cellspacing="0" style="background:#0d0d0d;border:1px solid rgba(255,45,45,0.2);border-radius:12px;overflow:hidden;">' +
    '<tr><td style="background:linear-gradient(135deg,#1a0000,#0d0d0d);padding:24px 32px;border-bottom:1px solid rgba(255,45,45,0.15);">' +
    '<span style="color:#ff2d2d;font-size:20px;font-weight:bold;">๐Ÿซ ' + escHtml_(CONFIG.SCHOOL_NAME) + '</span></td></tr>' +
    '<tr><td style="padding:20px 32px 0;"><h1 style="color:#fff;font-size:22px;margin:0 0 4px;">' + title + '</h1><div style="width:40px;height:3px;background:#ff2d2d;border-radius:2px;"></div></td></tr>' +
    '<tr><td style="padding:20px 32px 32px;">' + bodyContent + '</td></tr>' +
    '<tr><td style="padding:20px 32px;border-top:1px solid rgba(255,255,255,0.05);text-align:center;"><p style="color:#333;font-size:10px;margin:0;">Automated by Mission Control</p></td></tr>' +
    '</table></td></tr></table></body></html>';
}
๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง Parent Dashboard 52KB ยท 1,234 linesGSโ–ถ
/**
 * =========================================================
 *  PARENT DASHBOARD โ€” Automation #15
 *  Flavors Driving School
 * =========================================================
 *  Web portal for parents of under-18 students
 *  - Demo mode with fake parent/student data
 *  - Phone verification (last 4 digits)
 *  - Email-first + fuzzy name matching
 *  - View child's lesson progress & attendance
 *  - See upcoming scheduled lessons
 *  - Track payment balance & payment history
 *  - Automated weekly progress emails (MailApp + unsubscribe)
 *  - Multi-child support (one parent, multiple students)
 *  - "Search Again" button
 *  - SMS notification preference flag (future)
 *  - Mission Control dark theme (consistent #0d0d0d)
 * =========================================================
 */

// โ”€โ”€ CONFIG โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const CONFIG = {
  SCHEDULE_BOARD_ID: '1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE',
  REGISTRATION_ID: '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',
  PAYMENT_TRACKER_ID: '1XWNU7PygQX9jV6mFUUrgHYU_vrhS7NP1OHwNUNWRoOk',
  SCHOOL_NAME: 'Flavors Driving School',
  SCHOOL_EMAIL: '[email protected]',

  // DEMO MODE โ€” set to true for presentations
  DEMO_MODE: true,
};

// โ”€โ”€ DEMO DATA โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const DEMO = {
  parents: {
    '[email protected]': {
      name: 'Maria Rivera',
      phone: '5551234',
      children: ['Alex Rivera', 'Sofia Rivera'],
    },
    '[email protected]': {
      name: 'James Johnson',
      phone: '5559876',
      children: ['Sarah Johnson'],
    },
  },
  students: {
    'alex rivera': {
      name: 'Alex Rivera',
      registration: { package: '10 Lessons', dob: '3/15/2008', phone: '555-4321', email: '[email protected]' },
      lessons: [
        { date: '6/20/2025', time: '10:00 AM', instructor: 'Carlos', status: 'Completed' },
        { date: '6/18/2025', time: '2:00 PM', instructor: 'Carlos', status: 'Completed' },
        { date: '6/15/2025', time: '10:00 AM', instructor: 'Anisha', status: 'No Show' },
        { date: '6/12/2025', time: '11:00 AM', instructor: 'Carlos', status: 'Completed' },
      ],
      upcoming: [
        { date: '7/1/2025', dayName: 'Tue', time: '10:00 AM', instructor: 'Carlos' },
        { date: '7/3/2025', dayName: 'Thu', time: '2:00 PM', instructor: 'Carlos' },
      ],
      payments: { balance: 150, totalPaid: 350, totalDue: 500, payments: [
        { date: '6/1/2025', amount: 200, method: 'Zelle' },
        { date: '5/15/2025', amount: 150, method: 'Cash' },
      ]},
      progress: { totalLessons: 10, completedLessons: 3, percentage: 30, remaining: 7, noShows: 1, onTime: 3, attendanceRate: 75 },
    },
    'sofia rivera': {
      name: 'Sofia Rivera',
      registration: { package: '5 Lessons', dob: '8/22/2009', phone: '555-4321', email: '[email protected]' },
      lessons: [
        { date: '6/19/2025', time: '3:00 PM', instructor: 'Anisha', status: 'Completed' },
      ],
      upcoming: [
        { date: '7/2/2025', dayName: 'Wed', time: '11:00 AM', instructor: 'Anisha' },
      ],
      payments: { balance: 0, totalPaid: 250, totalDue: 250, payments: [
        { date: '5/20/2025', amount: 250, method: 'Card' },
      ]},
      progress: { totalLessons: 5, completedLessons: 1, percentage: 20, remaining: 4, noShows: 0, onTime: 1, attendanceRate: 100 },
    },
    'sarah johnson': {
      name: 'Sarah Johnson',
      registration: { package: '10 Lessons', dob: '1/10/2008', phone: '555-8765', email: '[email protected]' },
      lessons: [
        { date: '6/21/2025', time: '9:00 AM', instructor: 'Nick', status: 'Completed' },
        { date: '6/19/2025', time: '9:00 AM', instructor: 'Nick', status: 'Completed' },
        { date: '6/17/2025', time: '9:00 AM', instructor: 'Nick', status: 'Completed' },
        { date: '6/14/2025', time: '11:00 AM', instructor: 'Anisha', status: 'Completed' },
        { date: '6/12/2025', time: '9:00 AM', instructor: 'Nick', status: 'Completed' },
      ],
      upcoming: [
        { date: '7/1/2025', dayName: 'Tue', time: '9:00 AM', instructor: 'Nick' },
        { date: '7/3/2025', dayName: 'Thu', time: '9:00 AM', instructor: 'Nick' },
        { date: '7/5/2025', dayName: 'Sat', time: '10:00 AM', instructor: 'Nick' },
      ],
      payments: { balance: 100, totalPaid: 400, totalDue: 500, payments: [
        { date: '6/1/2025', amount: 200, method: 'Zelle' },
        { date: '5/10/2025', amount: 200, method: 'Cash' },
      ]},
      progress: { totalLessons: 10, completedLessons: 5, percentage: 50, remaining: 5, noShows: 0, onTime: 5, attendanceRate: 100 },
    },
  },
};

// โ”€โ”€ WEB APP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function doGet(e) {
  return HtmlService.createHtmlOutput(getPortalHTML_())
    .setTitle('Parent Dashboard โ€” ' + CONFIG.SCHOOL_NAME)
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .addMetaTag('viewport', 'width=device-width, initial-scale=1.0');
}

// โ”€โ”€ SETUP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function forceAuth() {
  SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  MailApp.getRemainingDailyQuota();
  Logger.log('โœ… All permissions authorized โ€” 3 sheets + email.');
}

function fullSetup() {
  createParentSheet_();
  setupProgressTrigger_();
  Logger.log('โœ… Parent Dashboard fully set up.');
}

function createParentSheet_() {
  const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  let sheet = ss.getSheetByName('Parent Links');
  if (!sheet) {
    sheet = ss.insertSheet('Parent Links');
    sheet.getRange('A1:G1').setValues([[
      'Student Name', 'Parent Name', 'Parent Email', 'Parent Phone', 'Date Linked', 'SMS Opt-In', 'Unsubscribed'
    ]]);
    sheet.getRange('A1:G1').setFontWeight('bold').setBackground('#1a1a1a').setFontColor('#ff2d2d').setFontSize(10);
    sheet.setFrozenRows(1);
    Logger.log('Created Parent Links sheet');
  }
}

function setupProgressTrigger_() {
  ScriptApp.getProjectTriggers().forEach(t => {
    if (t.getHandlerFunction() === 'sendWeeklyProgressToParents') {
      ScriptApp.deleteTrigger(t);
    }
  });

  ScriptApp.newTrigger('sendWeeklyProgressToParents')
    .timeBased()
    .onWeekDay(ScriptApp.WeekDay.FRIDAY)
    .atHour(17)
    .nearMinute(0)
    .create();

  Logger.log('โœ… Weekly progress email trigger created (Friday 5 PM)');
}

// โ”€โ”€ PHONE VERIFICATION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function verifyPhone(email, lastFour) {
  email = sanitize_(email).toLowerCase();
  lastFour = sanitize_(lastFour);

  if (!email || !lastFour || lastFour.length !== 4 || !/^\d{4}$/.test(lastFour)) {
    return { verified: false, error: 'Please enter the last 4 digits of your phone number.' };
  }

  if (CONFIG.DEMO_MODE) {
    const parent = DEMO.parents[email];
    if (!parent) return { verified: false, error: 'Email not found.' };
    const parentLast4 = parent.phone.replace(/\D/g, '').slice(-4);
    if (parentLast4 !== lastFour) return { verified: false, error: 'Phone number does not match our records.' };
    return { verified: true };
  }

  // Check Parent Links sheet
  const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  const parentSheet = ss.getSheetByName('Parent Links');
  if (!parentSheet || parentSheet.getLastRow() <= 1) return { verified: false, error: 'No parent records found.' };

  const data = parentSheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const emailCol = findCol_(headers, ['parent email', 'email']);
  const phoneCol = findCol_(headers, ['parent phone', 'phone']);

  for (let i = 1; i < data.length; i++) {
    const rowEmail = String(data[i][emailCol] || '').trim().toLowerCase();
    if (rowEmail === email) {
      const rowPhone = String(data[i][phoneCol] || '').replace(/\D/g, '');
      if (rowPhone.slice(-4) === lastFour) return { verified: true };
    }
  }

  // Also check registration sheet for student email matches
  const regSheet = ss.getSheets()[0];
  const regData = regSheet.getDataRange().getValues();
  const regHeaders = regData[0].map(h => String(h).toLowerCase().trim());
  const regEmailCol = findCol_(regHeaders, ['email', 'email address', 'student email']);
  const regPhoneCol = findCol_(regHeaders, ['phone', 'phone number', 'cell', 'mobile']);

  for (let i = 1; i < regData.length; i++) {
    const rowEmail = String(regData[i][regEmailCol] || '').trim().toLowerCase();
    if (rowEmail === email) {
      const rowPhone = String(regData[i][regPhoneCol] || '').replace(/\D/g, '');
      if (rowPhone.slice(-4) === lastFour) return { verified: true };
    }
  }

  return { verified: false, error: 'Phone number does not match our records.' };
}

// โ”€โ”€ DATA FETCHING (called from client) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function lookupParent(email) {
  email = sanitize_(email).toLowerCase();
  if (!email || !email.includes('@')) return { error: 'Please enter a valid email address.' };

  if (CONFIG.DEMO_MODE) return lookupParentDemo_(email);

  // Check Parent Links sheet
  const regSS = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  const parentSheet = regSS.getSheetByName('Parent Links');
  const children = [];

  if (parentSheet && parentSheet.getLastRow() > 1) {
    const data = parentSheet.getDataRange().getValues();
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const nameCol = findCol_(headers, ['student name', 'student', 'name']);
    const emailCol = findCol_(headers, ['parent email', 'email']);

    for (let i = 1; i < data.length; i++) {
      const parentEmail = String(data[i][emailCol] || '').trim().toLowerCase();
      if (parentEmail === email) {
        const studentName = String(data[i][nameCol] || '').trim();
        if (studentName && !children.includes(studentName)) children.push(studentName);
      }
    }
  }

  // Also check if email matches a student directly (parent using student's email)
  const regSheet = regSS.getSheets()[0];
  const regData = regSheet.getDataRange().getValues();
  const regHeaders = regData[0].map(h => String(h).toLowerCase().trim());
  const nameCol = findCol_(regHeaders, ['full name', 'name', 'student name']);
  const emailCol = findCol_(regHeaders, ['email', 'email address', 'student email']);

  for (let i = 1; i < regData.length; i++) {
    const studentEmail = String(regData[i][emailCol] || '').trim().toLowerCase();
    const studentName = String(regData[i][nameCol] || '').trim();
    if (studentEmail === email && studentName && !children.includes(studentName)) {
      // Also try fuzzy match to avoid near-dupes
      if (!children.some(c => fuzzyNameMatch_(c, studentName))) {
        children.push(studentName);
      }
    }
  }

  if (children.length === 0) {
    return { error: 'No students found linked to this email. Please contact us to set up parent access.' };
  }

  const results = children.map(name => getStudentData_(name));
  return { children: results };
}

function lookupParentDemo_(email) {
  const parent = DEMO.parents[email];
  if (!parent) {
    return { error: 'No students found linked to this email. Try [email protected] or [email protected]' };
  }
  const results = parent.children.map(name => {
    const key = name.toLowerCase();
    return DEMO.students[key] || { name: name, registration: {}, lessons: [], upcoming: [], payments: { balance: 0, totalPaid: 0, totalDue: 0, payments: [] }, progress: { totalLessons: 0, completedLessons: 0, percentage: 0, remaining: 0, noShows: 0, onTime: 0, attendanceRate: 100 } };
  });
  return { children: results };
}

function getStudentData_(studentName) {
  return {
    name: studentName,
    registration: getRegistrationInfo_(studentName),
    lessons: getLessonHistory_(studentName),
    upcoming: getUpcomingLessons_(studentName),
    payments: getPaymentInfo_(studentName),
    progress: calculateProgress_(studentName),
  };
}

function getRegistrationInfo_(name) {
  const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  const sheet = ss.getSheets()[0];
  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());

  const nameCol = findCol_(headers, ['full name', 'name', 'student name']);
  const emailCol = findCol_(headers, ['email', 'email address', 'student email']);
  const packageCol = findCol_(headers, ['lesson package', 'package', 'selected package']);
  const dobCol = findCol_(headers, ['date of birth', 'dob', 'birth date']);
  const phoneCol = findCol_(headers, ['phone', 'phone number', 'cell', 'mobile']);

  for (let i = 1; i < data.length; i++) {
    const rowName = String(data[i][nameCol] || '').trim();
    const rowEmail = emailCol !== -1 ? String(data[i][emailCol] || '').trim().toLowerCase() : '';

    if (rowName.toLowerCase() === name.toLowerCase() || fuzzyNameMatch_(rowName, name)) {
      return {
        package: String(data[i][packageCol] || ''),
        dob: data[i][dobCol] ? formatDate_(data[i][dobCol]) : '',
        phone: phoneCol !== -1 ? String(data[i][phoneCol] || '') : '',
        email: rowEmail,
      };
    }
  }
  return {};
}

function getLessonHistory_(name) {
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const sheet = ss.getSheetByName('Bookings');
  if (!sheet) return [];

  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const nameCol = findCol_(headers, ['student', 'student name', 'name']);
  const emailCol = findCol_(headers, ['email', 'student email']);
  const dateCol = findCol_(headers, ['date', 'lesson date', 'booking date']);
  const timeCol = findCol_(headers, ['time', 'lesson time', 'start time', 'time slot']);
  const instructorCol = findCol_(headers, ['instructor', 'instructor name']);
  const statusCol = findCol_(headers, ['status', 'booking status']);

  // Skip these statuses โ€” they're not real completed lessons
  const skipStatuses = ['cancelled', 'canceled', 'pending', 'scheduled', 'upcoming', 'rescheduled'];

  const lessons = [];
  const today = new Date();

  for (let i = 1; i < data.length; i++) {
    const rowName = String(data[i][nameCol] || '').trim();
    if (!matchesStudent_(rowName, '', name)) continue;

    const status = String(data[i][statusCol] || 'Completed').trim();
    if (skipStatuses.includes(status.toLowerCase())) continue;

    const rawDate = data[i][dateCol];
    const date = rawDate instanceof Date ? rawDate : new Date(rawDate);
    if (isNaN(date.getTime()) || date > today) continue;

    lessons.push({
      date: formatDate_(date),
      time: String(data[i][timeCol] || ''),
      instructor: String(data[i][instructorCol] || ''),
      status: status,
    });
  }

  lessons.sort((a, b) => new Date(b.date) - new Date(a.date));
  return lessons;
}

function getUpcomingLessons_(name) {
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const sheet = ss.getSheetByName('Bookings');
  if (!sheet) return [];

  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const nameCol = findCol_(headers, ['student', 'student name', 'name']);
  const dateCol = findCol_(headers, ['date', 'lesson date', 'booking date']);
  const timeCol = findCol_(headers, ['time', 'lesson time', 'start time', 'time slot']);
  const instructorCol = findCol_(headers, ['instructor', 'instructor name']);
  const statusCol = findCol_(headers, ['status', 'booking status']);

  // Skip all non-active statuses
  const skipStatuses = ['cancelled', 'canceled', 'no-show', 'no show', 'noshow', 'rescheduled', 'completed'];

  const upcoming = [];
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  for (let i = 1; i < data.length; i++) {
    const rowName = String(data[i][nameCol] || '').trim();
    if (!matchesStudent_(rowName, '', name)) continue;

    const status = String(data[i][statusCol] || '').trim().toLowerCase();
    if (skipStatuses.includes(status)) continue;

    const rawDate = data[i][dateCol];
    const date = rawDate instanceof Date ? rawDate : new Date(rawDate);
    if (isNaN(date.getTime()) || date < today) continue;

    upcoming.push({
      date: formatDate_(date),
      dayName: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][date.getDay()],
      time: String(data[i][timeCol] || ''),
      instructor: String(data[i][instructorCol] || ''),
    });
  }

  upcoming.sort((a, b) => new Date(a.date) - new Date(b.date));
  return upcoming;
}

function getPaymentInfo_(name) {
  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);

  let balance = 0, totalPaid = 0, totalDue = 0;
  const balSheet = ss.getSheetByName('Student Balances');

  if (balSheet) {
    const data = balSheet.getDataRange().getValues();
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const nameCol = findCol_(headers, ['student', 'student name', 'name']);
    const emailCol = findCol_(headers, ['email', 'student email']);
    const balCol = findCol_(headers, ['balance', 'remaining balance', 'amount due']);
    const paidCol = findCol_(headers, ['total paid', 'paid']);
    const dueCol = findCol_(headers, ['total due', 'package cost', 'total']);

    for (let i = 1; i < data.length; i++) {
      const rowName = String(data[i][nameCol] || '').trim();
      const rowEmail = emailCol !== -1 ? String(data[i][emailCol] || '').trim() : '';
      if (matchesStudent_(rowName, rowEmail, name)) {
        balance = parseFloat(data[i][balCol]) || 0;
        totalPaid = paidCol !== -1 ? (parseFloat(data[i][paidCol]) || 0) : 0;
        totalDue = dueCol !== -1 ? (parseFloat(data[i][dueCol]) || 0) : 0;
        break;
      }
    }
  }

  const payments = [];
  const paySheet = ss.getSheetByName('Payments') || ss.getSheets()[0];

  if (paySheet) {
    const data = paySheet.getDataRange().getValues();
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const nameCol = findCol_(headers, ['student', 'student name', 'name']);
    const emailCol = findCol_(headers, ['email', 'student email']);
    const amountCol = findCol_(headers, ['amount', 'payment amount']);
    const dateCol = findCol_(headers, ['date', 'payment date', 'timestamp']);
    const methodCol = findCol_(headers, ['method', 'payment method']);

    for (let i = 1; i < data.length; i++) {
      const rowName = String(data[i][nameCol] || '').trim();
      const rowEmail = emailCol !== -1 ? String(data[i][emailCol] || '').trim() : '';
      if (matchesStudent_(rowName, rowEmail, name)) {
        payments.push({
          date: formatDate_(data[i][dateCol]),
          amount: parseFloat(data[i][amountCol]) || 0,
          method: methodCol !== -1 ? String(data[i][methodCol] || '') : '',
        });
      }
    }
  }

  payments.sort((a, b) => new Date(b.date) - new Date(a.date));
  return { balance, totalPaid, totalDue, payments };
}

function calculateProgress_(name) {
  const reg = getRegistrationInfo_(name);
  const lessons = getLessonHistory_(name);

  const totalLessons = extractLessonCount_(reg.package);
  // getLessonHistory_ already filters out cancelled/pending/scheduled/upcoming/rescheduled
  // So count confirmed past lessons only
  const completedLessons = lessons.filter(l =>
    l.status.toLowerCase() !== 'no show' &&
    l.status.toLowerCase() !== 'no-show' &&
    l.status.toLowerCase() !== 'noshow'
  ).length;

  const percentage = totalLessons > 0 ? Math.round((completedLessons / totalLessons) * 100) : 0;
  const noShows = lessons.filter(l =>
    l.status.toLowerCase() === 'no show' ||
    l.status.toLowerCase() === 'no-show' ||
    l.status.toLowerCase() === 'noshow'
  ).length;
  const onTime = lessons.filter(l =>
    l.status.toLowerCase() === 'on time' ||
    l.status.toLowerCase() === 'completed'
  ).length;

  return {
    totalLessons,
    completedLessons,
    percentage: Math.min(100, percentage),
    remaining: Math.max(0, totalLessons - completedLessons),
    noShows,
    onTime,
    attendanceRate: (completedLessons + noShows) > 0 ? Math.round((completedLessons / (completedLessons + noShows)) * 100) : 100,
  };
}

// โ”€โ”€ STUDENT MATCHING (email-first + fuzzy name) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function matchesStudent_(rowName, rowEmail, targetName) {
  if (!rowName && !rowEmail) return false;
  // Exact name match
  if (rowName && rowName.toLowerCase() === targetName.toLowerCase()) return true;
  // Fuzzy name match
  if (rowName && fuzzyNameMatch_(rowName, targetName)) return true;
  return false;
}

// โ”€โ”€ WEEKLY PROGRESS EMAILS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function sendWeeklyProgressToParents() {
  if (CONFIG.DEMO_MODE) {
    Logger.log('โš ๏ธ DEMO MODE โ€” skipping weekly progress emails.');
    return;
  }

  try {
    const regSS = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
    const parentSheet = regSS.getSheetByName('Parent Links');
    if (!parentSheet || parentSheet.getLastRow() <= 1) return;

    const data = parentSheet.getDataRange().getValues();
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const nameCol = findCol_(headers, ['student name', 'student', 'name']);
    const parentNameCol = findCol_(headers, ['parent name', 'parent']);
    const emailCol = findCol_(headers, ['parent email', 'email']);
    const unsubCol = findCol_(headers, ['unsubscribed', 'unsub']);

    const parentMap = {};

    for (let i = 1; i < data.length; i++) {
      const studentName = String(data[i][nameCol] || '').trim();
      const parentName = parentNameCol !== -1 ? String(data[i][parentNameCol] || '').trim() : '';
      const parentEmail = String(data[i][emailCol] || '').trim().toLowerCase();
      const unsubscribed = unsubCol !== -1 ? String(data[i][unsubCol] || '').trim().toLowerCase() : '';

      if (!parentEmail || !studentName) continue;
      if (unsubscribed === 'yes' || unsubscribed === 'true') continue;

      if (!parentMap[parentEmail]) {
        parentMap[parentEmail] = { name: parentName, children: [] };
      }
      parentMap[parentEmail].children.push(studentName);
    }

    let sent = 0;
    for (const [email, parent] of Object.entries(parentMap)) {
      const childrenData = parent.children.map(name => ({
        name: escHtml_(name),
        progress: calculateProgress_(name),
        upcoming: getUpcomingLessons_(name).slice(0, 3),
        payments: getPaymentInfo_(name),
      }));

      const html = buildProgressEmail_(parent.name, childrenData);

      try {
        MailApp.sendEmail({
          to: email,
          subject: '๐Ÿ“Š Weekly Progress Report โ€” ' + CONFIG.SCHOOL_NAME,
          htmlBody: html,
          name: CONFIG.SCHOOL_NAME,
        });
        sent++;
      } catch (e) { Logger.log('Error emailing ' + email + ': ' + e.message); }
    }

    Logger.log('โœ… Sent weekly progress emails to ' + sent + ' parents.');
  } catch (e) {
    Logger.log('โŒ sendWeeklyProgressToParents error: ' + e.message);
  }
}

function buildProgressEmail_(parentName, children) {
  let childBlocks = '';

  children.forEach(function(child) {
    const p = child.progress;
    const barWidth = Math.max(5, p.percentage);
    const barColor = p.percentage >= 75 ? '#22c55e' : p.percentage >= 40 ? '#f59e0b' : '#ff2d2d';

    let upcomingRows = '';
    child.upcoming.forEach(function(u) {
      upcomingRows +=
        '<tr><td style="color:#ccc;padding:6px 8px;border-bottom:1px solid rgba(255,255,255,0.04);font-size:13px;">' + escHtml_(u.dayName) + ' ' + escHtml_(u.date) + '</td>' +
        '<td style="color:#ccc;padding:6px 8px;border-bottom:1px solid rgba(255,255,255,0.04);font-size:13px;">' + escHtml_(u.time) + '</td>' +
        '<td style="color:#ccc;padding:6px 8px;border-bottom:1px solid rgba(255,255,255,0.04);font-size:13px;">' + escHtml_(u.instructor) + '</td></tr>';
    });

    childBlocks +=
      '<table width="100%" style="background:rgba(255,255,255,0.03);border-radius:14px;margin:15px 0;border:1px solid rgba(255,255,255,0.06);" cellpadding="0" cellspacing="0">' +
      '<tr><td style="padding:20px;">' +
      '<p style="color:#fff;font-size:18px;font-weight:600;margin:0 0 15px;">๐ŸŽ“ ' + child.name + '</p>' +

      '<p style="color:#555;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin:0 0 8px;">Progress</p>' +
      '<div style="background:#0a0a0a;border-radius:6px;overflow:hidden;height:24px;margin-bottom:8px;">' +
      '<div style="background:' + barColor + ';height:100%;width:' + barWidth + '%;border-radius:6px;text-align:center;line-height:24px;color:#000;font-size:12px;font-weight:600;">' + p.percentage + '%</div></div>' +
      '<p style="color:#aaa;font-size:13px;margin:0 0 15px;">' + p.completedLessons + ' of ' + p.totalLessons + ' lessons completed โ€ข ' + p.remaining + ' remaining</p>' +

      (p.noShows > 0 ? '<p style="color:#ff2d2d;font-size:13px;margin:0 0 15px;">โš ๏ธ ' + p.noShows + ' no-show(s) recorded</p>' : '') +

      '<p style="color:#555;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin:0 0 8px;">Balance</p>' +
      '<p style="color:' + (child.payments.balance > 0 ? '#f59e0b' : '#22c55e') + ';font-size:16px;font-weight:600;margin:0 0 15px;">' +
      (child.payments.balance > 0 ? '$' + child.payments.balance.toFixed(2) + ' remaining' : 'โœ… Paid in full') + '</p>' +

      (child.upcoming.length > 0 ?
        '<p style="color:#555;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin:0 0 8px;">Upcoming Lessons</p>' +
        '<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:5px;">' +
        '<tr><td style="color:#ff2d2d;padding:6px 8px;font-size:10px;text-transform:uppercase;border-bottom:1px solid rgba(255,255,255,0.06);">Date</td>' +
        '<td style="color:#ff2d2d;padding:6px 8px;font-size:10px;text-transform:uppercase;border-bottom:1px solid rgba(255,255,255,0.06);">Time</td>' +
        '<td style="color:#ff2d2d;padding:6px 8px;font-size:10px;text-transform:uppercase;border-bottom:1px solid rgba(255,255,255,0.06);">Instructor</td></tr>' +
        upcomingRows + '</table>'
        : '<p style="color:#444;font-size:13px;">No upcoming lessons scheduled</p>') +

      '</td></tr></table>';
  });

  const unsubLink = '<p style="margin:8px 0 0;color:#333;font-size:10px;"><a href="mailto:' + escHtml_(CONFIG.SCHOOL_EMAIL) + '?subject=Unsubscribe%20Parent%20Progress%20Emails" style="color:#555;text-decoration:underline;">Unsubscribe from weekly updates</a></p>';

  return '<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"></head>' +
    '<body style="margin:0;padding:0;background:#000;font-family:-apple-system,BlinkMacSystemFont,\'SF Pro Display\',sans-serif;">' +
    '<table width="100%" cellpadding="0" cellspacing="0" style="background:#000;padding:20px;">' +
    '<tr><td align="center">' +
    '<table width="560" cellpadding="0" cellspacing="0" style="background:#0d0d0d;border-radius:20px;border:1px solid rgba(255,255,255,0.06);overflow:hidden;box-shadow:0 8px 30px rgba(0,0,0,0.5);">' +
    '<tr><td style="padding:30px 40px;border-bottom:1px solid rgba(255,255,255,0.06);">' +
    '<table width="100%"><tr>' +
    '<td style="color:#ff2d2d;font-size:22px;font-weight:700;">๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง Parent Progress Report</td>' +
    '<td align="right" style="color:#555;font-size:11px;">' + escHtml_(CONFIG.SCHOOL_NAME) + '</td>' +
    '</tr></table></td></tr>' +
    '<tr><td style="padding:30px 40px;">' +
    '<p style="color:#ccc;font-size:16px;line-height:1.6;">Hi <strong style="color:#fff;">' + escHtml_(parentName || 'there') + '</strong>,</p>' +
    '<p style="color:#aaa;font-size:15px;line-height:1.6;">Here\'s this week\'s progress update for your child' + (children.length > 1 ? 'ren' : '') + ':</p>' +
    childBlocks +
    '<p style="color:#444;font-size:13px;margin-top:25px;line-height:1.5;">Questions? Reply to this email or call us anytime.<br>โ€” ' + escHtml_(CONFIG.SCHOOL_NAME) + ' Team</p>' +
    '</td></tr>' +
    '<tr><td style="padding:16px 40px;border-top:1px solid rgba(255,255,255,0.06);text-align:center;">' +
    '<p style="color:#333;font-size:11px;margin:0;">Powered by Parent Dashboard โ€ข <span style="color:#ff2d2d;">' + escHtml_(CONFIG.SCHOOL_NAME) + '</span></p>' +
    unsubLink +
    '</td></tr></table></td></tr></table></body></html>';
}


// โ”€โ”€ PORTAL HTML โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function getPortalHTML_() {
  const demoNote = CONFIG.DEMO_MODE
    ? '<div style="background:rgba(255,45,45,0.08);border:1px solid rgba(255,45,45,0.2);border-radius:12px;padding:10px 16px;margin-bottom:20px;text-align:center;"><span style="color:#ff6b6b;font-size:12px;font-weight:700;letter-spacing:1px;text-transform:uppercase;">โœฆ Demo Mode โ€” Try [email protected] / 1234 or [email protected] / 9876</span></div>'
    : '';

  return '<!DOCTYPE html>\n' +
'<html lang="en">\n' +
'<head>\n' +
'<meta charset="utf-8">\n' +
'<meta name="viewport" content="width=device-width, initial-scale=1.0">\n' +
'<title>Parent Dashboard โ€” ' + escHtml_(CONFIG.SCHOOL_NAME) + '</title>\n' +
'<style>\n' +
'  * { margin: 0; padding: 0; box-sizing: border-box; }\n' +
'  body {\n' +
'    background: #000;\n' +
'    color: #ccc;\n' +
'    font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif;\n' +
'    min-height: 100vh;\n' +
'  }\n' +
'  .container { max-width: 800px; margin: 0 auto; padding: 20px; }\n' +
'  .header { text-align: center; padding: 40px 20px 30px; }\n' +
'  .header h1 { color: #ff2d2d; font-size: 28px; font-weight: 700; letter-spacing: -0.5px; margin-bottom: 6px; }\n' +
'  .header p { color: #555; font-size: 14px; }\n' +
'  .school-badge {\n' +
'    display: inline-block;\n' +
'    background: rgba(255,255,255,0.03);\n' +
'    border: 1px solid rgba(255,255,255,0.06);\n' +
'    border-radius: 20px;\n' +
'    padding: 6px 16px;\n' +
'    color: #888;\n' +
'    font-size: 12px;\n' +
'    margin-bottom: 20px;\n' +
'  }\n' +
'  .search-card {\n' +
'    background: #0d0d0d;\n' +
'    border: 1px solid rgba(255,255,255,0.06);\n' +
'    border-radius: 20px;\n' +
'    padding: 30px;\n' +
'    margin-bottom: 25px;\n' +
'    text-align: center;\n' +
'    box-shadow: 0 8px 30px rgba(0,0,0,0.5);\n' +
'  }\n' +
'  .search-card .icon { font-size: 48px; margin-bottom: 15px; display: block; }\n' +
'  .search-card p { color: #aaa; font-size: 15px; margin-bottom: 20px; line-height: 1.5; }\n' +
'  .search-row {\n' +
'    display: flex;\n' +
'    gap: 10px;\n' +
'    max-width: 500px;\n' +
'    margin: 0 auto 10px;\n' +
'  }\n' +
'  .search-row input {\n' +
'    flex: 1;\n' +
'    background: #0a0a0a;\n' +
'    border: 1px solid rgba(255,255,255,0.08);\n' +
'    border-radius: 12px;\n' +
'    padding: 14px 18px;\n' +
'    color: #fff;\n' +
'    font-size: 15px;\n' +
'    outline: none;\n' +
'    transition: border-color 0.3s;\n' +
'  }\n' +
'  .search-row input:focus { border-color: #ff2d2d; }\n' +
'  .search-row input::placeholder { color: #444; }\n' +
'  .search-row button, .btn-primary {\n' +
'    background: linear-gradient(135deg, #ff2d2d, #cc0000);\n' +
'    color: #fff;\n' +
'    border: none;\n' +
'    border-radius: 12px;\n' +
'    padding: 14px 24px;\n' +
'    font-size: 15px;\n' +
'    font-weight: 600;\n' +
'    cursor: pointer;\n' +
'    transition: all 0.3s;\n' +
'    white-space: nowrap;\n' +
'    box-shadow: 0 4px 16px rgba(255,45,45,0.3);\n' +
'  }\n' +
'  .search-row button:hover, .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(255,45,45,0.4); }\n' +
'  .search-row button:disabled { background: #333; cursor: not-allowed; transform: none; box-shadow: none; }\n' +
'  .phone-row {\n' +
'    display: flex;\n' +
'    gap: 10px;\n' +
'    max-width: 300px;\n' +
'    margin: 0 auto;\n' +
'  }\n' +
'  .phone-row input {\n' +
'    flex: 1;\n' +
'    background: #0a0a0a;\n' +
'    border: 1px solid rgba(255,255,255,0.08);\n' +
'    border-radius: 12px;\n' +
'    padding: 14px 18px;\n' +
'    color: #fff;\n' +
'    font-size: 15px;\n' +
'    outline: none;\n' +
'    text-align: center;\n' +
'    letter-spacing: 4px;\n' +
'    transition: border-color 0.3s;\n' +
'  }\n' +
'  .phone-row input:focus { border-color: #ff2d2d; }\n' +
'  .phone-row input::placeholder { color: #444; letter-spacing: 1px; }\n' +
'  .loading { text-align: center; padding: 40px; display: none; }\n' +
'  .spinner {\n' +
'    width: 40px; height: 40px;\n' +
'    border: 3px solid rgba(255,255,255,0.06);\n' +
'    border-top-color: #ff2d2d;\n' +
'    border-radius: 50%;\n' +
'    animation: spin 0.8s linear infinite;\n' +
'    margin: 0 auto 15px;\n' +
'  }\n' +
'  @keyframes spin { to { transform: rotate(360deg); } }\n' +
'  .error {\n' +
'    background: rgba(255,45,45,0.06);\n' +
'    border: 1px solid rgba(255,45,45,0.2);\n' +
'    border-radius: 12px;\n' +
'    padding: 16px 20px;\n' +
'    color: #ff6b6b;\n' +
'    text-align: center;\n' +
'    margin: 15px 0;\n' +
'    display: none;\n' +
'  }\n' +
'  .results { display: none; }\n' +
'  .search-again {\n' +
'    text-align: center;\n' +
'    margin-bottom: 24px;\n' +
'  }\n' +
'  .btn-secondary {\n' +
'    background: rgba(255,255,255,0.03);\n' +
'    color: #ccc;\n' +
'    border: 1px solid rgba(255,255,255,0.08);\n' +
'    border-radius: 12px;\n' +
'    padding: 12px 24px;\n' +
'    font-size: 14px;\n' +
'    font-weight: 500;\n' +
'    cursor: pointer;\n' +
'    transition: all 0.3s;\n' +
'  }\n' +
'  .btn-secondary:hover { background: rgba(255,255,255,0.06); border-color: #ff2d2d; color: #fff; }\n' +
'  .child-card {\n' +
'    background: #0d0d0d;\n' +
'    border: 1px solid rgba(255,255,255,0.06);\n' +
'    border-radius: 20px;\n' +
'    margin-bottom: 20px;\n' +
'    overflow: hidden;\n' +
'    transition: transform 0.2s;\n' +
'    box-shadow: 0 8px 30px rgba(0,0,0,0.5);\n' +
'  }\n' +
'  .child-card:hover { transform: translateY(-2px); }\n' +
'  .child-header {\n' +
'    background: rgba(255,255,255,0.02);\n' +
'    padding: 20px 24px;\n' +
'    border-bottom: 1px solid rgba(255,255,255,0.06);\n' +
'    display: flex;\n' +
'    align-items: center;\n' +
'    gap: 15px;\n' +
'  }\n' +
'  .child-avatar {\n' +
'    width: 48px; height: 48px;\n' +
'    background: linear-gradient(135deg, #ff2d2d, #cc0000);\n' +
'    border-radius: 14px;\n' +
'    display: flex;\n' +
'    align-items: center;\n' +
'    justify-content: center;\n' +
'    font-size: 22px;\n' +
'    color: #fff;\n' +
'    font-weight: 700;\n' +
'    box-shadow: 0 4px 15px rgba(255,45,45,0.3);\n' +
'  }\n' +
'  .child-name { color: #fff; font-size: 20px; font-weight: 600; }\n' +
'  .child-package { color: #888; font-size: 13px; margin-top: 2px; }\n' +
'  .child-body { padding: 24px; }\n' +
'  .stats-grid {\n' +
'    display: grid;\n' +
'    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));\n' +
'    gap: 12px;\n' +
'    margin-bottom: 24px;\n' +
'  }\n' +
'  .stat-bubble {\n' +
'    background: #0a0a0a;\n' +
'    border: 1px solid rgba(255,255,255,0.06);\n' +
'    border-radius: 14px;\n' +
'    padding: 16px;\n' +
'    text-align: center;\n' +
'    transition: all 0.3s;\n' +
'  }\n' +
'  .stat-bubble:hover {\n' +
'    transform: translateY(-3px) scale(1.02);\n' +
'    border-color: #ff2d2d;\n' +
'    box-shadow: 0 8px 25px rgba(255,45,45,0.15);\n' +
'  }\n' +
'  .stat-icon { font-size: 24px; margin-bottom: 8px; display: block; }\n' +
'  .stat-value { color: #fff; font-size: 22px; font-weight: 700; display: block; }\n' +
'  .stat-label { color: #555; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin-top: 4px; display: block; }\n' +
'  .progress-section { margin-bottom: 24px; }\n' +
'  .progress-label { display: flex; justify-content: space-between; margin-bottom: 8px; }\n' +
'  .progress-label span { color: #888; font-size: 13px; }\n' +
'  .progress-label strong { color: #fff; font-size: 13px; }\n' +
'  .progress-track {\n' +
'    background: #0a0a0a;\n' +
'    border-radius: 8px;\n' +
'    height: 20px;\n' +
'    overflow: hidden;\n' +
'    border: 1px solid rgba(255,255,255,0.06);\n' +
'  }\n' +
'  .progress-fill {\n' +
'    height: 100%;\n' +
'    border-radius: 8px;\n' +
'    transition: width 1s ease-out;\n' +
'    text-align: center;\n' +
'    line-height: 20px;\n' +
'    font-size: 11px;\n' +
'    font-weight: 600;\n' +
'    color: #000;\n' +
'  }\n' +
'  .section-title {\n' +
'    color: #ff2d2d;\n' +
'    font-size: 12px;\n' +
'    text-transform: uppercase;\n' +
'    letter-spacing: 1px;\n' +
'    margin: 24px 0 12px;\n' +
'    padding-bottom: 8px;\n' +
'    border-bottom: 1px solid rgba(255,255,255,0.06);\n' +
'  }\n' +
'  .lesson-item {\n' +
'    display: flex;\n' +
'    align-items: center;\n' +
'    padding: 12px 14px;\n' +
'    background: #0a0a0a;\n' +
'    border-radius: 12px;\n' +
'    margin-bottom: 8px;\n' +
'    border: 1px solid rgba(255,255,255,0.06);\n' +
'    transition: all 0.2s;\n' +
'  }\n' +
'  .lesson-item:hover { border-color: rgba(255,255,255,0.1); transform: translateX(3px); }\n' +
'  .lesson-dot {\n' +
'    width: 8px; height: 8px;\n' +
'    border-radius: 50%;\n' +
'    margin-right: 12px;\n' +
'    flex-shrink: 0;\n' +
'  }\n' +
'  .lesson-dot.upcoming { background: #3b82f6; box-shadow: 0 0 8px rgba(59,130,246,0.5); }\n' +
'  .lesson-dot.completed { background: #22c55e; box-shadow: 0 0 8px rgba(34,197,94,0.5); }\n' +
'  .lesson-dot.noshow { background: #ff2d2d; box-shadow: 0 0 8px rgba(255,45,45,0.5); }\n' +
'  .lesson-info { flex: 1; }\n' +
'  .lesson-date { color: #fff; font-size: 14px; font-weight: 500; }\n' +
'  .lesson-detail { color: #555; font-size: 12px; margin-top: 2px; }\n' +
'  .lesson-status {\n' +
'    font-size: 11px;\n' +
'    padding: 4px 10px;\n' +
'    border-radius: 8px;\n' +
'    font-weight: 600;\n' +
'    text-transform: uppercase;\n' +
'    letter-spacing: 0.5px;\n' +
'  }\n' +
'  .status-upcoming { background: rgba(59,130,246,0.1); color: #3b82f6; }\n' +
'  .status-completed { background: rgba(34,197,94,0.1); color: #22c55e; }\n' +
'  .status-noshow { background: rgba(255,45,45,0.1); color: #ff2d2d; }\n' +
'  .balance-card {\n' +
'    background: #0a0a0a;\n' +
'    border: 1px solid rgba(255,255,255,0.06);\n' +
'    border-radius: 14px;\n' +
'    padding: 20px;\n' +
'    text-align: center;\n' +
'    margin-bottom: 15px;\n' +
'  }\n' +
'  .balance-amount { font-size: 32px; font-weight: 700; margin: 8px 0; }\n' +
'  .balance-paid { color: #22c55e; }\n' +
'  .balance-due { color: #f59e0b; }\n' +
'  .balance-subtitle { color: #555; font-size: 12px; }\n' +
'  .payment-item {\n' +
'    display: flex;\n' +
'    justify-content: space-between;\n' +
'    padding: 10px 14px;\n' +
'    background: #0a0a0a;\n' +
'    border-radius: 10px;\n' +
'    margin-bottom: 6px;\n' +
'    border: 1px solid rgba(255,255,255,0.06);\n' +
'  }\n' +
'  .payment-item .date { color: #aaa; font-size: 13px; }\n' +
'  .payment-item .method { color: #555; font-size: 12px; }\n' +
'  .payment-item .amount { color: #22c55e; font-size: 14px; font-weight: 600; }\n' +
'  .empty-state { text-align: center; padding: 20px; color: #444; font-size: 14px; }\n' +
'  .footer { text-align: center; padding: 30px 20px; color: #333; font-size: 12px; }\n' +
'  .footer a { color: #ff2d2d; text-decoration: none; }\n' +
'  @media (max-width: 600px) {\n' +
'    .container { padding: 10px; }\n' +
'    .search-row, .phone-row { flex-direction: column; }\n' +
'    .stats-grid { grid-template-columns: repeat(2, 1fr); }\n' +
'    .child-header { padding: 16px; }\n' +
'    .child-body { padding: 16px; }\n' +
'  }\n' +
'</style>\n' +
'</head>\n' +
'<body>\n' +
'<div class="container">\n' +
'  <div class="header">\n' +
'    <div class="school-badge">๐Ÿš— ' + escHtml_(CONFIG.SCHOOL_NAME) + '</div>\n' +
'    <h1>๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง Parent Dashboard</h1>\n' +
'    <p>Track your child\'s driving lesson progress</p>\n' +
'  </div>\n' +
'  ' + demoNote + '\n' +
'  <div class="search-card" id="searchCard">\n' +
'    <span class="icon">๐Ÿ”</span>\n' +
'    <p>Enter your email address to view your child\'s progress, upcoming lessons, and payment status.</p>\n' +
'    <div id="emailStep">\n' +
'      <div class="search-row">\n' +
'        <input type="email" id="emailInput" placeholder="Enter your email address..." onkeypress="if(event.key===\'Enter\')doEmailStep()">\n' +
'        <button onclick="doEmailStep()" id="emailBtn">Continue</button>\n' +
'      </div>\n' +
'    </div>\n' +
'    <div id="phoneStep" style="display:none;">\n' +
'      <p style="color:#aaa;font-size:14px;margin-bottom:12px;">๐Ÿ”’ For security, enter the <strong style="color:#fff;">last 4 digits</strong> of your phone number on file.</p>\n' +
'      <div class="phone-row">\n' +
'        <input type="text" id="phoneInput" placeholder="1234" maxlength="4" pattern="[0-9]{4}" inputmode="numeric" onkeypress="if(event.key===\'Enter\')doPhoneStep()">\n' +
'        <button onclick="doPhoneStep()" id="phoneBtn" class="btn-primary">Verify</button>\n' +
'      </div>\n' +
'    </div>\n' +
'  </div>\n' +
'  <div class="error" id="errorMsg"></div>\n' +
'  <div class="loading" id="loading">\n' +
'    <div class="spinner"></div>\n' +
'    <p style="color:#555;">Loading dashboard...</p>\n' +
'  </div>\n' +
'  <div class="results" id="results"></div>\n' +
'  <div class="footer">\n' +
'    <p>Questions? Contact us at <a href="mailto:' + escHtml_(CONFIG.SCHOOL_EMAIL) + '">' + escHtml_(CONFIG.SCHOOL_EMAIL) + '</a></p>\n' +
'    <p style="margin-top:8px;">Powered by Parent Dashboard โ€ข ' + escHtml_(CONFIG.SCHOOL_NAME) + '</p>\n' +
'  </div>\n' +
'</div>\n' +
'<script>\n' +
'var currentEmail = "";\n' +
'\n' +
'function doEmailStep() {\n' +
'  var email = document.getElementById("emailInput").value.trim();\n' +
'  if (!email || !email.includes("@")) { showError("Please enter a valid email address."); return; }\n' +
'  currentEmail = email;\n' +
'  document.getElementById("errorMsg").style.display = "none";\n' +
'  document.getElementById("emailStep").style.display = "none";\n' +
'  document.getElementById("phoneStep").style.display = "block";\n' +
'  document.getElementById("phoneInput").focus();\n' +
'}\n' +
'\n' +
'function doPhoneStep() {\n' +
'  var code = document.getElementById("phoneInput").value.trim();\n' +
'  if (!code || code.length !== 4 || !/^\\d{4}$/.test(code)) {\n' +
'    showError("Please enter exactly 4 digits."); return;\n' +
'  }\n' +
'  document.getElementById("phoneBtn").disabled = true;\n' +
'  document.getElementById("loading").style.display = "block";\n' +
'  document.getElementById("errorMsg").style.display = "none";\n' +
'\n' +
'  google.script.run\n' +
'    .withSuccessHandler(function(res) {\n' +
'      if (!res.verified) {\n' +
'        document.getElementById("loading").style.display = "none";\n' +
'        document.getElementById("phoneBtn").disabled = false;\n' +
'        showError(res.error || "Verification failed.");\n' +
'        return;\n' +
'      }\n' +
'      // Verified โ€” now fetch data\n' +
'      google.script.run\n' +
'        .withSuccessHandler(handleResult)\n' +
'        .withFailureHandler(handleError)\n' +
'        .lookupParent(currentEmail);\n' +
'    })\n' +
'    .withFailureHandler(handleError)\n' +
'    .verifyPhone(currentEmail, code);\n' +
'}\n' +
'\n' +
'function handleResult(data) {\n' +
'  document.getElementById("loading").style.display = "none";\n' +
'  document.getElementById("phoneBtn").disabled = false;\n' +
'  if (data.error) { showError(data.error); return; }\n' +
'\n' +
'  var html = \'<div class="search-again"><button class="btn-secondary" onclick="searchAgain()">โ† Search Again</button></div>\';\n' +
'\n' +
'  data.children.forEach(function(child) {\n' +
'    var p = child.progress;\n' +
'    var barColor = p.percentage >= 75 ? "#22c55e" : p.percentage >= 40 ? "#f59e0b" : "#ff2d2d";\n' +
'    var initial = child.name.charAt(0).toUpperCase();\n' +
'\n' +
'    html += \'<div class="child-card">\';\n' +
'    html += \'<div class="child-header">\';\n' +
'    html += \'<div class="child-avatar">\' + esc(initial) + \'</div>\';\n' +
'    html += \'<div><div class="child-name">\' + esc(child.name) + \'</div>\';\n' +
'    html += \'<div class="child-package">\' + esc(child.registration.package || "Package not set") + \'</div></div>\';\n' +
'    html += \'</div>\';\n' +
'    html += \'<div class="child-body">\';\n' +
'\n' +
'    html += \'<div class="stats-grid">\';\n' +
'    html += statBubble("๐Ÿ“š", p.completedLessons, "Completed");\n' +
'    html += statBubble("๐Ÿ“…", p.remaining, "Remaining");\n' +
'    html += statBubble("โœ…", p.attendanceRate + "%", "Attendance");\n' +
'    html += statBubble("๐Ÿ’ฐ", child.payments.balance > 0 ? "$" + child.payments.balance.toFixed(0) : "Paid", "Balance");\n' +
'    html += \'</div>\';\n' +
'\n' +
'    html += \'<div class="progress-section">\';\n' +
'    html += \'<div class="progress-label"><span>Lesson Progress</span><strong>\' + p.completedLessons + " / " + p.totalLessons + \'</strong></div>\';\n' +
'    html += \'<div class="progress-track"><div class="progress-fill" style="width:\' + Math.max(3, p.percentage) + "%;background:" + barColor + \'">\' + p.percentage + \'%</div></div>\';\n' +
'    html += \'</div>\';\n' +
'\n' +
'    if (p.noShows > 0) {\n' +
'      html += \'<div style="background:rgba(255,45,45,0.06);border:1px solid rgba(255,45,45,0.2);border-radius:10px;padding:10px 14px;margin-bottom:15px;">\';\n' +
'      html += \'<span style="color:#ff2d2d;font-size:13px;">โš ๏ธ \' + p.noShows + \' no-show(s) recorded. Please ensure your child arrives on time.</span></div>\';\n' +
'    }\n' +
'\n' +
'    html += \'<div class="section-title">๐Ÿ“… Upcoming Lessons</div>\';\n' +
'    if (child.upcoming.length > 0) {\n' +
'      child.upcoming.forEach(function(u) {\n' +
'        html += \'<div class="lesson-item">\';\n' +
'        html += \'<div class="lesson-dot upcoming"></div>\';\n' +
'        html += \'<div class="lesson-info"><div class="lesson-date">\' + esc(u.dayName) + ", " + esc(u.date) + \'</div>\';\n' +
'        html += \'<div class="lesson-detail">\' + esc(u.time) + " โ€ข " + esc(u.instructor) + \'</div></div>\';\n' +
'        html += \'<span class="lesson-status status-upcoming">Upcoming</span>\';\n' +
'        html += \'</div>\';\n' +
'      });\n' +
'    } else {\n' +
'      html += \'<div class="empty-state">No upcoming lessons scheduled</div>\';\n' +
'    }\n' +
'\n' +
'    html += \'<div class="section-title">๐Ÿ“‹ Lesson History</div>\';\n' +
'    if (child.lessons.length > 0) {\n' +
'      child.lessons.slice(0, 10).forEach(function(l) {\n' +
'        var isNoShow = l.status.toLowerCase() === "no show" || l.status.toLowerCase() === "no-show" || l.status.toLowerCase() === "noshow";\n' +
'        var dotClass = isNoShow ? "noshow" : "completed";\n' +
'        var statusClass = isNoShow ? "status-noshow" : "status-completed";\n' +
'        html += \'<div class="lesson-item">\';\n' +
'        html += \'<div class="lesson-dot \' + dotClass + \'"></div>\';\n' +
'        html += \'<div class="lesson-info"><div class="lesson-date">\' + esc(l.date) + \'</div>\';\n' +
'        html += \'<div class="lesson-detail">\' + esc(l.time) + " โ€ข " + esc(l.instructor) + \'</div></div>\';\n' +
'        html += \'<span class="lesson-status \' + statusClass + \'">\' + esc(l.status) + \'</span>\';\n' +
'        html += \'</div>\';\n' +
'      });\n' +
'      if (child.lessons.length > 10) {\n' +
'        html += \'<div class="empty-state">+ \' + (child.lessons.length - 10) + \' more lessons</div>\';\n' +
'      }\n' +
'    } else {\n' +
'      html += \'<div class="empty-state">No lesson history yet</div>\';\n' +
'    }\n' +
'\n' +
'    html += \'<div class="section-title">๐Ÿ’ณ Payments</div>\';\n' +
'    html += \'<div class="balance-card">\';\n' +
'    html += \'<div class="balance-subtitle">CURRENT BALANCE</div>\';\n' +
'    if (child.payments.balance > 0) {\n' +
'      html += \'<div class="balance-amount balance-due">$\' + child.payments.balance.toFixed(2) + \'</div>\';\n' +
'      html += \'<div class="balance-subtitle">$\' + child.payments.totalPaid.toFixed(2) + " paid of $" + child.payments.totalDue.toFixed(2) + \'</div>\';\n' +
'    } else {\n' +
'      html += \'<div class="balance-amount balance-paid">โœ… Paid in Full</div>\';\n' +
'      html += \'<div class="balance-subtitle">Total: $\' + child.payments.totalPaid.toFixed(2) + \'</div>\';\n' +
'    }\n' +
'    html += \'</div>\';\n' +
'\n' +
'    if (child.payments.payments.length > 0) {\n' +
'      child.payments.payments.forEach(function(pay) {\n' +
'        html += \'<div class="payment-item">\';\n' +
'        html += \'<div><div class="date">\' + esc(pay.date) + \'</div><div class="method">\' + esc(pay.method) + \'</div></div>\';\n' +
'        html += \'<div class="amount">+$\' + pay.amount.toFixed(2) + \'</div>\';\n' +
'        html += \'</div>\';\n' +
'      });\n' +
'    }\n' +
'\n' +
'    html += \'</div></div>\';\n' +
'  });\n' +
'\n' +
'  document.getElementById("results").innerHTML = html;\n' +
'  document.getElementById("results").style.display = "block";\n' +
'  document.getElementById("searchCard").style.display = "none";\n' +
'}\n' +
'\n' +
'function searchAgain() {\n' +
'  document.getElementById("results").style.display = "none";\n' +
'  document.getElementById("results").innerHTML = "";\n' +
'  document.getElementById("searchCard").style.display = "block";\n' +
'  document.getElementById("emailStep").style.display = "block";\n' +
'  document.getElementById("phoneStep").style.display = "none";\n' +
'  document.getElementById("emailInput").value = "";\n' +
'  document.getElementById("phoneInput").value = "";\n' +
'  document.getElementById("errorMsg").style.display = "none";\n' +
'  currentEmail = "";\n' +
'}\n' +
'\n' +
'function handleError(err) {\n' +
'  document.getElementById("loading").style.display = "none";\n' +
'  document.getElementById("phoneBtn").disabled = false;\n' +
'  showError("Something went wrong. Please try again.");\n' +
'}\n' +
'\n' +
'function showError(msg) {\n' +
'  var el = document.getElementById("errorMsg");\n' +
'  el.textContent = msg;\n' +
'  el.style.display = "block";\n' +
'}\n' +
'\n' +
'function esc(s) {\n' +
'  if (!s) return "";\n' +
'  var d = document.createElement("div");\n' +
'  d.appendChild(document.createTextNode(String(s)));\n' +
'  return d.innerHTML;\n' +
'}\n' +
'\n' +
'function statBubble(icon, value, label) {\n' +
'  return \'<div class="stat-bubble">\'\n' +
'    + \'<span class="stat-icon">\' + icon + \'</span>\'\n' +
'    + \'<span class="stat-value">\' + value + \'</span>\'\n' +
'    + \'<span class="stat-label">\' + label + \'</span>\'\n' +
'    + \'</div>\';\n' +
'}\n' +
'</script>\n' +
'</body>\n' +
'</html>';
}


// ================================================================
//  FUZZY NAME MATCHING
// ================================================================

function fuzzyNameMatch_(name1, name2) {
  if (!name1 || !name2) return false;
  const n1 = name1.toLowerCase().replace(/\s+/g, ' ').trim();
  const n2 = name2.toLowerCase().replace(/\s+/g, ' ').trim();
  if (n1 === n2) return true;
  if (n1.includes(n2) || n2.includes(n1)) return true;

  const p1 = n1.split(' ').filter(Boolean);
  const p2 = n2.split(' ').filter(Boolean);

  if (p1.length >= 2 && p2.length >= 2) {
    if (p1[p1.length-1] === p2[p2.length-1] && p1[0].substring(0,3) === p2[0].substring(0,3)) return true;
    if (p1[0] === p2[p2.length-1] && p1[p1.length-1] === p2[0]) return true;
  }
  if (levenshtein_(n1, n2) <= 2) return true;
  return false;
}

function levenshtein_(a, b) {
  if (a.length === 0) return b.length;
  if (b.length === 0) return a.length;
  const m = [];
  for (let i = 0; i <= b.length; i++) m[i] = [i];
  for (let j = 0; j <= a.length; j++) m[0][j] = j;
  for (let i = 1; i <= b.length; i++) {
    for (let j = 1; j <= a.length; j++) {
      m[i][j] = b.charAt(i-1) === a.charAt(j-1) ? m[i-1][j-1] : Math.min(m[i-1][j-1]+1, m[i][j-1]+1, m[i-1][j]+1);
    }
  }
  return m[b.length][a.length];
}


// ================================================================
//  UTILITY FUNCTIONS
// ================================================================

function findCol_(headers, candidates) {
  for (const c of candidates) {
    const idx = headers.findIndex(h => h.includes(c.toLowerCase()));
    if (idx !== -1) return idx;
  }
  return -1;
}

function escHtml_(str) {
  if (!str) return '';
  return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function sanitize_(input) {
  if (!input) return '';
  return String(input).trim().replace(/[<>{}()\[\]\\\/]/g, '').replace(/\s+/g, ' ').substring(0, 200);
}

function formatDate_(date) {
  if (!date) return '';
  if (!(date instanceof Date)) date = new Date(date);
  if (isNaN(date.getTime())) return '';
  return (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear();
}

function extractLessonCount_(pkg) {
  if (!pkg) return 0;
  const match = pkg.toString().match(/(\d+)\s*lesson/i);
  if (match) return parseInt(match[1]);
  const match2 = pkg.toString().match(/^(\d+)$/);
  if (match2) return parseInt(match2[1]);
  const lower = pkg.toString().toLowerCase();
  if (lower.includes('beginner')) return 10;
  if (lower.includes('standard')) return 10;
  if (lower.includes('premium')) return 15;
  if (lower.includes('intensive')) return 20;
  return 0;
}
๐Ÿ’ฐ Payment Tracker 53KB ยท 1,346 linesGSโ–ถ
/**
 * =========================================================
 *  PAYMENT TRACKER
 *  Flavors Driving School
 * =========================================================
 *  Tracks all student payments, balances, and payment plans.
 *
 *  - Records payments with receipt emails
 *  - Auto-imports students from Registration + 5-Hour Signups
 *  - Per-student balance tracking with status colors
 *  - Payment plan management with installment tracking
 *  - Weekly overdue alerts (admin + student reminders)
 *  - 7-day friendly payment reminders (before overdue)
 *  - Refund processing
 *  - Student payment history API (for Student Portal)
 *  - Revenue trend tracking
 *  - Sync to Registration sheet
 *  - Dashboard with live stats
 *  - Demo mode for safe presentations
 * =========================================================
 */

const CONFIG = {
  // โ”€โ”€ Sheet IDs โ”€โ”€
  PAYMENT_TRACKER_ID: '1XWNU7PygQX9jV6mFUUrgHYU_vrhS7NP1OHwNUNWRoOk',
  REGISTRATION_ID:    '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',
  SIGNUPS_SHEET_ID:   '1lE2_amcNj3-ao3TiP_U0VpaWHqmx4R-0rkF3n3xl7fM',

  // โ”€โ”€ Sheet tabs โ”€โ”€
  REGISTRATION_SHEET_TAB: '',
  SIGNUPS_SHEET_TAB:      '',

  // โ”€โ”€ Branding โ”€โ”€
  SCHOOL_NAME:    'Flavors Driving School',
  SCHOOL_TAGLINE: 'Your Road to Freedom Starts Here',
  ADMIN_EMAIL:    '[email protected]',
  SCHOOL_PHONE:   '(718) 555-0100',

  TIMEZONE: 'America/New_York',

  PACKAGES: {
    '3 Lessons':     250,
    '5 Lessons':     445,
    '10 Lessons':    710,
    '15 Lessons':    950,
    '25 Lessons':    1500,
    '5-Hour Class':  65,
    'Beginner':      710,
    'Standard':      710,
    'Premium':       950,
    'Intensive':     1500
  },

  PAYMENT_PLAN_THRESHOLD: 445,
  PAYMENT_METHODS: ['Cash', 'Zelle', 'Credit Card', 'Check', 'Other'],
  OVERDUE_DAYS: 14,
  REMINDER_DAYS: 7,

  // โ”€โ”€ Demo mode โ”€โ”€
  DEMO_MODE: true
};

/* ================================================================
   DEMO DATA
   ================================================================ */
const DEMO = {
  payment: {
    id: 'PAY-0042',
    student: 'Sarah Johnson',
    email: '[email protected]',
    amount: 250,
    method: 'Zelle',
    totalPaid: 500,
    balance: 210,
    pkg: '10 Lessons'
  },
  dashboard: {
    totalStudents: 24,
    totalOwed: 14580,
    totalCollected: 9850,
    outstanding: 4730,
    collectionRate: '67.6%',
    paidInFull: 12,
    partial: 8,
    unpaid: 4,
    overdue: 3
  },
  overdueStudents: [
    { name: 'Mike Rivera',    balance: 460, days: '21 days', phone: '718-555-0456' },
    { name: 'David Chen',     balance: 710, days: '18 days', phone: '917-555-0789' },
    { name: 'Lisa Thompson',  balance: 65,  days: 'Never paid', phone: '646-555-0321' }
  ]
};

/* ================================================================
   SETUP & AUTH
   ================================================================ */

function forceAuth() {
  SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID).getSheetByName('test_auth_ignore');
  SpreadsheetApp.openById(CONFIG.REGISTRATION_ID).getSheetByName('test_auth_ignore');
  SpreadsheetApp.openById(CONFIG.SIGNUPS_SHEET_ID).getSheetByName('test_auth_ignore');
  MailApp.getRemainingDailyQuota();
  Logger.log('โœ… All permissions authorized. You can close this now.');
}

function fullSetup() {
  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);

  // โ”€โ”€ Payments sheet โ”€โ”€
  let paySheet = ss.getSheetByName('Payments');
  if (!paySheet) {
    paySheet = ss.insertSheet('Payments');
    paySheet.appendRow([
      'Payment ID', 'Timestamp', 'Student Name', 'Student Email', 'Student Phone',
      'Package', 'Payment Amount', 'Payment Method', 'Notes',
      'Recorded By', 'Plan Payment #', 'Type'
    ]);
    styleHeader_(paySheet, 12);
    Logger.log('โœ… Created "Payments" sheet.');
  }

  // โ”€โ”€ Balances sheet โ”€โ”€
  let balSheet = ss.getSheetByName('Balances');
  if (!balSheet) {
    balSheet = ss.insertSheet('Balances');
    balSheet.appendRow([
      'Student Name', 'Email', 'Phone', 'Package', 'Total Owed',
      'Total Paid', 'Balance Due', 'Status', 'On Payment Plan',
      'Last Payment Date', 'Days Since Payment', 'Reminder Sent'
    ]);
    styleHeader_(balSheet, 12);
    Logger.log('โœ… Created "Balances" sheet.');
  }

  // โ”€โ”€ Payment Plans sheet โ”€โ”€
  let planSheet = ss.getSheetByName('Payment Plans');
  if (!planSheet) {
    planSheet = ss.insertSheet('Payment Plans');
    planSheet.appendRow([
      'Student Name', 'Email', 'Package', 'Total Owed', 'Number of Installments',
      'Installment Amount', 'Installments Paid', 'Remaining',
      'Next Due Date', 'Status'
    ]);
    styleHeader_(planSheet, 10);
    Logger.log('โœ… Created "Payment Plans" sheet.');
  }

  // โ”€โ”€ Dashboard sheet โ”€โ”€
  let dashSheet = ss.getSheetByName('Dashboard');
  if (!dashSheet) {
    ss.insertSheet('Dashboard');
    Logger.log('โœ… Created "Dashboard" sheet.');
  }

  // โ”€โ”€ Revenue Log sheet โ”€โ”€
  let revSheet = ss.getSheetByName('Revenue Log');
  if (!revSheet) {
    revSheet = ss.insertSheet('Revenue Log');
    revSheet.appendRow(['Month', 'Year', 'Revenue Collected', 'New Students', 'Payments Count']);
    styleHeader_(revSheet, 5);
    Logger.log('โœ… Created "Revenue Log" sheet.');
  }

  // โ”€โ”€ Import students โ”€โ”€
  importStudents_();

  // โ”€โ”€ Triggers (clean old first) โ”€โ”€
  ScriptApp.getProjectTriggers().forEach(t => {
    const fn = t.getHandlerFunction();
    if (['checkOverdue', 'sendPaymentReminders', 'refreshDashboard'].includes(fn)) {
      ScriptApp.deleteTrigger(t);
    }
  });

  // Monday 9 AM โ€” overdue check
  ScriptApp.newTrigger('checkOverdue')
    .timeBased().onWeekDay(ScriptApp.WeekDay.MONDAY).atHour(9).nearMinute(0).create();

  // Thursday 10 AM โ€” friendly reminders (before they go overdue)
  ScriptApp.newTrigger('sendPaymentReminders')
    .timeBased().onWeekDay(ScriptApp.WeekDay.THURSDAY).atHour(10).nearMinute(0).create();

  refreshDashboard();

  Logger.log('โœ… Payment Tracker setup complete.');
  Logger.log('๐Ÿ“Œ Triggers: Overdue check Monday 9 AM, Payment reminders Thursday 10 AM.');
}

function styleHeader_(sheet, numCols) {
  sheet.getRange(1, 1, 1, numCols).setBackground('#1a1a1a').setFontColor('#ff2d2d').setFontWeight('bold').setFontSize(10);
  sheet.setFrozenRows(1);
}

/* ================================================================
   IMPORT STUDENTS
   ================================================================ */

function importStudents_() {
  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  const balSheet = ss.getSheetByName('Balances');
  if (!balSheet) return 0;

  // Build existing index by email + name
  const existingData = balSheet.getLastRow() > 1 ? balSheet.getRange(2, 1, balSheet.getLastRow() - 1, balSheet.getLastColumn()).getValues() : [];
  const existing = new Set();
  for (const row of existingData) {
    existing.add(String(row[0] || '').toLowerCase().trim()); // name
    if (row[1]) existing.add(String(row[1]).toLowerCase().trim()); // email
  }

  let imported = 0;

  // โ”€โ”€ Registration sheet โ”€โ”€
  try {
    const regSS = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
    const regSheet = CONFIG.REGISTRATION_SHEET_TAB
      ? (regSS.getSheetByName(CONFIG.REGISTRATION_SHEET_TAB) || regSS.getSheets()[0])
      : regSS.getSheets()[0];
    if (regSheet && regSheet.getLastRow() > 1) {
      const data = regSheet.getDataRange().getValues();
      const h = data[0].map(c => String(c || '').toLowerCase().trim());
      const nCol = findCol_(h, ['student name', 'full name', 'name']);
      const eCol = findCol_(h, ['email', 'student email', 'email address']);
      const pCol = findCol_(h, ['phone', 'phone number', 'mobile']);
      const pkgCol = findCol_(h, ['package', 'lesson package', 'program']);

      for (let i = 1; i < data.length; i++) {
        const name = sanitize_(String(data[i][nCol] || '').trim());
        if (!name) continue;
        const email = eCol >= 0 ? String(data[i][eCol] || '').trim().toLowerCase() : '';

        // Dedup by email-first, then name
        if (email && existing.has(email)) continue;
        if (existing.has(name.toLowerCase())) continue;

        const phone = pCol >= 0 ? String(data[i][pCol] || '').trim() : '';
        const pkgRaw = pkgCol >= 0 ? String(data[i][pkgCol] || '') : '';
        const { pkg, owed } = resolvePackage_(pkgRaw);

        balSheet.appendRow([name, email, phone, pkg, owed, 0, owed, owed > 0 ? 'Unpaid' : 'N/A', 'No', '', '', '']);
        existing.add(name.toLowerCase());
        if (email) existing.add(email);
        imported++;
      }
    }
  } catch (e) {
    Logger.log('Registration import error: ' + e.message);
  }

  // โ”€โ”€ 5-Hour Signups โ”€โ”€
  try {
    const sigSS = SpreadsheetApp.openById(CONFIG.SIGNUPS_SHEET_ID);
    const sigSheet = CONFIG.SIGNUPS_SHEET_TAB
      ? (sigSS.getSheetByName(CONFIG.SIGNUPS_SHEET_TAB) || sigSS.getSheets()[0])
      : sigSS.getSheets()[0];
    if (sigSheet && sigSheet.getLastRow() > 1) {
      const data = sigSheet.getDataRange().getValues();
      const h = data[0].map(c => String(c || '').toLowerCase().trim());
      const nCol = findCol_(h, ['full name', 'student name', 'name']);
      const eCol = findCol_(h, ['email', 'student email']);
      const pCol = findCol_(h, ['phone', 'phone number', 'mobile']);

      for (let i = 1; i < data.length; i++) {
        const name = sanitize_(String(data[i][nCol] || '').trim());
        if (!name) continue;
        const email = eCol >= 0 ? String(data[i][eCol] || '').trim().toLowerCase() : '';

        if (email && existing.has(email)) continue;
        if (existing.has(name.toLowerCase())) continue;

        const phone = pCol >= 0 ? String(data[i][pCol] || '').trim() : '';
        balSheet.appendRow([name, email, phone, '5-Hour Class', 65, 0, 65, 'Unpaid', 'No', '', '', '']);
        existing.add(name.toLowerCase());
        if (email) existing.add(email);
        imported++;
      }
    }
  } catch (e) {
    Logger.log('Signups import error: ' + e.message);
  }

  Logger.log('โœ… Imported ' + imported + ' student(s).');
  return imported;
}

function resolvePackage_(pkgRaw) {
  const raw = String(pkgRaw || '').toLowerCase().trim();
  if (!raw) return { pkg: 'Unknown', owed: 0 };

  // Sort by longest key first to match "5-Hour Class" before "5 Lessons"
  const keys = Object.keys(CONFIG.PACKAGES).sort((a, b) => b.length - a.length);
  for (const key of keys) {
    if (raw.includes(key.toLowerCase())) return { pkg: key, owed: CONFIG.PACKAGES[key] };
  }

  // Try numeric extraction
  const numMatch = raw.match(/(\d+)/);
  if (numMatch) {
    const num = numMatch[1];
    for (const key of keys) {
      if (key.includes(num)) return { pkg: key, owed: CONFIG.PACKAGES[key] };
    }
  }

  return { pkg: pkgRaw || 'Unknown', owed: 0 };
}

/* ================================================================
   RECORD PAYMENT
   ================================================================ */

function recordPayment(studentName, amount, method, notes, planPaymentNum) {
  if (CONFIG.DEMO_MODE) {
    const d = DEMO.payment;
    Logger.log('๐ŸŽญ DEMO MODE โ€” would record: $' + amount + ' from ' + studentName + ' via ' + method);
    return { success: true, paymentId: d.id, message: '๐ŸŽญ Demo payment recorded.' };
  }

  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  const paySheet = ss.getSheetByName('Payments');
  const balSheet = ss.getSheetByName('Balances');
  if (!paySheet || !balSheet) return { success: false, message: 'Payments or Balances sheet not found.' };

  // Sanitize
  studentName = sanitize_(studentName);
  method = sanitize_(method || 'Cash');
  notes = sanitize_(notes || '');
  amount = parseFloat(String(amount).replace(/[$,]/g, '')) || 0;

  if (amount <= 0) return { success: false, message: 'Invalid payment amount.' };

  // โ”€โ”€ Find student (email-first + fuzzy name) โ”€โ”€
  const balData = balSheet.getDataRange().getValues();
  const balHeaders = balData[0].map(h => String(h).toLowerCase().trim());
  const bNameCol  = findCol_(balHeaders, ['student name', 'name']);
  const bEmailCol = findCol_(balHeaders, ['email']);
  const bPhoneCol = findCol_(balHeaders, ['phone']);
  const bPkgCol   = findCol_(balHeaders, ['package']);
  const bOwedCol  = findCol_(balHeaders, ['total owed']);
  const bPaidCol  = findCol_(balHeaders, ['total paid']);
  const bBalCol   = findCol_(balHeaders, ['balance due']);
  const bStatCol  = findCol_(balHeaders, ['status']);
  const bLastCol  = findCol_(balHeaders, ['last payment date']);
  const bDaysCol  = findCol_(balHeaders, ['days since payment']);

  let studentRow = -1;
  const nameLower = studentName.toLowerCase().trim();

  // Try exact name match first
  for (let i = 1; i < balData.length; i++) {
    if (String(balData[i][bNameCol] || '').toLowerCase().trim() === nameLower) {
      studentRow = i + 1;
      break;
    }
  }

  // Fuzzy name fallback
  if (studentRow === -1) {
    for (let i = 1; i < balData.length; i++) {
      const rowName = String(balData[i][bNameCol] || '').toLowerCase().trim();
      if (levenshtein_(rowName, nameLower) <= 2) {
        studentRow = i + 1;
        break;
      }
    }
  }

  if (studentRow === -1) return { success: false, message: 'Student not found: ' + studentName + '. Import them first.' };

  const row = balData[studentRow - 1];
  const studentEmail = bEmailCol >= 0 ? String(row[bEmailCol] || '') : '';
  const studentPhone = bPhoneCol >= 0 ? String(row[bPhoneCol] || '') : '';
  const pkg = bPkgCol >= 0 ? String(row[bPkgCol] || '') : '';
  const totalOwed = bOwedCol >= 0 ? (parseFloat(row[bOwedCol]) || 0) : 0;
  const prevPaid = bPaidCol >= 0 ? (parseFloat(row[bPaidCol]) || 0) : 0;
  const totalPaid = prevPaid + amount;
  const balanceDue = Math.max(0, totalOwed - totalPaid);

  // Overpayment warning
  if (totalPaid > totalOwed && totalOwed > 0) {
    Logger.log('โš ๏ธ Overpayment: ' + studentName + ' paid $' + totalPaid.toFixed(2) + ' but only owes $' + totalOwed.toFixed(2));
  }

  // Generate short payment ID
  const payId = generatePaymentId_(paySheet);

  try {
    paySheet.appendRow([
      payId, new Date(), studentName, studentEmail, studentPhone,
      pkg, amount, method, notes, 'System', planPaymentNum || '', 'Payment'
    ]);

    // Update Balances
    if (bPaidCol >= 0) balSheet.getRange(studentRow, bPaidCol + 1).setValue(totalPaid);
    if (bBalCol >= 0) balSheet.getRange(studentRow, bBalCol + 1).setValue(balanceDue);

    const newStatus = balanceDue <= 0 ? 'Paid in Full' : 'Partial';
    if (bStatCol >= 0) {
      const statusCell = balSheet.getRange(studentRow, bStatCol + 1);
      statusCell.setValue(newStatus);
      statusCell.setBackground(balanceDue <= 0 ? '#0a2e0a' : '#2e1a0a');
      statusCell.setFontColor(balanceDue <= 0 ? '#22c55e' : '#ff9900');
    }

    if (bLastCol >= 0) balSheet.getRange(studentRow, bLastCol + 1).setValue(new Date());
    if (bDaysCol >= 0) balSheet.getRange(studentRow, bDaysCol + 1).setValue(0);

    updatePaymentPlan_(ss, studentName, amount);
    sendPaymentReceipt_(payId, studentName, studentEmail, amount, method, totalPaid, balanceDue, pkg);
    syncToRegistration_(studentName, studentEmail, totalPaid, balanceDue, method);
    updateRevenue_(ss, amount);

    try { refreshDashboard(); } catch (_) {}

    return {
      success: true,
      paymentId: payId,
      message: 'โœ… Payment recorded: ' + studentName + ' paid $' + amount.toFixed(2) + ' via ' + method + '. Balance: $' + balanceDue.toFixed(2)
    };
  } catch (err) {
    Logger.log('recordPayment error: ' + (err.message || err));
    notifyAdmin_('Payment Error', 'recordPayment failed for ' + studentName + ': ' + String(err.message || err).substring(0, 500));
    return { success: false, message: 'Payment could not be saved: ' + (err.message || String(err)) };
  }
}

/* ================================================================
   REFUND
   ================================================================ */

function processRefund(paymentId, refundAmount, reason) {
  if (CONFIG.DEMO_MODE) {
    Logger.log('๐ŸŽญ DEMO MODE โ€” would refund $' + refundAmount + ' for ' + paymentId);
    return { success: true, message: '๐ŸŽญ Demo refund.' };
  }

  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  const paySheet = ss.getSheetByName('Payments');
  const balSheet = ss.getSheetByName('Balances');
  if (!paySheet || !balSheet) return { success: false, message: 'Sheets not found.' };

  refundAmount = parseFloat(String(refundAmount).replace(/[$,]/g, '')) || 0;
  if (refundAmount <= 0) return { success: false, message: 'Invalid refund amount.' };

  // Find original payment
  const payData = paySheet.getDataRange().getValues();
  const payHeaders = payData[0].map(h => String(h).toLowerCase().trim());
  const pIdCol   = findCol_(payHeaders, ['payment id', 'id']);
  const pNameCol = findCol_(payHeaders, ['student name', 'student']);
  const pEmailCol = findCol_(payHeaders, ['student email', 'email']);
  const pPhoneCol = findCol_(payHeaders, ['student phone', 'phone']);
  const pPkgCol  = findCol_(payHeaders, ['package']);

  let originalRow = null;
  for (let i = 1; i < payData.length; i++) {
    if (String(payData[i][pIdCol] || '').trim() === paymentId) {
      originalRow = payData[i];
      break;
    }
  }

  if (!originalRow) return { success: false, message: 'Payment ' + paymentId + ' not found.' };

  const studentName = String(originalRow[pNameCol] || '');
  const studentEmail = pEmailCol >= 0 ? String(originalRow[pEmailCol] || '') : '';
  const studentPhone = pPhoneCol >= 0 ? String(originalRow[pPhoneCol] || '') : '';
  const pkg = pPkgCol >= 0 ? String(originalRow[pPkgCol] || '') : '';

  // Record refund as negative payment
  const refundId = generatePaymentId_(paySheet);
  paySheet.appendRow([
    refundId, new Date(), studentName, studentEmail, studentPhone,
    pkg, -refundAmount, 'Refund', 'Refund for ' + paymentId + ': ' + sanitize_(reason || ''),
    'System', '', 'Refund'
  ]);

  // Update Balances
  const balData = balSheet.getDataRange().getValues();
  const balHeaders = balData[0].map(h => String(h).toLowerCase().trim());
  const bNameCol = findCol_(balHeaders, ['student name', 'name']);
  const bPaidCol = findCol_(balHeaders, ['total paid']);
  const bBalCol  = findCol_(balHeaders, ['balance due']);
  const bOwedCol = findCol_(balHeaders, ['total owed']);
  const bStatCol = findCol_(balHeaders, ['status']);

  for (let i = 1; i < balData.length; i++) {
    if (String(balData[i][bNameCol] || '').toLowerCase().trim() === studentName.toLowerCase().trim()) {
      const row = i + 1;
      const newPaid = Math.max(0, (parseFloat(balData[i][bPaidCol]) || 0) - refundAmount);
      const owed = parseFloat(balData[i][bOwedCol]) || 0;
      const newBal = Math.max(0, owed - newPaid);

      balSheet.getRange(row, bPaidCol + 1).setValue(newPaid);
      balSheet.getRange(row, bBalCol + 1).setValue(newBal);
      if (bStatCol >= 0) {
        const status = newBal <= 0 ? 'Paid in Full' : (newPaid > 0 ? 'Partial' : 'Unpaid');
        balSheet.getRange(row, bStatCol + 1).setValue(status);
      }
      break;
    }
  }

  // Notify
  sendRefundEmail_(refundId, studentName, studentEmail, refundAmount, reason || '', paymentId);
  Logger.log('โœ… Refund processed: $' + refundAmount.toFixed(2) + ' for ' + studentName + ' (original: ' + paymentId + ')');

  try { refreshDashboard(); } catch (_) {}

  return { success: true, refundId: refundId, message: 'โœ… Refund of $' + refundAmount.toFixed(2) + ' processed for ' + studentName };
}

/* ================================================================
   PAYMENT HISTORY API (for Student Portal)
   ================================================================ */

function getPaymentHistory(email) {
  if (CONFIG.DEMO_MODE) {
    return [
      { id: 'PAY-0042', date: '02/19/2026', amount: 250, method: 'Zelle', balance: 210, type: 'Payment' },
      { id: 'PAY-0038', date: '02/10/2026', amount: 250, method: 'Cash',  balance: 460, type: 'Payment' }
    ];
  }

  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  const paySheet = ss.getSheetByName('Payments');
  if (!paySheet || paySheet.getLastRow() < 2) return [];

  const data = paySheet.getDataRange().getValues();
  const h = data[0].map(c => String(c || '').toLowerCase().trim());
  const idCol    = findCol_(h, ['payment id', 'id']);
  const dateCol  = findCol_(h, ['timestamp', 'date']);
  const emailCol = findCol_(h, ['student email', 'email']);
  const amtCol   = findCol_(h, ['payment amount', 'amount']);
  const methCol  = findCol_(h, ['payment method', 'method']);
  const typeCol  = findCol_(h, ['type']);

  const emailLower = String(email || '').toLowerCase().trim();
  const results = [];

  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    if (emailCol < 0 || String(row[emailCol] || '').toLowerCase().trim() !== emailLower) continue;

    const dateVal = dateCol >= 0 ? row[dateCol] : '';
    let dateStr = '';
    if (dateVal instanceof Date && !isNaN(dateVal.getTime())) {
      dateStr = Utilities.formatDate(dateVal, CONFIG.TIMEZONE, 'MM/dd/yyyy');
    }

    results.push({
      id:     idCol >= 0 ? String(row[idCol] || '') : '',
      date:   dateStr,
      amount: amtCol >= 0 ? parseFloat(row[amtCol]) || 0 : 0,
      method: methCol >= 0 ? String(row[methCol] || '') : '',
      type:   typeCol >= 0 ? String(row[typeCol] || 'Payment') : 'Payment'
    });
  }

  results.sort((a, b) => {
    const da = new Date(a.date), db = new Date(b.date);
    return db - da;
  });

  return results;
}

/* ================================================================
   PAYMENT PLANS
   ================================================================ */

function setupPaymentPlan(studentName, numInstallments) {
  if (CONFIG.DEMO_MODE) {
    Logger.log('๐ŸŽญ DEMO MODE โ€” would create payment plan for ' + studentName);
    return { success: true, message: '๐ŸŽญ Demo plan created.' };
  }

  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  const balSheet = ss.getSheetByName('Balances');
  const planSheet = ss.getSheetByName('Payment Plans');
  if (!balSheet || !planSheet) return { success: false, message: 'Sheets not found.' };

  const balData = balSheet.getDataRange().getValues();
  const balHeaders = balData[0].map(h => String(h).toLowerCase().trim());
  const bNameCol = findCol_(balHeaders, ['student name', 'name']);
  const bEmailCol = findCol_(balHeaders, ['email']);
  const bBalCol  = findCol_(balHeaders, ['balance due']);
  const bPkgCol  = findCol_(balHeaders, ['package']);
  const bPlanCol = findCol_(balHeaders, ['on payment plan']);

  let studentRow = -1;
  let balanceDue = 0, pkg = '', email = '';
  const nameLower = String(studentName).toLowerCase().trim();

  for (let i = 1; i < balData.length; i++) {
    if (String(balData[i][bNameCol] || '').toLowerCase().trim() === nameLower ||
        levenshtein_(String(balData[i][bNameCol] || '').toLowerCase().trim(), nameLower) <= 2) {
      studentRow = i + 1;
      balanceDue = parseFloat(balData[i][bBalCol]) || 0;
      pkg = String(balData[i][bPkgCol] || '');
      email = bEmailCol >= 0 ? String(balData[i][bEmailCol] || '') : '';
      break;
    }
  }

  if (studentRow === -1) return { success: false, message: 'Student not found.' };
  if (balanceDue <= 0) return { success: false, message: 'No balance due.' };

  // Prevent duplicate active plan
  const planData = planSheet.getDataRange().getValues();
  for (let p = 1; p < planData.length; p++) {
    if (String(planData[p][0] || '').toLowerCase().trim() === nameLower &&
        String(planData[p][planData[0].length - 1] || '').toLowerCase() === 'active') {
      return { success: false, message: 'Student already has an active payment plan.' };
    }
  }

  const installmentAmount = Math.ceil(balanceDue / numInstallments * 100) / 100;
  const nextDue = new Date();
  nextDue.setDate(nextDue.getDate() + 14);

  planSheet.appendRow([
    studentName, email, pkg, balanceDue, numInstallments,
    installmentAmount, 0, balanceDue, nextDue, 'Active'
  ]);

  if (bPlanCol >= 0) balSheet.getRange(studentRow, bPlanCol + 1).setValue('Yes');

  Logger.log('โœ… Payment plan: ' + studentName + ' โ€” ' + numInstallments + ' x $' + installmentAmount.toFixed(2));
  return { success: true, message: 'Plan created: ' + numInstallments + ' payments of $' + installmentAmount.toFixed(2) + ' every 2 weeks.' };
}

function updatePaymentPlan_(ss, studentName, paymentAmount) {
  const planSheet = ss.getSheetByName('Payment Plans');
  if (!planSheet || planSheet.getLastRow() < 2) return;

  const data = planSheet.getDataRange().getValues();
  const nameLower = String(studentName).toLowerCase().trim();

  for (let i = 1; i < data.length; i++) {
    if (String(data[i][0] || '').toLowerCase().trim() !== nameLower) continue;
    const statusCol = data[0].length - 1;
    if (String(data[i][statusCol] || '').toLowerCase() !== 'active') continue;

    const row = i + 1;
    const installmentAmount = parseFloat(data[i][5]) || 0;
    const remainingBefore = parseFloat(data[i][7]) || 0;
    const remaining = Math.max(0, remainingBefore - paymentAmount);
    let installmentsPaid = parseInt(data[i][6], 10) || 0;

    if (installmentAmount > 0) {
      installmentsPaid += Math.min(
        Math.floor(paymentAmount / installmentAmount),
        Math.ceil(remainingBefore / installmentAmount)
      );
    } else {
      installmentsPaid += 1;
    }

    planSheet.getRange(row, 7).setValue(installmentsPaid);
    planSheet.getRange(row, 8).setValue(remaining);

    if (remaining > 0) {
      const nextDue = new Date();
      nextDue.setDate(nextDue.getDate() + 14);
      planSheet.getRange(row, 9).setValue(nextDue);
      planSheet.getRange(row, 10).setValue('Active');
    } else {
      planSheet.getRange(row, 9).setValue('');
      planSheet.getRange(row, 10).setValue('Completed');
    }
    break;
  }
}

/* ================================================================
   REVENUE TRACKING
   ================================================================ */

function updateRevenue_(ss, amount) {
  try {
    const revSheet = ss.getSheetByName('Revenue Log');
    if (!revSheet) return;

    const now = new Date();
    const month = Utilities.formatDate(now, CONFIG.TIMEZONE, 'MMMM');
    const year = Utilities.formatDate(now, CONFIG.TIMEZONE, 'yyyy');

    const data = revSheet.getDataRange().getValues();
    for (let i = 1; i < data.length; i++) {
      if (String(data[i][0]) === month && String(data[i][1]) === year) {
        const row = i + 1;
        revSheet.getRange(row, 3).setValue((parseFloat(data[i][2]) || 0) + amount);
        revSheet.getRange(row, 5).setValue((parseInt(data[i][4], 10) || 0) + 1);
        return;
      }
    }

    // New month
    revSheet.appendRow([month, year, amount, 0, 1]);
  } catch (e) {
    Logger.log('Revenue tracking error: ' + e.message);
  }
}

/* ================================================================
   DASHBOARD
   ================================================================ */

function refreshDashboard() {
  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  let dashSheet = ss.getSheetByName('Dashboard');
  if (!dashSheet) dashSheet = ss.insertSheet('Dashboard');
  dashSheet.clear();

  const balSheet = ss.getSheetByName('Balances');
  if (!balSheet || balSheet.getLastRow() < 2) return;

  const balData = balSheet.getDataRange().getValues();
  const h = balData[0].map(c => String(c || '').toLowerCase().trim());
  const bNameCol = findCol_(h, ['student name', 'name']);
  const bOwedCol = findCol_(h, ['total owed']);
  const bPaidCol = findCol_(h, ['total paid']);
  const bBalCol  = findCol_(h, ['balance due']);
  const bStatCol = findCol_(h, ['status']);
  const bPlanCol = findCol_(h, ['on payment plan']);
  const bLastCol = findCol_(h, ['last payment date']);
  const bDaysCol = findCol_(h, ['days since payment']);

  let totalStudents = 0, totalOwed = 0, totalPaid = 0, totalBalance = 0;
  let paidInFull = 0, partial = 0, unpaid = 0, overdue = 0, onPlan = 0;
  const overdueStudents = [];
  const now = new Date();

  for (let i = 1; i < balData.length; i++) {
    const row = balData[i];
    const name = bNameCol >= 0 ? String(row[bNameCol] || '').trim() : '';
    if (!name) continue;

    totalStudents++;
    const owed = bOwedCol >= 0 ? (parseFloat(row[bOwedCol]) || 0) : 0;
    const paid = bPaidCol >= 0 ? (parseFloat(row[bPaidCol]) || 0) : 0;
    const balance = bBalCol >= 0 ? (parseFloat(row[bBalCol]) || 0) : 0;
    const status = bStatCol >= 0 ? String(row[bStatCol] || '').toLowerCase() : '';
    const plan = bPlanCol >= 0 ? String(row[bPlanCol] || '').toLowerCase() : '';

    totalOwed += owed;
    totalPaid += paid;
    totalBalance += balance;

    if (status === 'paid in full') paidInFull++;
    else if (status === 'partial') partial++;
    else unpaid++;

    if (plan === 'yes') onPlan++;

    if (balance > 0) {
      const lastPay = bLastCol >= 0 ? row[bLastCol] : null;
      let daysSince = 999;
      if (lastPay instanceof Date && !isNaN(lastPay.getTime())) {
        daysSince = Math.floor((now - lastPay) / 86400000);
        if (bDaysCol >= 0) balSheet.getRange(i + 1, bDaysCol + 1).setValue(daysSince);
      }
      if (daysSince > CONFIG.OVERDUE_DAYS || (!(lastPay instanceof Date) && balance > 0 && owed > 0)) {
        overdue++;
        overdueStudents.push({
          name: name,
          balance: balance,
          days: daysSince === 999 ? 'Never paid' : daysSince + ' days'
        });
      }
    }
  }

  const tz = CONFIG.TIMEZONE;
  const rows = [
    ['', ''],
    ['  ๐Ÿ“Š  PAYMENT DASHBOARD', ''],
    ['', ''],
    ['  Total Students', totalStudents],
    ['  Total Revenue Owed', '$' + totalOwed.toFixed(2)],
    ['  Total Collected', '$' + totalPaid.toFixed(2)],
    ['  Outstanding Balance', '$' + totalBalance.toFixed(2)],
    ['  Collection Rate', totalOwed > 0 ? (totalPaid / totalOwed * 100).toFixed(1) + '%' : '0%'],
    ['', ''],
    ['  ๐Ÿ“‹  STATUS BREAKDOWN', ''],
    ['', ''],
    ['  โœ… Paid in Full', paidInFull],
    ['  ๐ŸŸก Partial Payment', partial],
    ['  ๐Ÿ”ด Unpaid', unpaid],
    ['  โš ๏ธ Overdue (>' + CONFIG.OVERDUE_DAYS + ' days)', overdue],
    ['  ๐Ÿ“… On Payment Plan', onPlan],
    ['', ''],
    ['  โš ๏ธ  OVERDUE STUDENTS', ''],
    ['', '']
  ];

  for (const o of overdueStudents) {
    rows.push(['  ' + o.name, '$' + o.balance.toFixed(2) + ' (' + o.days + ')']);
  }
  if (overdueStudents.length === 0) rows.push(['  No overdue students ๐ŸŽ‰', '']);

  rows.push(['', '']);
  rows.push(['  Last updated: ' + Utilities.formatDate(now, tz, 'M/d/yyyy h:mm a'), '']);

  dashSheet.getRange(1, 1, rows.length, 2).setValues(rows);

  // Styling
  dashSheet.getRange(1, 1, rows.length, 2)
    .setBackground('#0a0a0a').setFontColor('#cccccc').setFontFamily('Arial').setFontSize(12);

  for (const r of [2, 10, 18]) {
    dashSheet.getRange(r, 1, 1, 2).setFontColor('#ff2d2d').setFontWeight('bold').setFontSize(14);
  }
  dashSheet.getRange(4, 2, 5, 1).setFontColor('#ffffff').setFontWeight('bold').setFontSize(13);
  dashSheet.getRange(12, 2).setFontColor('#22c55e');
  dashSheet.getRange(13, 2).setFontColor('#ff9900');
  dashSheet.getRange(14, 2).setFontColor('#ff4444');
  dashSheet.getRange(15, 2).setFontColor('#ff2d2d').setFontWeight('bold');
  dashSheet.setColumnWidth(1, 300);
  dashSheet.setColumnWidth(2, 250);

  Logger.log('โœ… Dashboard refreshed.');
}

/* ================================================================
   OVERDUE CHECK + STUDENT REMINDERS
   ================================================================ */

function checkOverdue() {
  try {
    if (CONFIG.DEMO_MODE) {
      Logger.log('๐ŸŽญ DEMO MODE โ€” would flag ' + DEMO.overdueStudents.length + ' overdue students.');
      return;
    }
    runOverdueCheck_();
  } catch (e) {
    Logger.log('checkOverdue error: ' + (e.message || e));
    notifyAdmin_('Payment Tracker โ€” Overdue Check Failed', String(e.message || e).substring(0, 500));
    throw e;
  }
}

function runOverdueCheck_() {
  const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  const balSheet = ss.getSheetByName('Balances');
  if (!balSheet || balSheet.getLastRow() < 2) return;

  const data = balSheet.getDataRange().getValues();
  const h = data[0].map(c => String(c || '').toLowerCase().trim());
  const bNameCol  = findCol_(h, ['student name', 'name']);
  const bEmailCol = findCol_(h, ['email']);
  const bPhoneCol = findCol_(h, ['phone']);
  const bPkgCol   = findCol_(h, ['package']);
  const bBalCol   = findCol_(h, ['balance due']);
  const bStatCol  = findCol_(h, ['status']);
  const bLastCol  = findCol_(h, ['last payment date']);
  const bDaysCol  = findCol_(h, ['days since payment']);

  const now = new Date();
  const overdueStudents = [];

  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    const name = String(row[bNameCol] || '').trim();
    if (!name) continue;
    const balance = parseFloat(row[bBalCol]) || 0;
    if (balance <= 0) continue;

    const lastPay = bLastCol >= 0 ? row[bLastCol] : null;
    let daysSince = 999;
    if (lastPay instanceof Date && !isNaN(lastPay.getTime())) {
      daysSince = Math.floor((now - lastPay) / 86400000);
      if (bDaysCol >= 0) balSheet.getRange(i + 1, bDaysCol + 1).setValue(daysSince);
    }

    if (daysSince > CONFIG.OVERDUE_DAYS || (!(lastPay instanceof Date) && balance > 0)) {
      if (bStatCol >= 0) {
        balSheet.getRange(i + 1, bStatCol + 1).setValue('OVERDUE')
          .setBackground('#2e0a0a').setFontColor('#ff2d2d');
      }
      overdueStudents.push({
        name: name,
        email: bEmailCol >= 0 ? String(row[bEmailCol] || '') : '',
        phone: bPhoneCol >= 0 ? String(row[bPhoneCol] || '') : '',
        pkg: bPkgCol >= 0 ? String(row[bPkgCol] || '') : '',
        balance: balance,
        days: daysSince === 999 ? 'Never paid' : daysSince + ' days'
      });
    }
  }

  if (overdueStudents.length > 0) {
    sendOverdueAdminAlert_(overdueStudents);
    // Also email each overdue student
    for (const s of overdueStudents) {
      if (s.email && s.email.includes('@')) {
        sendOverdueStudentEmail_(s.name, s.email, s.balance, s.pkg);
      }
    }
  }

  refreshDashboard();
  Logger.log('Overdue check: ' + overdueStudents.length + ' student(s) overdue.');
}

/** Friendly payment reminders โ€” sent before overdue threshold */
function sendPaymentReminders() {
  try {
    if (CONFIG.DEMO_MODE) {
      Logger.log('๐ŸŽญ DEMO MODE โ€” would send payment reminders.');
      return;
    }

    const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
    const balSheet = ss.getSheetByName('Balances');
    if (!balSheet || balSheet.getLastRow() < 2) return;

    const data = balSheet.getDataRange().getValues();
    const h = data[0].map(c => String(c || '').toLowerCase().trim());
    const bNameCol    = findCol_(h, ['student name', 'name']);
    const bEmailCol   = findCol_(h, ['email']);
    const bBalCol     = findCol_(h, ['balance due']);
    const bPkgCol     = findCol_(h, ['package']);
    const bLastCol    = findCol_(h, ['last payment date']);
    const bDaysCol    = findCol_(h, ['days since payment']);
    const bRemCol     = findCol_(h, ['reminder sent']);
    const bOwedCol    = findCol_(h, ['total owed']);

    const now = new Date();
    let sent = 0;

    for (let i = 1; i < data.length; i++) {
      const row = data[i];
      const name = String(row[bNameCol] || '').trim();
      const email = bEmailCol >= 0 ? String(row[bEmailCol] || '').trim() : '';
      const balance = parseFloat(row[bBalCol]) || 0;
      const owed = bOwedCol >= 0 ? (parseFloat(row[bOwedCol]) || 0) : 0;

      if (!name || !email || !email.includes('@') || balance <= 0 || owed <= 0) continue;

      // Already reminded this week?
      if (bRemCol >= 0) {
        const lastReminder = row[bRemCol];
        if (lastReminder instanceof Date && (now - lastReminder) < 7 * 86400000) continue;
      }

      const lastPay = bLastCol >= 0 ? row[bLastCol] : null;
      let daysSince = 999;
      if (lastPay instanceof Date) daysSince = Math.floor((now - lastPay) / 86400000);

      // Send reminder between REMINDER_DAYS and OVERDUE_DAYS (the "friendly zone")
      if (daysSince >= CONFIG.REMINDER_DAYS && daysSince <= CONFIG.OVERDUE_DAYS) {
        const pkg = bPkgCol >= 0 ? String(row[bPkgCol] || '') : '';
        sendFriendlyReminder_(name, email, balance, pkg);
        if (bRemCol >= 0) balSheet.getRange(i + 1, bRemCol + 1).setValue(now);
        sent++;
      }
    }

    Logger.log('Payment reminders sent: ' + sent);
  } catch (e) {
    Logger.log('sendPaymentReminders error: ' + (e.message || e));
    notifyAdmin_('Payment Reminders Failed', String(e.message || e).substring(0, 500));
  }
}

/* ================================================================
   FORM HANDLER
   ================================================================ */

function onPaymentFormSubmit(e) {
  try {
    if (CONFIG.DEMO_MODE) {
      Logger.log('๐ŸŽญ DEMO MODE โ€” form submit ignored.');
      return;
    }

    if (!e || !e.namedValues) {
      Logger.log('onPaymentFormSubmit: no event data.');
      return;
    }

    const val = (keys) => {
      for (const k of keys) {
        const v = e.namedValues[k];
        if (v && v[0]) return v[0].trim();
      }
      return '';
    };

    const studentName = sanitize_(val(['Student Name', 'student name']));
    const amount = parseFloat(String(val(['Payment Amount', 'Amount'])).replace(/[$,]/g, '')) || 0;
    const method = sanitize_(val(['Payment Method', 'Method'])) || 'Cash';
    const notes = sanitize_(val(['Notes', 'notes']));

    const result = recordPayment(studentName, amount, method, notes);

    if (!result.success) {
      notifyAdmin_('โš ๏ธ Payment Recording Failed โ€” ' + studentName,
        'Failed: ' + result.message + '\nStudent: ' + studentName + ', Amount: $' + amount + ', Method: ' + method);
    }

    Logger.log(result.message);
  } catch (err) {
    Logger.log('onPaymentFormSubmit error: ' + (err.message || err));
    notifyAdmin_('Payment Form Error', String(err.message || err).substring(0, 500));
  }
}

/* ================================================================
   PAYMENT ID GENERATOR
   ================================================================ */

function generatePaymentId_(paySheet) {
  let maxNum = 0;
  if (paySheet && paySheet.getLastRow() > 1) {
    const ids = paySheet.getRange(2, 1, paySheet.getLastRow() - 1, 1).getValues();
    for (const row of ids) {
      const match = String(row[0] || '').match(/PAY-(\d+)/);
      if (match) {
        const num = parseInt(match[1], 10);
        if (num > maxNum) maxNum = num;
      }
    }
  }
  return 'PAY-' + String(maxNum + 1).padStart(4, '0');
}

/* ================================================================
   REGISTRATION SYNC
   ================================================================ */

function syncToRegistration_(studentName, studentEmail, totalPaid, balanceDue, method) {
  try {
    const regSS = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
    const regSheet = CONFIG.REGISTRATION_SHEET_TAB
      ? (regSS.getSheetByName(CONFIG.REGISTRATION_SHEET_TAB) || regSS.getSheets()[0])
      : regSS.getSheets()[0];
    if (!regSheet || regSheet.getLastRow() < 2) return;

    const data = regSheet.getDataRange().getValues();
    const h = data[0].map(c => String(c || '').toLowerCase().trim());
    const nameCol   = findCol_(h, ['full name', 'student name', 'name']);
    const emailCol  = findCol_(h, ['email', 'student email']);
    const statusCol = findCol_(h, ['payment status']);
    const amountCol = findCol_(h, ['amount paid']);
    const notesCol  = findCol_(h, ['notes']);

    if (nameCol < 0 || statusCol < 0) return;

    const nameLower = String(studentName).toLowerCase().trim();
    const emailLower = String(studentEmail).toLowerCase().trim();

    // Email-first, then name match
    let matchedRow = -1;
    for (let i = 1; i < data.length; i++) {
      if (emailLower && emailCol >= 0 && String(data[i][emailCol] || '').toLowerCase().trim() === emailLower) {
        matchedRow = i + 1;
        break;
      }
    }
    if (matchedRow === -1) {
      for (let i = 1; i < data.length; i++) {
        if (String(data[i][nameCol] || '').toLowerCase().trim() === nameLower) {
          matchedRow = i + 1;
          break;
        }
      }
    }

    if (matchedRow === -1) return;

    const status = balanceDue <= 0 ? 'Paid in Full' : 'Partial โ€” $' + balanceDue.toFixed(0) + ' remaining';
    regSheet.getRange(matchedRow, statusCol + 1).setValue(status);

    if (amountCol >= 0) {
      regSheet.getRange(matchedRow, amountCol + 1).setValue(totalPaid);
    }

    if (notesCol >= 0) {
      const existing = String(data[matchedRow - 1][notesCol] || '').trim();
      const prevPaid = amountCol >= 0 ? (parseFloat(data[matchedRow - 1][amountCol]) || 0) : 0;
      const payNote = Utilities.formatDate(new Date(), CONFIG.TIMEZONE, 'M/d/yy') + ': $' + (totalPaid - prevPaid).toFixed(0) + ' via ' + method;
      regSheet.getRange(matchedRow, notesCol + 1).setValue(existing ? existing + ' | ' + payNote : payNote);
    }

    Logger.log('Registration synced for ' + studentName);
  } catch (e) {
    Logger.log('Registration sync error: ' + e.message);
  }
}

/* ================================================================
   EMAIL TEMPLATES (Mission Control theme)
   ================================================================ */

function sendPaymentReceipt_(payId, studentName, studentEmail, amount, method, totalPaid, balanceDue, pkg) {
  const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe&body=Please%20remove%20' + encodeURIComponent(studentEmail) + '%20from%20payment%20emails.';

  const html = emailWrap_(
    iconHeader_('๐Ÿ’ต', '#22c55e', 'Payment Received'),
    '<table width="100%" cellpadding="0" cellspacing="0" border="0">'
    + detailRow_('Receipt #', esc_(payId))
    + detailRow_('Student', esc_(studentName))
    + detailRow_('Package', esc_(pkg))
    + detailRow_('Amount Paid', '$' + amount.toFixed(2), '#22c55e')
    + detailRow_('Method', esc_(method))
    + detailRow_('Total Paid', '$' + totalPaid.toFixed(2))
    + detailRow_('Balance Due', '$' + balanceDue.toFixed(2), balanceDue > 0 ? '#ff4444' : '#22c55e')
    + '</table>',
    unsub
  );

  // Admin
  MailApp.sendEmail({
    to: CONFIG.ADMIN_EMAIL,
    subject: '๐Ÿ’ต Payment: ' + studentName + ' โ€” $' + amount.toFixed(2) + ' | Balance: $' + balanceDue.toFixed(2),
    body: 'Payment: ' + studentName + ' paid $' + amount.toFixed(2) + ' via ' + method + '. Balance: $' + balanceDue.toFixed(2),
    htmlBody: html, name: CONFIG.SCHOOL_NAME
  });

  // Student
  if (studentEmail && studentEmail.includes('@')) {
    MailApp.sendEmail({
      to: studentEmail,
      subject: 'Payment Receipt โ€” $' + amount.toFixed(2) + ' received โ€” ' + CONFIG.SCHOOL_NAME,
      body: 'Hi ' + studentName + '! Payment of $' + amount.toFixed(2) + ' received. Balance: $' + balanceDue.toFixed(2),
      htmlBody: html, name: CONFIG.SCHOOL_NAME
    });
  }
}

function sendRefundEmail_(refundId, studentName, studentEmail, amount, reason, originalPayId) {
  const html = emailWrap_(
    iconHeader_('โ†ฉ๏ธ', '#f59e0b', 'Refund Processed'),
    '<table width="100%" cellpadding="0" cellspacing="0" border="0">'
    + detailRow_('Refund ID', esc_(refundId))
    + detailRow_('Original Payment', esc_(originalPayId))
    + detailRow_('Student', esc_(studentName))
    + detailRow_('Refund Amount', '$' + amount.toFixed(2), '#f59e0b')
    + (reason ? detailRow_('Reason', esc_(reason)) : '')
    + '</table>', ''
  );

  MailApp.sendEmail({
    to: CONFIG.ADMIN_EMAIL,
    subject: 'โ†ฉ๏ธ Refund: $' + amount.toFixed(2) + ' โ€” ' + studentName,
    body: 'Refund of $' + amount.toFixed(2) + ' for ' + studentName + ' (original: ' + originalPayId + ')',
    htmlBody: html, name: CONFIG.SCHOOL_NAME
  });

  if (studentEmail && studentEmail.includes('@')) {
    const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe&body=Please%20remove%20me%20from%20payment%20emails.';
    MailApp.sendEmail({
      to: studentEmail,
      subject: 'Refund Processed โ€” $' + amount.toFixed(2) + ' โ€” ' + CONFIG.SCHOOL_NAME,
      body: 'A refund of $' + amount.toFixed(2) + ' has been processed. Reason: ' + (reason || 'N/A'),
      htmlBody: emailWrap_(iconHeader_('โ†ฉ๏ธ', '#f59e0b', 'Refund Processed'),
        '<p style="font-size:14px;color:rgba(255,255,255,0.6);text-align:center;">A refund of <strong style="color:#f59e0b;">$' + amount.toFixed(2) + '</strong> has been processed to your account.</p>'
        + (reason ? '<p style="font-size:13px;color:rgba(255,255,255,0.4);text-align:center;">Reason: ' + esc_(reason) + '</p>' : ''), unsub),
      name: CONFIG.SCHOOL_NAME
    });
  }
}

function sendOverdueAdminAlert_(students) {
  let totalOverdue = 0;
  let rows = '';
  for (const s of students) {
    totalOverdue += s.balance;
    rows += '<tr><td style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.06);color:#ccc;font-size:13px;">' + esc_(s.name) + '</td>'
      + '<td style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.06);color:#ff4444;font-size:13px;font-weight:600;">$' + s.balance.toFixed(2) + '</td>'
      + '<td style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.06);color:#888;">' + esc_(s.days) + '</td>'
      + '<td style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.06);color:#666;">' + esc_(s.email || 'โ€”') + '</td>'
      + '<td style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.06);color:#666;">' + esc_(s.phone || 'โ€”') + '</td></tr>';
  }

  const html = emailWrap_(
    iconHeader_('โš ๏ธ', '#ff2d2d', 'Overdue Payments Alert'),
    '<div style="background:rgba(255,45,45,0.08);border:1px solid rgba(255,45,45,0.15);border-radius:12px;padding:16px;text-align:center;margin-bottom:20px;">'
    + '<span style="font-size:12px;color:#888;">Total Overdue</span><br>'
    + '<span style="font-size:28px;font-weight:700;color:#ff2d2d;">$' + totalOverdue.toFixed(2) + '</span>'
    + '<span style="display:block;font-size:12px;color:#888;margin-top:4px;">' + students.length + ' student' + (students.length !== 1 ? 's' : '') + '</span></div>'
    + '<table width="100%" style="border:1px solid rgba(255,255,255,0.06);border-radius:10px;overflow:hidden;">'
    + '<tr style="background:rgba(255,255,255,0.03);"><th style="padding:10px;text-align:left;font-size:11px;color:#666;">Student</th><th style="padding:10px;text-align:left;font-size:11px;color:#666;">Balance</th><th style="padding:10px;text-align:left;font-size:11px;color:#666;">Overdue</th><th style="padding:10px;text-align:left;font-size:11px;color:#666;">Email</th><th style="padding:10px;text-align:left;font-size:11px;color:#666;">Phone</th></tr>'
    + rows + '</table>', ''
  );

  MailApp.sendEmail({
    to: CONFIG.ADMIN_EMAIL,
    subject: 'โš ๏ธ ' + students.length + ' Overdue โ€” $' + totalOverdue.toFixed(2) + ' outstanding',
    body: students.length + ' students overdue. Total: $' + totalOverdue.toFixed(2),
    htmlBody: html, name: CONFIG.SCHOOL_NAME
  });
}

function sendOverdueStudentEmail_(name, email, balance, pkg) {
  const firstName = (name || '').split(' ')[0] || name;
  const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe&body=Please%20remove%20' + encodeURIComponent(email) + '%20from%20payment%20emails.';

  const html = emailWrap_(
    iconHeader_('๐Ÿ“‹', '#ff2d2d', 'Payment Overdue'),
    '<p style="font-size:14px;color:rgba(255,255,255,0.6);line-height:1.7;text-align:center;">'
    + 'Hi <strong style="color:#fff;">' + esc_(firstName) + '</strong>, our records show an outstanding balance on your account.</p>'
    + '<div style="background:rgba(255,45,45,0.08);border:1px solid rgba(255,45,45,0.2);border-radius:12px;padding:16px;text-align:center;margin:16px 0;">'
    + '<div style="font-size:10px;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:1px;">Balance Due</div>'
    + '<div style="font-size:28px;font-weight:800;color:#ff2d2d;margin-top:4px;">$' + balance.toFixed(2) + '</div>'
    + '<div style="font-size:12px;color:rgba(255,255,255,0.4);margin-top:4px;">' + esc_(pkg) + '</div></div>'
    + '<p style="font-size:13px;color:rgba(255,255,255,0.5);text-align:center;">We accept Cash and Zelle. Contact us at ' + esc_(CONFIG.SCHOOL_PHONE) + ' if you need to set up a payment plan.</p>',
    unsub
  );

  try {
    MailApp.sendEmail({
      to: email,
      subject: 'Payment Reminder โ€” $' + balance.toFixed(2) + ' balance due โ€” ' + CONFIG.SCHOOL_NAME,
      body: 'Hi ' + firstName + ', you have a balance of $' + balance.toFixed(2) + ' for ' + pkg + '. Please contact us at ' + CONFIG.SCHOOL_PHONE,
      htmlBody: html, name: CONFIG.SCHOOL_NAME
    });
  } catch (e) {
    Logger.log('Overdue email failed for ' + name + ': ' + e.message);
  }
}

function sendFriendlyReminder_(name, email, balance, pkg) {
  const firstName = (name || '').split(' ')[0] || name;
  const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe&body=Please%20remove%20' + encodeURIComponent(email) + '%20from%20payment%20emails.';

  const html = emailWrap_(
    iconHeader_('๐Ÿ’ก', '#f59e0b', 'Friendly Reminder'),
    '<p style="font-size:14px;color:rgba(255,255,255,0.6);line-height:1.7;text-align:center;">'
    + 'Hi <strong style="color:#fff;">' + esc_(firstName) + '</strong>! Just a quick reminder about your upcoming balance.</p>'
    + '<div style="background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.2);border-radius:12px;padding:16px;text-align:center;margin:16px 0;">'
    + '<div style="font-size:10px;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:1px;">Balance Due</div>'
    + '<div style="font-size:28px;font-weight:800;color:#f59e0b;margin-top:4px;">$' + balance.toFixed(2) + '</div></div>'
    + '<p style="font-size:13px;color:rgba(255,255,255,0.5);text-align:center;">No rush โ€” just wanted to keep you in the loop! We accept Cash and Zelle. Questions? Call ' + esc_(CONFIG.SCHOOL_PHONE) + '</p>',
    unsub
  );

  try {
    MailApp.sendEmail({
      to: email,
      subject: 'Friendly Reminder โ€” $' + balance.toFixed(2) + ' balance โ€” ' + CONFIG.SCHOOL_NAME,
      body: 'Hi ' + firstName + '! Quick reminder: you have a balance of $' + balance.toFixed(2) + '. No rush! Call ' + CONFIG.SCHOOL_PHONE + ' with questions.',
      htmlBody: html, name: CONFIG.SCHOOL_NAME
    });
  } catch (e) {
    Logger.log('Friendly reminder failed for ' + name + ': ' + e.message);
  }
}

/* ================================================================
   EMAIL BUILDING BLOCKS
   ================================================================ */

function emailWrap_(header, body, unsubLink) {
  const sch = esc_(CONFIG.SCHOOL_NAME);
  const tag = esc_(CONFIG.SCHOOL_TAGLINE);
  return '<!DOCTYPE html><html><head><meta charset="utf-8"></head>'
    + '<body style="margin:0;padding:0;background:#000;font-family:Arial,Helvetica,sans-serif;">'
    + '<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#000;"><tr><td align="center" style="padding:20px;">'
    + '<table width="560" cellpadding="0" cellspacing="0" border="0" style="background:#0d0d0d;border-radius:16px;border:1px solid rgba(255,255,255,0.06);overflow:hidden;">'
    + '<tr><td style="padding:24px 30px;border-bottom:1px solid rgba(255,255,255,0.06);">' + header + '</td></tr>'
    + '<tr><td style="padding:24px 30px;">' + body + '</td></tr>'
    + '<tr><td style="padding:16px 30px;border-top:1px solid rgba(255,255,255,0.06);text-align:center;">'
    + '<p style="font-size:11px;color:rgba(255,255,255,0.2);margin:0;">' + sch + ' โ€” ' + tag + '</p>'
    + (unsubLink ? '<p style="font-size:10px;margin:6px 0 0;"><a href="' + unsubLink + '" style="color:rgba(255,255,255,0.15);text-decoration:underline;">Unsubscribe</a></p>' : '')
    + '</td></tr></table></td></tr></table></body></html>';
}

function iconHeader_(emoji, color, title) {
  return '<div style="display:inline-block;width:40px;height:40px;background:' + color + ';border-radius:12px;line-height:40px;font-size:20px;text-align:center;">' + emoji + '</div>'
    + '<span style="color:#fff;font-size:18px;font-weight:700;margin-left:12px;">' + esc_(title) + '</span>';
}

function detailRow_(label, value, color) {
  return '<tr><td style="padding:8px 0;color:#666;font-size:13px;width:130px;">' + label + '</td>'
    + '<td style="padding:8px 0;color:' + (color || '#fff') + ';font-size:13px;font-weight:600;">' + value + '</td></tr>';
}

/* ================================================================
   SHARED HELPERS
   ================================================================ */

function findCol_(headers, candidates) {
  for (let i = 0; i < headers.length; i++) {
    const h = String(headers[i] || '').toLowerCase().trim();
    for (const kw of candidates) {
      if (kw && h.includes(String(kw).toLowerCase().trim())) return i;
    }
  }
  return -1;
}

function esc_(str) {
  if (str == null) return '';
  return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function sanitize_(str) {
  return String(str || '').replace(/[<>{}()\[\]\\\/]/g, '').substring(0, 200).trim();
}

function levenshtein_(a, b) {
  const m = a.length, n = b.length;
  const dp = Array.from({ length: m + 1 }, (_, i) => i);
  for (let j = 1; j <= n; j++) {
    let prev = dp[0]; dp[0] = j;
    for (let i = 1; i <= m; i++) {
      const temp = dp[i];
      dp[i] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[i], dp[i - 1]);
      prev = temp;
    }
  }
  return dp[m];
}

function notifyAdmin_(subject, body) {
  if (!CONFIG.ADMIN_EMAIL) return;
  try { MailApp.sendEmail(CONFIG.ADMIN_EMAIL, subject, body, { name: CONFIG.SCHOOL_NAME }); } catch (_) {}
}

/* ================================================================
   MANUAL TOOLS
   ================================================================ */

function testDemoPayment() {
  const d = DEMO.payment;
  Logger.log('๐ŸŽญ Demo payment:');
  Logger.log('  ' + d.student + ' paid $' + d.amount + ' via ' + d.method);
  Logger.log('  Total paid: $' + d.totalPaid + ' | Balance: $' + d.balance);
  Logger.log('  Payment ID: ' + d.id);
}

function manualImport() {
  if (CONFIG.DEMO_MODE) { Logger.log('๐ŸŽญ DEMO MODE โ€” import skipped.'); return; }
  const count = importStudents_();
  Logger.log('Imported ' + count + ' student(s).');
}

function testPayment() {
  const result = recordPayment('Marcus Johnson', 100, 'Cash', 'Test payment');
  Logger.log(JSON.stringify(result));
}
๐ŸŽ Referral Rewards 42KB ยท 1,033 linesGSโ–ถ
/**
 * Referral Rewards System โ€” Flavors Driving School
 * 
 * Turns every happy student into a salesperson. When a student completes
 * their lesson package, they receive a unique referral code. When a new
 * student signs up using that code, BOTH get a reward ($25 credit).
 * 
 * FEATURES:
 *   - Demo mode for presentations (no real emails, fake data)
 *   - Email-first + fuzzy name matching for completion detection
 *   - Referral code format validation
 *   - Unsubscribe links on all student emails
 *   - Leaderboard โ€” top referrers summary
 *   - Self-referral prevention
 *   - Duplicate processing guard
 *   - Configurable via Settings sheet
 * 
 * SHEETS (in this spreadsheet):
 *   1. "Referral Codes"  โ€” Every student's unique code, status, usage count
 *   2. "Referral Log"    โ€” Complete history of who referred who
 *   3. "Settings"        โ€” Reward amounts, limits, and customization
 * 
 * CONNECTED SHEETS:
 *   - Student Registration: 1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY
 *   - Instructor Schedule Board: 1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE
 *   - Payment Tracker: 1XWNU7PygQX9jV6mFUUrgHYU_vrhS7NP1OHwNUNWRoOk
 * 
 * TRIGGERS:
 *   - Daily at 10:30 AM: Check for newly completed students โ†’ send referral codes
 *   - Every 2 hours: Check if new registration has a referral code โ†’ process it
 */

// ============ CONFIGURATION ============
const REFERRAL_CONFIG = {
  // Connected sheets
  REGISTRATION_ID: '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',
  SCHEDULE_ID: '1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE',
  PAYMENT_ID: '1XWNU7PygQX9jV6mFUUrgHYU_vrhS7NP1OHwNUNWRoOk',
  
  ADMIN_EMAIL: '[email protected]',
  SCHOOL_NAME: 'Flavors Driving School',
  
  // DEMO MODE โ€” set to true for presentations
  DEMO_MODE: true,
  
  // Reward settings (also editable in Settings sheet)
  REFERRER_REWARD: 25,
  REFEREE_REWARD: 25,
  MAX_REFERRALS: 10,
  CODE_EXPIRY_DAYS: 365,
  
  // Referral code pattern: FIRSTNAME-XXXX
  CODE_PATTERN: /^[A-Z]+-[A-Z2-9]{4}$/,
  
  PACKAGES: {
    '3 Lessons': 3,
    '5 Lessons': 5,
    '10 Lessons': 10,
    '15 Lessons': 15,
    '25 Lessons': 25,
    '5-Hour Class': 1
  }
};

// ============ GET SPREADSHEET HELPER ============
function getSpreadsheet() {
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  if (ss) return ss;
  
  const id = PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID');
  if (id) return SpreadsheetApp.openById(id);
  
  ss = SpreadsheetApp.create('Referral Rewards โ€” Flavors Driving School');
  PropertiesService.getScriptProperties().setProperty('SPREADSHEET_ID', ss.getId());
  Logger.log('Created new spreadsheet: ' + ss.getUrl());
  return ss;
}

// ============ FORCE AUTH โ€” hits ALL connected sheets ============
function forceAuth() {
  const reg = SpreadsheetApp.openById(REFERRAL_CONFIG.REGISTRATION_ID);
  Logger.log('Registration: ' + reg.getName());
  const sched = SpreadsheetApp.openById(REFERRAL_CONFIG.SCHEDULE_ID);
  Logger.log('Schedule: ' + sched.getName());
  const pay = SpreadsheetApp.openById(REFERRAL_CONFIG.PAYMENT_ID);
  Logger.log('Payments: ' + pay.getName());
  MailApp.getRemainingDailyQuota();
  Logger.log('โœ… Auth complete โ€” all sheets + email accessible.');
}


// ================================================================
//  SETUP
// ================================================================

function fullSetup() {
  const ss = getSpreadsheet();
  
  // --- Referral Codes Sheet ---
  let codesSheet = ss.getSheetByName('Referral Codes');
  if (!codesSheet) codesSheet = ss.insertSheet('Referral Codes');
  codesSheet.clear();
  
  const codesHeaders = [
    'Student Name', 'Email', 'Referral Code', 'Date Generated',
    'Expires', 'Times Used', 'Total Earned', 'Status'
  ];
  codesSheet.appendRow(codesHeaders);
  formatHeader(codesSheet, codesHeaders.length);
  codesSheet.setColumnWidths(1, 1, 180);
  codesSheet.setColumnWidth(2, 220);
  codesSheet.setColumnWidth(3, 140);
  codesSheet.setColumnWidth(4, 140);
  codesSheet.setColumnWidth(5, 140);
  codesSheet.setColumnWidth(6, 100);
  codesSheet.setColumnWidth(7, 110);
  codesSheet.setColumnWidth(8, 100);
  
  // --- Referral Log Sheet ---
  let logSheet = ss.getSheetByName('Referral Log');
  if (!logSheet) logSheet = ss.insertSheet('Referral Log');
  logSheet.clear();
  
  const logHeaders = [
    'Date', 'Referral Code Used', 'Referrer Name', 'Referrer Email',
    'New Student Name', 'New Student Email', 'New Student Package',
    'Referrer Reward', 'New Student Reward', 'Status', 'Processing ID'
  ];
  logSheet.appendRow(logHeaders);
  formatHeader(logSheet, logHeaders.length);
  
  // --- Settings Sheet ---
  let settingsSheet = ss.getSheetByName('Settings');
  if (!settingsSheet) settingsSheet = ss.insertSheet('Settings');
  settingsSheet.clear();
  
  const settingsData = [
    ['Setting', 'Value', 'Description'],
    ['Demo Mode', REFERRAL_CONFIG.DEMO_MODE ? 'Yes' : 'No', 'Set to Yes for demos โ€” uses fake data, no real emails'],
    ['Referrer Reward ($)', REFERRAL_CONFIG.REFERRER_REWARD, 'Credit given to the person who referred'],
    ['New Student Reward ($)', REFERRAL_CONFIG.REFEREE_REWARD, 'Credit given to the new student who was referred'],
    ['Max Referrals Per Student', REFERRAL_CONFIG.MAX_REFERRALS, 'Maximum times one code can be used (0 = unlimited)'],
    ['Code Expiry (Days)', REFERRAL_CONFIG.CODE_EXPIRY_DAYS, 'Days until a code expires (0 = never)'],
    ['School Name', REFERRAL_CONFIG.SCHOOL_NAME, 'Used in email templates'],
    ['Admin Email', REFERRAL_CONFIG.ADMIN_EMAIL, 'Receives referral notifications'],
    ['Unsubscribe Email', REFERRAL_CONFIG.ADMIN_EMAIL, 'Students reply here to opt out of emails'],
    ['Include Code in Review Email', 'Yes', 'Also send referral code in the Google Review Request email'],
    ['Minimum Package for Code', 'Any', 'Minimum package to earn a referral code (Any, 5 Lessons, 10 Lessons, etc.)']
  ];
  settingsSheet.getRange(1, 1, settingsData.length, 3).setValues(settingsData);
  formatHeader(settingsSheet, 3);
  settingsSheet.setColumnWidth(1, 240);
  settingsSheet.setColumnWidth(2, 120);
  settingsSheet.setColumnWidth(3, 380);
  
  // Style
  const sRange = settingsSheet.getRange(2, 1, settingsData.length - 1, 3);
  sRange.setBackground('#0a0a0a');
  settingsSheet.getRange(2, 1, settingsData.length - 1, 1).setFontColor('#888888');
  settingsSheet.getRange(2, 2, settingsData.length - 1, 1).setFontColor('#ffffff').setFontWeight('bold');
  settingsSheet.getRange(2, 3, settingsData.length - 1, 1).setFontColor('#555555').setFontSize(9);
  settingsSheet.getRange(3, 2).setFontColor('#ff2d2d').setFontSize(12);
  settingsSheet.getRange(4, 2).setFontColor('#ff2d2d').setFontSize(12);
  
  // Demo mode row highlight
  settingsSheet.getRange(2, 2).setFontColor('#f59e0b').setFontSize(12);
  
  setupTriggers();
  
  Logger.log('=== REFERRAL REWARDS SYSTEM SETUP COMPLETE ===');
}


// ================================================================
//  DAILY CHECK โ€” Find completed students and send referral codes
// ================================================================

function dailyCompletionCheck() {
  const ss = getSpreadsheet();
  const settings = getSettings(ss);
  
  // === DEMO MODE โ€” log but don't process ===
  if (settings.demoMode) {
    Logger.log('โš ๏ธ DEMO MODE โ€” dailyCompletionCheck skipped. Set Demo Mode to "No" in Settings for production.');
    return;
  }
  
  const codesSheet = ss.getSheetByName('Referral Codes');
  
  // Get students who already have codes (by email + name)
  const existingCodes = getExistingCodes(codesSheet);
  
  // Get all registered students
  const students = getRegisteredStudents();
  
  // Get attendance counts (only confirmed past lessons)
  const attendance = getAttendanceCounts();
  
  let newCodes = 0;
  
  for (const student of students) {
    // Skip if already has a code (check email first, then name)
    if (student.email && existingCodes.emails.has(student.email.toLowerCase().trim())) continue;
    const nameKey = student.name.toLowerCase().trim();
    if (existingCodes.names.has(nameKey)) continue;
    if (isFuzzyTracked(nameKey, existingCodes.names)) continue;
    
    // Get completed lessons (email-first + fuzzy name matching)
    const completed = getStudentAttendance(student, attendance);
    const purchased = student.lessonCount;
    
    if (purchased <= 0 || completed < purchased) continue;
    
    // Check minimum package requirement
    if (settings.minPackage !== 'any') {
      const minLessons = REFERRAL_CONFIG.PACKAGES[settings.minPackage] || 0;
      if (purchased < minLessons) continue;
    }
    
    // Generate unique referral code
    const code = generateCode(student.name, codesSheet);
    
    // Calculate expiry
    let expiryDate = 'Never';
    if (settings.codeExpiryDays > 0) {
      const expiry = new Date();
      expiry.setDate(expiry.getDate() + settings.codeExpiryDays);
      expiryDate = expiry;
    }
    
    // Add to sheet
    codesSheet.appendRow([
      student.name, student.email, code, new Date(),
      expiryDate, 0, 0, 'Active'
    ]);
    
    // Style
    const rowNum = codesSheet.getLastRow();
    codesSheet.getRange(rowNum, 3).setFontWeight('bold').setFontColor('#ff2d2d');
    codesSheet.getRange(rowNum, 8).setFontColor('#22c55e');
    codesSheet.getRange(rowNum, 1, 1, 8).setBackground('#0a0a0a');
    
    // Send email
    sendReferralCodeEmail(student, code, settings);
    
    newCodes++;
    if (student.email) existingCodes.emails.add(student.email.toLowerCase().trim());
    existingCodes.names.add(nameKey);
    
    Logger.log('Generated referral code ' + code + ' for ' + student.name);
  }
  
  if (newCodes > 0) {
    sendAdminNewCodesAlert(newCodes, settings);
  }
  
  Logger.log('Daily check complete: ' + newCodes + ' new referral codes generated');
}


// ================================================================
//  REFERRAL PROCESSING
// ================================================================

function processReferral(newStudentName, newStudentEmail, newStudentPackage, referralCode) {
  const ss = getSpreadsheet();
  const settings = getSettings(ss);
  
  // Sanitize
  referralCode = String(referralCode).trim().toUpperCase();
  newStudentName = String(newStudentName).trim();
  newStudentEmail = String(newStudentEmail).trim();
  newStudentPackage = String(newStudentPackage).trim();
  
  if (!referralCode) return { success: false, message: 'No referral code provided' };
  
  // === DEMO MODE ===
  if (settings.demoMode) {
    Logger.log('โš ๏ธ DEMO MODE โ€” processReferral simulated for code: ' + referralCode);
    return { success: true, message: 'DEMO: Referral would be processed for code ' + referralCode };
  }
  
  // Validate code format
  if (!REFERRAL_CONFIG.CODE_PATTERN.test(referralCode)) {
    return { success: false, message: 'Invalid referral code format. Expected: NAME-XXXX (e.g., SARAH-7X3K)' };
  }
  
  const codesSheet = ss.getSheetByName('Referral Codes');
  const logSheet = ss.getSheetByName('Referral Log');
  
  // Generate processing ID for duplicate guard
  const processingId = newStudentEmail.toLowerCase() + '|' + referralCode;
  
  // Check if already processed (duplicate guard)
  const logData = logSheet.getDataRange().getValues();
  for (let i = 1; i < logData.length; i++) {
    const existingId = String(logData[i][10] || '').trim();
    if (existingId === processingId) {
      return { success: false, message: 'This referral has already been processed' };
    }
  }
  
  // Find the referral code
  const codesData = codesSheet.getDataRange().getValues();
  let codeRow = -1;
  let referrer = null;
  
  for (let i = 1; i < codesData.length; i++) {
    if (String(codesData[i][2]).trim().toUpperCase() === referralCode) {
      codeRow = i + 1;
      referrer = {
        name: String(codesData[i][0]).trim(),
        email: String(codesData[i][1]).trim(),
        code: String(codesData[i][2]).trim(),
        timesUsed: parseInt(codesData[i][5]) || 0,
        totalEarned: parseFloat(codesData[i][6]) || 0,
        status: String(codesData[i][7]).trim()
      };
      break;
    }
  }
  
  // Validate
  if (!referrer) return { success: false, message: 'Invalid referral code: ' + referralCode };
  if (referrer.status !== 'Active') return { success: false, message: 'Referral code is ' + referrer.status.toLowerCase() };
  
  // Max referrals check
  if (settings.maxReferrals > 0 && referrer.timesUsed >= settings.maxReferrals) {
    codesSheet.getRange(codeRow, 8).setValue('Maxed Out').setFontColor('#f59e0b');
    return { success: false, message: 'Referral code has reached maximum uses' };
  }
  
  // Expiry check
  const expiryVal = codesData[codeRow - 1][4];
  if (expiryVal instanceof Date && expiryVal < new Date()) {
    codesSheet.getRange(codeRow, 8).setValue('Expired').setFontColor('#ef4444');
    return { success: false, message: 'Referral code has expired' };
  }
  
  // Self-referral check
  if (referrer.email.toLowerCase() === newStudentEmail.toLowerCase()) {
    return { success: false, message: 'Cannot use your own referral code' };
  }
  
  // --- Process it ---
  const referrerReward = settings.referrerReward;
  const refereeReward = settings.refereeReward;
  
  // Update codes sheet
  const newTimesUsed = referrer.timesUsed + 1;
  const newTotalEarned = referrer.totalEarned + referrerReward;
  codesSheet.getRange(codeRow, 6).setValue(newTimesUsed);
  codesSheet.getRange(codeRow, 7).setValue(newTotalEarned);
  
  if (settings.maxReferrals > 0 && newTimesUsed >= settings.maxReferrals) {
    codesSheet.getRange(codeRow, 8).setValue('Maxed Out').setFontColor('#f59e0b');
  }
  
  // Log with processing ID
  logSheet.appendRow([
    new Date(), referralCode, referrer.name, referrer.email,
    newStudentName, newStudentEmail, newStudentPackage,
    referrerReward, refereeReward, 'Credited', processingId
  ]);
  
  const logRowNum = logSheet.getLastRow();
  logSheet.getRange(logRowNum, 1, 1, 11).setBackground('#0a0a0a');
  logSheet.getRange(logRowNum, 8).setFontColor('#22c55e').setFontWeight('bold');
  logSheet.getRange(logRowNum, 9).setFontColor('#22c55e').setFontWeight('bold');
  logSheet.getRange(logRowNum, 10).setFontColor('#22c55e');
  
  // Emails
  sendReferrerNotification(referrer, newStudentName, referrerReward, newTotalEarned, settings);
  sendRefereeWelcome(newStudentName, newStudentEmail, refereeReward, referrer.name, settings);
  sendAdminReferralAlert(referrer, newStudentName, newStudentPackage, referrerReward, refereeReward, settings);
  
  Logger.log('Referral processed: ' + newStudentName + ' referred by ' + referrer.name);
  return { success: true, message: 'Referral processed successfully!' };
}


// ================================================================
//  CHECK NEW REGISTRATIONS
// ================================================================

function checkNewRegistrations() {
  const ss = getSpreadsheet();
  const settings = getSettings(ss);
  
  if (settings.demoMode) {
    Logger.log('โš ๏ธ DEMO MODE โ€” checkNewRegistrations skipped.');
    return;
  }
  
  const logSheet = ss.getSheetByName('Referral Log');
  
  // Get already-processed pairs via processing ID
  const logData = logSheet.getDataRange().getValues();
  const processedIds = new Set();
  for (let i = 1; i < logData.length; i++) {
    const pid = String(logData[i][10] || '').trim();
    if (pid) processedIds.add(pid);
  }
  
  try {
    const regSheet = SpreadsheetApp.openById(REFERRAL_CONFIG.REGISTRATION_ID);
    const data = regSheet.getSheets()[0].getDataRange().getValues();
    if (data.length < 2) return;
    
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const nameCol = findCol(headers, ['name', 'full name', 'student name']);
    const emailCol = findCol(headers, ['email', 'student email', 'email address']);
    const pkgCol = findCol(headers, ['package', 'lesson package', 'selected package']);
    const refCol = findCol(headers, ['referral', 'referral code', 'referred by', 'promo code', 'coupon']);
    
    if (refCol === -1) {
      Logger.log('No referral code column found in Registration sheet.');
      return;
    }
    
    for (let i = 1; i < data.length; i++) {
      const name = nameCol !== -1 ? String(data[i][nameCol]).trim() : '';
      const email = emailCol !== -1 ? String(data[i][emailCol]).trim() : '';
      const pkg = pkgCol !== -1 ? String(data[i][pkgCol]).trim() : '';
      const refCode = String(data[i][refCol]).trim().toUpperCase();
      
      if (!refCode || !name) continue;
      
      // Check processing ID
      const pid = email.toLowerCase() + '|' + refCode;
      if (processedIds.has(pid)) continue;
      
      const result = processReferral(name, email, pkg, refCode);
      if (result.success) processedIds.add(pid);
      Logger.log('Registration check โ€” ' + name + ': ' + result.message);
    }
  } catch (e) {
    Logger.log('Error checking registrations: ' + e.message);
  }
}


// ================================================================
//  LEADERBOARD โ€” Top referrers summary
// ================================================================

function getLeaderboard(topN) {
  topN = topN || 10;
  const ss = getSpreadsheet();
  const codesSheet = ss.getSheetByName('Referral Codes');
  if (!codesSheet) return [];
  
  const data = codesSheet.getDataRange().getValues();
  const referrers = [];
  
  for (let i = 1; i < data.length; i++) {
    const name = String(data[i][0]).trim();
    const timesUsed = parseInt(data[i][5]) || 0;
    const totalEarned = parseFloat(data[i][6]) || 0;
    const code = String(data[i][2]).trim();
    const status = String(data[i][7]).trim();
    
    if (timesUsed > 0) {
      referrers.push({ name, code, timesUsed, totalEarned, status });
    }
  }
  
  // Sort by times used (desc), then total earned (desc)
  referrers.sort((a, b) => b.timesUsed - a.timesUsed || b.totalEarned - a.totalEarned);
  
  return referrers.slice(0, topN);
}

function emailLeaderboard() {
  const settings = getSettings(getSpreadsheet());
  const leaders = getLeaderboard(10);
  
  if (leaders.length === 0) {
    Logger.log('No referrals yet โ€” no leaderboard to send.');
    return;
  }
  
  let rows = '';
  leaders.forEach((r, i) => {
    const medal = i === 0 ? '๐Ÿฅ‡' : i === 1 ? '๐Ÿฅˆ' : i === 2 ? '๐Ÿฅ‰' : 'โ–ช๏ธ';
    rows += `<tr>
      <td style="padding:8px 12px;color:#888;font-size:14px;">${medal}</td>
      <td style="padding:8px 12px;color:#fff;font-weight:600;font-size:14px;">${r.name}</td>
      <td style="padding:8px 12px;color:#ff2d2d;font-weight:700;font-size:14px;text-align:center;">${r.timesUsed}</td>
      <td style="padding:8px 12px;color:#22c55e;font-weight:700;font-size:14px;text-align:right;">$${r.totalEarned}</td>
    </tr>`;
  });
  
  const html = `
<!DOCTYPE html>
<html><head><meta charset="utf-8"></head>
<body style="margin:0;padding:0;background:#000;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display',sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#000;padding:20px;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#0d0d0d;border-radius:20px;border:1px solid rgba(255,255,255,0.06);overflow:hidden;">
  <tr><td style="padding:32px 40px 16px;text-align:center;">
    <div style="font-size:36px;">๐Ÿ†</div>
    <h1 style="color:#fff;font-size:20px;margin:12px 0 0;">Referral Leaderboard</h1>
    <p style="color:#888;font-size:13px;margin:4px 0 0;">${settings.schoolName} โ€” Top Referrers</p>
  </td></tr>
  <tr><td style="padding:8px 24px 24px;">
    <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">
      <tr style="border-bottom:1px solid rgba(255,255,255,0.06);">
        <th style="padding:8px 12px;text-align:left;color:#555;font-size:11px;text-transform:uppercase;letter-spacing:1px;"></th>
        <th style="padding:8px 12px;text-align:left;color:#555;font-size:11px;text-transform:uppercase;letter-spacing:1px;">Student</th>
        <th style="padding:8px 12px;text-align:center;color:#555;font-size:11px;text-transform:uppercase;letter-spacing:1px;">Referrals</th>
        <th style="padding:8px 12px;text-align:right;color:#555;font-size:11px;text-transform:uppercase;letter-spacing:1px;">Earned</th>
      </tr>
      ${rows}
    </table>
  </td></tr>
  <tr><td style="padding:16px 40px;border-top:1px solid rgba(255,255,255,0.06);text-align:center;">
    <p style="margin:0;color:#333;font-size:11px;"><span style="color:#ff2d2d;">${settings.schoolName}</span> โ€” Referral Rewards</p>
  </td></tr>
</table>
</td></tr></table>
</body></html>`;
  
  try {
    MailApp.sendEmail({
      to: settings.adminEmail,
      subject: '๐Ÿ† Referral Leaderboard โ€” ' + settings.schoolName,
      htmlBody: html
    });
    Logger.log('Leaderboard email sent.');
  } catch (e) {
    Logger.log('Error sending leaderboard: ' + e.message);
  }
}


// ================================================================
//  CODE GENERATION
// ================================================================

function generateCode(studentName, codesSheet) {
  const firstName = studentName.split(' ')[0].toUpperCase().replace(/[^A-Z]/g, '') || 'STUDENT';
  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // No I, O, 0, 1
  let suffix = '';
  
  for (let i = 0; i < 4; i++) {
    suffix += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  
  const code = firstName + '-' + suffix;
  
  // Verify uniqueness
  if (codesSheet) {
    const existingData = codesSheet.getDataRange().getValues();
    for (let i = 1; i < existingData.length; i++) {
      if (String(existingData[i][2]).toUpperCase() === code) {
        return generateCode(studentName, codesSheet);
      }
    }
  }
  
  return code;
}


// ================================================================
//  EMAIL TEMPLATES
// ================================================================

function buildUnsubscribeLink(studentName, unsubEmail) {
  return 'mailto:' + (unsubEmail || '') + '?subject=Unsubscribe%20Referral%20Emails&body=Please%20remove%20me%20from%20referral%20reward%20emails.%20Name:%20' + encodeURIComponent(studentName);
}

function sendReferralCodeEmail(student, code, settings) {
  const firstName = student.name.split(' ')[0];
  const unsub = buildUnsubscribeLink(student.name, settings.unsubscribeEmail);
  
  const html = `
<!DOCTYPE html>
<html><head><meta charset="utf-8"></head>
<body style="margin:0;padding:0;background:#000000;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','Helvetica Neue',Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#000000;padding:20px;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#0d0d0d;border-radius:20px;border:1px solid rgba(255,255,255,0.06);overflow:hidden;box-shadow:0 8px 30px rgba(0,0,0,0.5);">

  <tr><td style="padding:40px 40px 16px;text-align:center;">
    <div style="font-size:40px;letter-spacing:4px;">๐ŸŽ‰๐Ÿš—๐Ÿ’จ</div>
    <h1 style="margin:16px 0 0;color:#ffffff;font-size:22px;font-weight:700;">
      Congrats ${firstName}! You've earned a reward!
    </h1>
    <p style="margin:8px 0 0;color:#888;font-size:14px;line-height:1.5;">
      You completed your lessons at <span style="color:#ff2d2d;font-weight:600;">${settings.schoolName}</span> โ€” and now you can help your friends save too.
    </p>
  </td></tr>

  <tr><td style="padding:16px 40px 8px;text-align:center;">
    <div style="background:rgba(255,45,45,0.08);border:2px dashed rgba(255,45,45,0.3);border-radius:16px;padding:24px;margin:0 auto;max-width:300px;">
      <p style="margin:0 0 8px;color:#888;font-size:12px;text-transform:uppercase;letter-spacing:1px;">Your Referral Code</p>
      <p style="margin:0;color:#ff2d2d;font-size:32px;font-weight:800;letter-spacing:2px;">${code}</p>
    </div>
  </td></tr>

  <tr><td style="padding:20px 40px 24px;">
    <h2 style="color:#fff;font-size:16px;font-weight:600;margin:0 0 12px;">Here's how it works:</h2>
    <table cellpadding="0" cellspacing="0" style="width:100%;">
      <tr>
        <td style="padding:8px 12px 8px 0;vertical-align:top;">
          <div style="width:28px;height:28px;background:rgba(255,45,45,0.15);border-radius:8px;text-align:center;line-height:28px;font-size:13px;color:#ff2d2d;font-weight:700;">1</div>
        </td>
        <td style="padding:8px 0;color:#bbb;font-size:14px;line-height:1.5;">
          Share your code <strong style="color:#ff2d2d;">${code}</strong> with friends or family
        </td>
      </tr>
      <tr>
        <td style="padding:8px 12px 8px 0;vertical-align:top;">
          <div style="width:28px;height:28px;background:rgba(255,45,45,0.15);border-radius:8px;text-align:center;line-height:28px;font-size:13px;color:#ff2d2d;font-weight:700;">2</div>
        </td>
        <td style="padding:8px 0;color:#bbb;font-size:14px;line-height:1.5;">
          They enter your code when they sign up for lessons
        </td>
      </tr>
      <tr>
        <td style="padding:8px 12px 8px 0;vertical-align:top;">
          <div style="width:28px;height:28px;background:rgba(255,45,45,0.15);border-radius:8px;text-align:center;line-height:28px;font-size:13px;color:#ff2d2d;font-weight:700;">3</div>
        </td>
        <td style="padding:8px 0;color:#bbb;font-size:14px;line-height:1.5;">
          <strong style="color:#22c55e;">You get $${settings.referrerReward}</strong> credit and <strong style="color:#22c55e;">they get $${settings.refereeReward} off</strong> their package!
        </td>
      </tr>
    </table>
    <p style="color:#666;font-size:12px;margin:16px 0 0;text-align:center;">
      ${settings.maxReferrals > 0 ? 'You can refer up to ' + settings.maxReferrals + ' friends. ' : ''}No limit on savings! ๐Ÿ’ฐ
    </p>
  </td></tr>

  <tr><td style="padding:0 40px 32px;text-align:center;">
    <p style="margin:0;color:#888;font-size:13px;">
      Just tell them to mention code <strong style="color:#ff2d2d;">${code}</strong> when they register!
    </p>
  </td></tr>

  <tr><td style="padding:16px 40px;border-top:1px solid rgba(255,255,255,0.06);text-align:center;">
    <p style="margin:0;color:#333;font-size:11px;">
      <span style="color:#ff2d2d;">${settings.schoolName}</span><br>
      <span style="color:#444;">Thank you for being an amazing student! ๐Ÿ™</span>
    </p>
    <p style="margin:8px 0 0;color:#333;font-size:10px;">
      <a href="${unsub}" style="color:#555;text-decoration:underline;">Unsubscribe</a>
    </p>
  </td></tr>

</table>
</td></tr></table>
</body></html>`;

  try {
    MailApp.sendEmail({ to: student.email, subject: '๐ŸŽ‰ You earned a referral reward! Share code ' + code + ' โ€” ' + settings.schoolName, htmlBody: html });
  } catch (e) { Logger.log('Error sending referral code email to ' + student.email + ': ' + e.message); }
}

function sendReferrerNotification(referrer, newStudentName, reward, totalEarned, settings) {
  const firstName = referrer.name.split(' ')[0];
  const newFirstName = newStudentName.split(' ')[0];
  const unsub = buildUnsubscribeLink(referrer.name, settings.unsubscribeEmail);
  
  const html = `
<!DOCTYPE html>
<html><head><meta charset="utf-8"></head>
<body style="margin:0;padding:0;background:#000000;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','Helvetica Neue',Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#000000;padding:20px;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#0d0d0d;border-radius:20px;border:1px solid rgba(255,255,255,0.06);overflow:hidden;">

  <tr><td style="padding:36px 40px 16px;text-align:center;">
    <div style="font-size:36px;">๐Ÿ’ฐ</div>
    <h1 style="margin:12px 0 0;color:#ffffff;font-size:20px;font-weight:700;">
      Ka-ching! ${newFirstName} just used your code!
    </h1>
  </td></tr>

  <tr><td style="padding:12px 40px 24px;text-align:center;">
    <div style="background:rgba(34,197,94,0.08);border:1px solid rgba(34,197,94,0.2);border-radius:14px;padding:20px;display:inline-block;">
      <p style="margin:0;color:#888;font-size:12px;text-transform:uppercase;letter-spacing:1px;">Credit Earned</p>
      <p style="margin:4px 0 0;color:#22c55e;font-size:36px;font-weight:800;">+$${reward}</p>
    </div>
    <p style="margin:12px 0 0;color:#666;font-size:13px;">
      Total referral credits: <strong style="color:#22c55e;">$${totalEarned}</strong>
    </p>
  </td></tr>

  <tr><td style="padding:0 40px 24px;text-align:center;">
    <p style="color:#bbb;font-size:14px;line-height:1.6;margin:0;">
      Your friend ${newFirstName} signed up for driving lessons and used your referral code. 
      Your <strong style="color:#22c55e;">$${reward} credit</strong> will be applied to your account!
    </p>
  </td></tr>

  <tr><td style="padding:0 40px 24px;text-align:center;">
    <p style="color:#888;font-size:13px;margin:0;">
      Keep sharing your code <strong style="color:#ff2d2d;">${referrer.code}</strong> to earn more! ๐Ÿ”ฅ
    </p>
  </td></tr>

  <tr><td style="padding:16px 40px;border-top:1px solid rgba(255,255,255,0.06);text-align:center;">
    <p style="margin:0;color:#333;font-size:11px;"><span style="color:#ff2d2d;">${settings.schoolName}</span> โ€” Referral Rewards</p>
    <p style="margin:8px 0 0;color:#333;font-size:10px;"><a href="${unsub}" style="color:#555;text-decoration:underline;">Unsubscribe</a></p>
  </td></tr>

</table>
</td></tr></table>
</body></html>`;

  try {
    MailApp.sendEmail({ to: referrer.email, subject: '๐Ÿ’ฐ +$' + reward + '! Someone used your referral code โ€” ' + settings.schoolName, htmlBody: html });
  } catch (e) { Logger.log('Error sending referrer notification: ' + e.message); }
}

function sendRefereeWelcome(name, email, reward, referrerName, settings) {
  const firstName = name.split(' ')[0];
  const referrerFirst = referrerName.split(' ')[0];
  const unsub = buildUnsubscribeLink(name, settings.unsubscribeEmail);
  
  const html = `
<!DOCTYPE html>
<html><head><meta charset="utf-8"></head>
<body style="margin:0;padding:0;background:#000000;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','Helvetica Neue',Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#000000;padding:20px;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#0d0d0d;border-radius:20px;border:1px solid rgba(255,255,255,0.06);overflow:hidden;">

  <tr><td style="padding:36px 40px 16px;text-align:center;">
    <div style="font-size:36px;">๐ŸŽŠ๐Ÿš—</div>
    <h1 style="margin:12px 0 0;color:#ffffff;font-size:20px;font-weight:700;">
      Welcome ${firstName}! You've got a reward!
    </h1>
  </td></tr>

  <tr><td style="padding:12px 40px 24px;text-align:center;">
    <p style="color:#bbb;font-size:14px;line-height:1.6;margin:0;">
      Thanks to your friend ${referrerFirst}'s referral, you've earned a 
      <strong style="color:#22c55e;">$${reward} credit</strong> on your driving lessons!
    </p>
    <div style="background:rgba(34,197,94,0.08);border:1px solid rgba(34,197,94,0.2);border-radius:14px;padding:16px;margin:16px auto;max-width:200px;">
      <p style="margin:0;color:#888;font-size:12px;text-transform:uppercase;letter-spacing:1px;">Your Reward</p>
      <p style="margin:4px 0 0;color:#22c55e;font-size:28px;font-weight:800;">$${reward} OFF</p>
    </div>
    <p style="color:#888;font-size:13px;margin:0;">
      This credit will be applied to your package automatically.
    </p>
  </td></tr>

  <tr><td style="padding:0 40px 24px;text-align:center;">
    <p style="color:#666;font-size:13px;margin:0;">
      Welcome to <span style="color:#ff2d2d;font-weight:600;">${settings.schoolName}</span>! We can't wait to help you get on the road. ๐Ÿš—
    </p>
  </td></tr>

  <tr><td style="padding:16px 40px;border-top:1px solid rgba(255,255,255,0.06);text-align:center;">
    <p style="margin:0;color:#333;font-size:11px;"><span style="color:#ff2d2d;">${settings.schoolName}</span> โ€” Referral Rewards</p>
    <p style="margin:8px 0 0;color:#333;font-size:10px;"><a href="${unsub}" style="color:#555;text-decoration:underline;">Unsubscribe</a></p>
  </td></tr>

</table>
</td></tr></table>
</body></html>`;

  try {
    MailApp.sendEmail({ to: email, subject: '๐ŸŽŠ Welcome! You earned $' + reward + ' off your lessons โ€” ' + settings.schoolName, htmlBody: html });
  } catch (e) { Logger.log('Error sending referee welcome: ' + e.message); }
}

function sendAdminNewCodesAlert(count, settings) {
  try {
    MailApp.sendEmail({
      to: settings.adminEmail,
      subject: '๐ŸŽฏ ' + count + ' New Referral Code(s) Generated โ€” ' + settings.schoolName,
      htmlBody: '<div style="font-family:sans-serif;background:#0a0a0a;color:#ccc;padding:24px;border-radius:12px;"><h2 style="color:#ff2d2d;margin:0 0 8px;">New Referral Codes</h2><p>' + count + ' student(s) completed their package and received referral codes today.</p></div>'
    });
  } catch (e) { Logger.log('Error sending admin alert: ' + e.message); }
}

function sendAdminReferralAlert(referrer, newStudentName, pkg, referrerReward, refereeReward, settings) {
  try {
    MailApp.sendEmail({
      to: settings.adminEmail,
      subject: '๐Ÿ’ฐ Referral Used! ' + newStudentName + ' referred by ' + referrer.name + ' โ€” ' + settings.schoolName,
      htmlBody: '<div style="font-family:sans-serif;background:#0a0a0a;color:#ccc;padding:24px;border-radius:12px;"><h2 style="color:#22c55e;margin:0 0 12px;">Referral Processed โœ…</h2><p><strong style="color:#fff;">' + newStudentName + '</strong> signed up for <strong>' + pkg + '</strong></p><p>Referred by: <strong style="color:#fff;">' + referrer.name + '</strong> (code: <strong style="color:#ff2d2d;">' + referrer.code + '</strong>)</p><hr style="border:none;border-top:1px solid #222;margin:12px 0;"><p>Referrer credit: <strong style="color:#22c55e;">+$' + referrerReward + '</strong></p><p>New student discount: <strong style="color:#22c55e;">$' + refereeReward + ' off</strong></p></div>'
    });
  } catch (e) { Logger.log('Error sending admin referral alert: ' + e.message); }
}


// ================================================================
//  DATA HELPERS
// ================================================================

function getExistingCodes(codesSheet) {
  const data = codesSheet.getDataRange().getValues();
  const emails = new Set();
  const names = new Set();
  for (let i = 1; i < data.length; i++) {
    const email = String(data[i][1]).toLowerCase().trim();
    const name = String(data[i][0]).toLowerCase().trim();
    if (email && email.includes('@')) emails.add(email);
    if (name) names.add(name);
  }
  return { emails, names };
}

function getRegisteredStudents() {
  const students = [];
  try {
    const regSheet = SpreadsheetApp.openById(REFERRAL_CONFIG.REGISTRATION_ID);
    const data = regSheet.getSheets()[0].getDataRange().getValues();
    if (data.length < 2) return students;
    
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const nameCol = findCol(headers, ['name', 'full name', 'student name']);
    const emailCol = findCol(headers, ['email', 'student email', 'email address']);
    const pkgCol = findCol(headers, ['package', 'lesson package', 'selected package', 'number of lessons']);
    
    for (let i = 1; i < data.length; i++) {
      const name = nameCol !== -1 ? String(data[i][nameCol]).trim() : '';
      const email = emailCol !== -1 ? String(data[i][emailCol]).trim() : '';
      const pkgRaw = pkgCol !== -1 ? String(data[i][pkgCol]).trim() : '';
      if (!name) continue;
      
      let pkg = 'Unknown', lessonCount = 0;
      for (const [key, count] of Object.entries(REFERRAL_CONFIG.PACKAGES)) {
        if (pkgRaw.toLowerCase().includes(key.toLowerCase())) { pkg = key; lessonCount = count; break; }
      }
      students.push({ name, email, package: pkg, lessonCount });
    }
  } catch (e) { Logger.log('Error reading registrations: ' + e.message); }
  return students;
}

function getAttendanceCounts() {
  // Returns { byEmail: {}, byName: {} }
  const counts = { byEmail: {}, byName: {} };
  try {
    const schedSheet = SpreadsheetApp.openById(REFERRAL_CONFIG.SCHEDULE_ID);
    const data = schedSheet.getSheets()[0].getDataRange().getValues();
    if (data.length < 2) return counts;
    
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const nameCol = findCol(headers, ['student name', 'student', 'name']);
    const emailCol = findCol(headers, ['email', 'student email']);
    const statusCol = findCol(headers, ['status', 'booking status']);
    const dateCol = findCol(headers, ['date', 'lesson date', 'booking date']);
    
    const now = new Date();
    const skipStatuses = ['cancelled', 'canceled', 'no-show', 'no show', 'noshow', 'pending', 'scheduled', 'upcoming', 'rescheduled'];
    
    for (let i = 1; i < data.length; i++) {
      const name = nameCol !== -1 ? String(data[i][nameCol]).trim().toLowerCase() : '';
      const email = emailCol !== -1 ? String(data[i][emailCol]).trim().toLowerCase() : '';
      if (!name && !email) continue;
      
      // Only count confirmed past lessons
      if (dateCol !== -1) {
        const d = data[i][dateCol];
        if (d instanceof Date && d > now) continue;
        if (!(d instanceof Date) && d) {
          const parsed = new Date(d);
          if (!isNaN(parsed) && parsed > now) continue;
        }
      }
      
      if (statusCol !== -1) {
        const status = String(data[i][statusCol]).toLowerCase().trim();
        if (skipStatuses.includes(status)) continue;
      }
      
      if (email && email.includes('@')) counts.byEmail[email] = (counts.byEmail[email] || 0) + 1;
      if (name) counts.byName[name] = (counts.byName[name] || 0) + 1;
    }
  } catch (e) { Logger.log('Error reading schedule: ' + e.message); }
  return counts;
}

function getStudentAttendance(student, attendance) {
  // Email first
  if (student.email) {
    const ek = student.email.toLowerCase().trim();
    if (attendance.byEmail[ek]) return attendance.byEmail[ek];
  }
  // Exact name
  const nk = student.name.toLowerCase().trim();
  if (attendance.byName[nk]) return attendance.byName[nk];
  // Fuzzy name
  for (const [aName, count] of Object.entries(attendance.byName)) {
    if (fuzzyNameMatch(nk, aName)) return count;
  }
  return 0;
}

// ============ FUZZY NAME MATCHING ============

function fuzzyNameMatch(name1, name2) {
  if (!name1 || !name2) return false;
  const n1 = name1.toLowerCase().replace(/\s+/g, ' ').trim();
  const n2 = name2.toLowerCase().replace(/\s+/g, ' ').trim();
  if (n1 === n2) return true;
  if (n1.includes(n2) || n2.includes(n1)) return true;
  
  const p1 = n1.split(' ').filter(Boolean);
  const p2 = n2.split(' ').filter(Boolean);
  
  if (p1.length >= 2 && p2.length >= 2) {
    if (p1[p1.length-1] === p2[p2.length-1] && p1[0].substring(0,3) === p2[0].substring(0,3)) return true;
    if (p1[0] === p2[p2.length-1] && p1[p1.length-1] === p2[0]) return true;
  }
  if (levenshtein(n1, n2) <= 2) return true;
  return false;
}

function isFuzzyTracked(nameKey, trackedNames) {
  for (const tracked of trackedNames) {
    if (fuzzyNameMatch(nameKey, tracked)) return true;
  }
  return false;
}

function levenshtein(a, b) {
  if (a.length === 0) return b.length;
  if (b.length === 0) return a.length;
  const m = [];
  for (let i = 0; i <= b.length; i++) m[i] = [i];
  for (let j = 0; j <= a.length; j++) m[0][j] = j;
  for (let i = 1; i <= b.length; i++) {
    for (let j = 1; j <= a.length; j++) {
      m[i][j] = b.charAt(i-1) === a.charAt(j-1) ? m[i-1][j-1] : Math.min(m[i-1][j-1]+1, m[i][j-1]+1, m[i-1][j]+1);
    }
  }
  return m[b.length][a.length];
}

function getSettings(ss) {
  const d = {
    demoMode: REFERRAL_CONFIG.DEMO_MODE,
    referrerReward: REFERRAL_CONFIG.REFERRER_REWARD,
    refereeReward: REFERRAL_CONFIG.REFEREE_REWARD,
    maxReferrals: REFERRAL_CONFIG.MAX_REFERRALS,
    codeExpiryDays: REFERRAL_CONFIG.CODE_EXPIRY_DAYS,
    schoolName: REFERRAL_CONFIG.SCHOOL_NAME,
    adminEmail: REFERRAL_CONFIG.ADMIN_EMAIL,
    unsubscribeEmail: REFERRAL_CONFIG.ADMIN_EMAIL,
    includeInReviewEmail: true,
    minPackage: 'any'
  };
  
  const sheet = ss.getSheetByName('Settings');
  if (!sheet) return d;
  
  const data = sheet.getDataRange().getValues();
  for (let i = 1; i < data.length; i++) {
    const key = String(data[i][0]).toLowerCase().trim();
    const val = String(data[i][1]).trim();
    
    if (key.includes('demo mode')) d.demoMode = val.toLowerCase() === 'yes';
    else if (key.includes('referrer reward')) d.referrerReward = parseFloat(val) || 25;
    else if (key.includes('new student reward')) d.refereeReward = parseFloat(val) || 25;
    else if (key.includes('max referrals')) d.maxReferrals = parseInt(val) || 0;
    else if (key.includes('expiry')) d.codeExpiryDays = parseInt(val) || 0;
    else if (key.includes('school name')) d.schoolName = val;
    else if (key.includes('admin email')) d.adminEmail = val;
    else if (key.includes('unsubscribe')) d.unsubscribeEmail = val;
    else if (key.includes('review email')) d.includeInReviewEmail = val.toLowerCase() === 'yes';
    else if (key.includes('minimum package')) d.minPackage = val.toLowerCase();
  }
  return d;
}

function findCol(headers, keywords) {
  for (const kw of keywords) {
    const idx = headers.findIndex(h => h.includes(kw));
    if (idx !== -1) return idx;
  }
  return -1;
}

function formatHeader(sheet, cols) {
  const range = sheet.getRange(1, 1, 1, cols);
  range.setBackground('#1a1a1a').setFontColor('#ff2d2d').setFontWeight('bold').setFontSize(10).setHorizontalAlignment('center');
  sheet.setFrozenRows(1);
}


// ================================================================
//  TRIGGERS
// ================================================================

function setupTriggers() {
  const clean = ['dailyCompletionCheck', 'checkNewRegistrations'];
  ScriptApp.getProjectTriggers().forEach(t => {
    if (clean.includes(t.getHandlerFunction())) ScriptApp.deleteTrigger(t);
  });
  
  ScriptApp.newTrigger('dailyCompletionCheck').timeBased().everyDays(1).atHour(10).nearMinute(30).create();
  ScriptApp.newTrigger('checkNewRegistrations').timeBased().everyHours(2).create();
  
  Logger.log('Triggers set: dailyCompletionCheck at 10:30 AM, checkNewRegistrations every 2h');
}


// ================================================================
//  MANUAL TESTING
// ================================================================

function testReferralCodeEmail() {
  const settings = getSettings(getSpreadsheet());
  const code = generateCode('Test', null);
  sendReferralCodeEmail({ name: 'Test Student', email: REFERRAL_CONFIG.ADMIN_EMAIL }, code, settings);
  Logger.log('Test referral code email sent with code: ' + code);
}

function testReferrerNotification() {
  const settings = getSettings(getSpreadsheet());
  sendReferrerNotification({ name: 'Test Referrer', email: REFERRAL_CONFIG.ADMIN_EMAIL, code: 'TEST-X7K2' }, 'New Friend', 25, 75, settings);
  Logger.log('Test referrer notification sent');
}

function testRefereeWelcome() {
  const settings = getSettings(getSpreadsheet());
  sendRefereeWelcome('New Student', REFERRAL_CONFIG.ADMIN_EMAIL, 25, 'Test Referrer', settings);
  Logger.log('Test referee welcome sent');
}

function testLeaderboard() {
  emailLeaderboard();
}
๐Ÿง  Smart Scheduling Optimizer 51KB ยท 1,184 linesGSโ–ถ
/**
 * =========================================================
 *  SMART SCHEDULING OPTIMIZER โ€” Automation #14
 *  Flavors Driving School
 * =========================================================
 *  Features:
 *  - Demo mode for presentations (fake data, no real emails)
 *  - Scan Schedule Board for open slots
 *  - Match unscheduled students to open slots (email-first + fuzzy name)
 *  - Travel time buffer (15-min gap flags)
 *  - Student preference matching (instructor + time + day)
 *  - Cancellation backfill (auto-email waitlist)
 *  - Instructor load balancing
 *  - Schedule Health Score (0-100)
 *  - Weekly optimization report
 *  - Priority fills (most lessons remaining first)
 *  - "We have an opening" auto-emails with unsubscribe
 *  - SMS-ready flag for future text notifications
 *  - Duplicate email guard (ScriptProperties cooldown)
 * =========================================================
 */

// โ”€โ”€ CONFIG โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const CONFIG = {
  SCHEDULE_BOARD_ID: '1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE',
  REGISTRATION_ID: '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',
  PAYMENT_TRACKER_ID: '1XWNU7PygQX9jV6mFUUrgHYU_vrhS7NP1OHwNUNWRoOk',
  SCHOOL_NAME: 'Flavors Driving School',
  SCHOOL_EMAIL: '[email protected]',
  INSTRUCTORS: ['Anisha', 'Carlos', 'Nick'],
  WORK_START: 9,
  WORK_END: 18,
  SLOT_DURATION: 60,
  TRAVEL_BUFFER_MINUTES: 15,
  LOAD_BALANCE_THRESHOLD: 0.3,
  MAX_EMAILS_PER_RUN: 20,
  EMAIL_COOLDOWN_HOURS: 48, // Don't re-email same student within this window

  // DEMO MODE โ€” set to true for presentations
  DEMO_MODE: true,
};

// โ”€โ”€ FORCE AUTH โ€” all connected sheets + email โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function forceAuth() {
  SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
  MailApp.getRemainingDailyQuota();
  Logger.log('โœ… All permissions authorized โ€” 3 sheets + email.');
}

// โ”€โ”€ SETUP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function fullSetup() {
  createOptimizerSheets_();
  setupTriggers_();
  Logger.log('โœ… Smart Scheduling Optimizer fully set up.');
}

function createOptimizerSheets_() {
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);

  // Student Preferences
  let prefSheet = ss.getSheetByName('Student Preferences');
  if (!prefSheet) {
    prefSheet = ss.insertSheet('Student Preferences');
    prefSheet.getRange('A1:F1').setValues([['Student Name', 'Email', 'Preferred Instructor', 'Preferred Time', 'Preferred Days', 'Notes']]);
    formatHeader_(prefSheet, 6);
  }

  // Waitlist
  let waitSheet = ss.getSheetByName('Waitlist');
  if (!waitSheet) {
    waitSheet = ss.insertSheet('Waitlist');
    waitSheet.getRange('A1:H1').setValues([['Student Name', 'Email', 'Phone', 'Preferred Instructor', 'Preferred Day', 'Preferred Time', 'Date Added', 'Status']]);
    formatHeader_(waitSheet, 8);
  }

  // Optimization Log
  let logSheet = ss.getSheetByName('Optimization Log');
  if (!logSheet) {
    logSheet = ss.insertSheet('Optimization Log');
    logSheet.getRange('A1:F1').setValues([['Date', 'Action', 'Student', 'Instructor', 'Slot', 'Details']]);
    formatHeader_(logSheet, 6);
  }

  // Optimizer Settings
  let settingsSheet = ss.getSheetByName('Optimizer Settings');
  if (!settingsSheet) {
    settingsSheet = ss.insertSheet('Optimizer Settings');
    settingsSheet.getRange('A1:B1').setValues([['Setting', 'Value']]);
    formatHeader_(settingsSheet, 2);
    const defaults = [
      ['Demo Mode', CONFIG.DEMO_MODE ? 'Yes' : 'No'],
      ['Travel Buffer (minutes)', CONFIG.TRAVEL_BUFFER_MINUTES],
      ['Load Balance Threshold (%)', CONFIG.LOAD_BALANCE_THRESHOLD * 100],
      ['Max Emails Per Run', CONFIG.MAX_EMAILS_PER_RUN],
      ['Auto-Send Opening Emails', 'Yes'],
      ['Auto-Backfill Cancellations', 'Yes'],
      ['Weekly Report Day', 'Monday'],
      ['Weekly Report Hour', 8],
      ['Unsubscribe Email', CONFIG.SCHOOL_EMAIL],
      ['SMS Notifications (Future)', 'No'],
      ['Email Cooldown (hours)', CONFIG.EMAIL_COOLDOWN_HOURS],
    ];
    settingsSheet.getRange(2, 1, defaults.length, 2).setValues(defaults);
    settingsSheet.getRange(2, 2).setFontColor('#f59e0b').setFontWeight('bold').setFontSize(12);
    settingsSheet.getRange(2, 1, defaults.length, 2).setBackground('#0a0a0a');
    settingsSheet.getRange(2, 1, defaults.length, 1).setFontColor('#888');
    settingsSheet.getRange(2, 2, defaults.length, 1).setFontColor('#fff').setFontWeight('bold');
    settingsSheet.setColumnWidth(1, 260);
    settingsSheet.setColumnWidth(2, 160);
  }
}

function formatHeader_(sheet, cols) {
  sheet.getRange(1, 1, 1, cols).setFontWeight('bold').setBackground('#1a1a1a').setFontColor('#ff2d2d').setFontSize(10);
  sheet.setFrozenRows(1);
}

// โ”€โ”€ TRIGGERS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function setupTriggers_() {
  const clean = ['dailyOptimize', 'weeklyReport'];
  ScriptApp.getProjectTriggers().forEach(t => {
    if (clean.includes(t.getHandlerFunction())) ScriptApp.deleteTrigger(t);
  });

  ScriptApp.newTrigger('dailyOptimize').timeBased().everyDays(1).atHour(7).nearMinute(0).create();
  ScriptApp.newTrigger('weeklyReport').timeBased().onWeekDay(ScriptApp.WeekDay.MONDAY).atHour(8).nearMinute(0).create();

  Logger.log('โœ… Triggers: dailyOptimize 7 AM daily, weeklyReport Monday 8 AM');
}


// ================================================================
//  DEMO DATA
// ================================================================

const DEMO = {
  openSlots: [
    { dateStr: '7/1/2025', dayName: 'Tuesday', time: '10:00 AM', hour: 10, instructor: 'Carlos', key: 'demo1' },
    { dateStr: '7/1/2025', dayName: 'Tuesday', time: '2:00 PM', hour: 14, instructor: 'Anisha', key: 'demo2' },
    { dateStr: '7/2/2025', dayName: 'Wednesday', time: '11:00 AM', hour: 11, instructor: 'Nick', key: 'demo3' },
    { dateStr: '7/2/2025', dayName: 'Wednesday', time: '3:00 PM', hour: 15, instructor: 'Carlos', key: 'demo4' },
    { dateStr: '7/3/2025', dayName: 'Thursday', time: '9:00 AM', hour: 9, instructor: 'Anisha', key: 'demo5' },
    { dateStr: '7/3/2025', dayName: 'Thursday', time: '1:00 PM', hour: 13, instructor: 'Nick', key: 'demo6' },
    { dateStr: '7/4/2025', dayName: 'Friday', time: '10:00 AM', hour: 10, instructor: 'Carlos', key: 'demo7' },
    { dateStr: '7/4/2025', dayName: 'Friday', time: '4:00 PM', hour: 16, instructor: 'Anisha', key: 'demo8' },
  ],
  students: [
    { name: 'Sarah Johnson', email: '[email protected]', phone: '555-1234', package: '10 Lessons', totalLessons: 10, lessonsRemaining: 7, balance: 150 },
    { name: 'Marcus Williams', email: '[email protected]', phone: '555-5678', package: '5 Lessons', totalLessons: 5, lessonsRemaining: 3, balance: 0 },
    { name: 'Priya Patel', email: '[email protected]', phone: '555-9012', package: '15 Lessons', totalLessons: 15, lessonsRemaining: 12, balance: 300 },
  ],
  preferences: {
    'sarah johnson': { preferredInstructor: 'Carlos', preferredTime: 'Morning', preferredDays: 'Tue, Thu' },
    'marcus williams': { preferredInstructor: '', preferredTime: 'Afternoon', preferredDays: '' },
    'priya patel': { preferredInstructor: 'Anisha', preferredTime: '10 AM', preferredDays: 'Mon, Wed, Fri' },
  },
  loadData: {
    'Anisha': { booked: 22, total: 63, utilization: 0.35 },
    'Carlos': { booked: 35, total: 63, utilization: 0.56 },
    'Nick': { booked: 18, total: 63, utilization: 0.29 },
  }
};


// ================================================================
//  CORE: DAILY OPTIMIZATION
// ================================================================

function dailyOptimize() {
  try {
    const settings = getSettings_();
    const isDemo = settings.demoMode;

    if (isDemo) {
      Logger.log('โš ๏ธ DEMO MODE โ€” running with simulated data, no real emails sent.');
    }

    const today = new Date();
    const daysToScan = 7;

    const openSlots = isDemo ? DEMO.openSlots : getOpenSlots_(today, daysToScan);
    const unscheduledStudents = isDemo ? DEMO.students : getUnscheduledStudents_();
    const preferences = isDemo ? DEMO.preferences : getStudentPreferences_();
    const loadData = isDemo ? DEMO.loadData : getInstructorLoad_(today, daysToScan);

    if (openSlots.length === 0 || unscheduledStudents.length === 0) {
      logAction_('Daily Scan', '-', '-', '-',
        'Open slots: ' + openSlots.length + ', Unscheduled: ' + unscheduledStudents.length + '. No matches needed.');
      return;
    }

    const matches = scoreMatches_(openSlots, unscheduledStudents, preferences, loadData);
    const maxEmails = settings.maxEmails || CONFIG.MAX_EMAILS_PER_RUN;
    const autoSend = settings.autoSendOpenings;
    const cooldownHours = settings.emailCooldownHours || CONFIG.EMAIL_COOLDOWN_HOURS;

    if (autoSend && matches.length > 0) {
      let sent = 0;
      for (const match of matches) {
        if (sent >= maxEmails) break;

        // Duplicate email guard โ€” skip if emailed recently
        if (!isDemo && hasRecentEmail_(match.student.email, cooldownHours)) {
          Logger.log('โญ๏ธ Skipping ' + match.student.email + ' โ€” emailed within ' + cooldownHours + 'h');
          continue;
        }

        if (!isDemo) {
          sendOpeningEmail_(match, settings);
          recordEmailSent_(match.student.email);
        }
        logAction_('Opening Email' + (isDemo ? ' [DEMO]' : ''), match.student.name, match.slot.instructor,
          formatSlot_(match.slot), 'Score: ' + match.score);
        sent++;
      }
      Logger.log((isDemo ? 'DEMO: Would send ' : 'Sent ') + sent + ' opening emails.');
    }

    // Calculate health score
    const totalSlots = CONFIG.INSTRUCTORS.length * daysToScan * (CONFIG.WORK_END - CONFIG.WORK_START);
    const filledSlots = totalSlots - openSlots.length;
    const healthScore = calculateHealthScore_(filledSlots, totalSlots, loadData, unscheduledStudents.length);

    logAction_('Daily Scan Complete', '-', '-', '-',
      'Slots: ' + openSlots.length + ' open / ' + totalSlots + ' total, Matches: ' + matches.length + ', Health: ' + healthScore + '/100');

  } catch (e) {
    Logger.log('โŒ dailyOptimize error: ' + e.message + '\n' + e.stack);
    logAction_('ERROR', '-', '-', '-', 'dailyOptimize: ' + e.message);
  }
}


// ================================================================
//  EMAIL COOLDOWN (duplicate guard)
// ================================================================

function hasRecentEmail_(email, cooldownHours) {
  try {
    const props = PropertiesService.getScriptProperties();
    const key = 'email_sent_' + email.toLowerCase();
    const lastSent = props.getProperty(key);
    if (!lastSent) return false;
    const hoursSince = (Date.now() - parseInt(lastSent)) / (1000 * 60 * 60);
    return hoursSince < cooldownHours;
  } catch (e) { return false; }
}

function recordEmailSent_(email) {
  try {
    const props = PropertiesService.getScriptProperties();
    props.setProperty('email_sent_' + email.toLowerCase(), String(Date.now()));
  } catch (e) { Logger.log('Could not record email timestamp: ' + e.message); }
}


// ================================================================
//  SCHEDULE HEALTH SCORE (0-100)
// ================================================================

function calculateHealthScore_(filledSlots, totalSlots, loadData, unscheduledCount) {
  // Utilization component (0-40 points): higher utilization = better
  const utilPct = totalSlots > 0 ? filledSlots / totalSlots : 0;
  const utilScore = Math.round(utilPct * 40);

  // Balance component (0-30 points): even distribution = better
  const utils = CONFIG.INSTRUCTORS.map(i => (loadData[i] ? loadData[i].utilization : 0));
  const maxU = Math.max(...utils);
  const minU = Math.min(...utils);
  const spread = maxU - minU; // 0 = perfect balance, 1 = worst
  const balanceScore = Math.round((1 - spread) * 30);

  // Coverage component (0-30 points): fewer unscheduled students = better
  const coverageScore = unscheduledCount === 0 ? 30 : Math.max(0, 30 - (unscheduledCount * 3));

  return Math.min(100, utilScore + balanceScore + coverageScore);
}

function getHealthScore() {
  const settings = getSettings_();
  const isDemo = settings.demoMode;
  const today = new Date();

  const openSlots = isDemo ? DEMO.openSlots : getOpenSlots_(today, 7);
  const unscheduled = isDemo ? DEMO.students : getUnscheduledStudents_();
  const loadData = isDemo ? DEMO.loadData : getInstructorLoad_(today, 7);
  const totalSlots = CONFIG.INSTRUCTORS.length * 7 * (CONFIG.WORK_END - CONFIG.WORK_START);
  const filledSlots = totalSlots - openSlots.length;

  const score = calculateHealthScore_(filledSlots, totalSlots, loadData, unscheduled.length);
  Logger.log('Schedule Health Score: ' + score + '/100');
  return score;
}


// ================================================================
//  OPEN SLOT SCANNER
// ================================================================

function getOpenSlots_(startDate, days) {
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const bookingsSheet = ss.getSheetByName('Bookings') || ss.getSheets()[0];
  const bookingsData = bookingsSheet.getDataRange().getValues();
  if (bookingsData.length < 2) return [];

  const headers = bookingsData[0].map(h => String(h).toLowerCase().trim());
  const dateCol = findCol_(headers, ['date', 'lesson date', 'booking date']);
  const timeCol = findCol_(headers, ['time', 'lesson time', 'start time', 'time slot']);
  const instructorCol = findCol_(headers, ['instructor', 'instructor name']);
  const statusCol = findCol_(headers, ['status', 'booking status']);

  // Only active bookings occupy slots โ€” skip cancelled/no-show
  const skipStatuses = ['cancelled', 'canceled', 'no-show', 'no show', 'noshow'];

  const bookedSlots = new Set();
  for (let i = 1; i < bookingsData.length; i++) {
    const status = String(bookingsData[i][statusCol] || '').toLowerCase().trim();
    if (skipStatuses.includes(status)) continue;

    const date = bookingsData[i][dateCol];
    const time = String(bookingsData[i][timeCol] || '').trim();
    const instructor = String(bookingsData[i][instructorCol] || '').trim();

    if (date && instructor) {
      const dateStr = formatDate_(date instanceof Date ? date : new Date(date));
      // Normalize time for consistent matching
      const normalizedTime = normalizeTime_(time);
      bookedSlots.add(dateStr + '|' + normalizedTime + '|' + instructor);
    }
  }

  const openSlots = [];
  for (let d = 0; d < days; d++) {
    const date = new Date(startDate);
    date.setDate(date.getDate() + d);
    const dateStr = formatDate_(date);
    const dayName = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][date.getDay()];

    for (const instructor of CONFIG.INSTRUCTORS) {
      for (let hour = CONFIG.WORK_START; hour < CONFIG.WORK_END; hour++) {
        const timeStr = formatHour_(hour);
        const key = dateStr + '|' + timeStr + '|' + instructor;
        if (!bookedSlots.has(key)) {
          openSlots.push({ date: date, dateStr: dateStr, dayName: dayName, time: timeStr, hour: hour, instructor: instructor, key: key });
        }
      }
    }
  }
  return openSlots;
}


// ================================================================
//  UNSCHEDULED STUDENT FINDER
// ================================================================

function getUnscheduledStudents_() {
  const regSS = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  const regData = regSS.getSheets()[0].getDataRange().getValues();
  if (regData.length < 2) return [];

  const regHeaders = regData[0].map(h => String(h).toLowerCase().trim());
  const nameCol = findCol_(regHeaders, ['full name', 'name', 'student name']);
  const emailCol = findCol_(regHeaders, ['email', 'email address', 'student email']);
  const packageCol = findCol_(regHeaders, ['lesson package', 'package', 'class type', 'selected package']);
  const phoneCol = findCol_(regHeaders, ['phone', 'phone number', 'cell', 'mobile']);

  // Get booked students (by email + name)
  const bookedEmails = new Set();
  const bookedNames = new Set();

  const schedSS = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const bookingsSheet = schedSS.getSheetByName('Bookings') || schedSS.getSheets()[0];
  if (bookingsSheet) {
    const bookData = bookingsSheet.getDataRange().getValues();
    const bookHeaders = bookData[0].map(h => String(h).toLowerCase().trim());
    const bookNameCol = findCol_(bookHeaders, ['student', 'student name', 'name']);
    const bookEmailCol = findCol_(bookHeaders, ['email', 'student email']);
    const bookStatusCol = findCol_(bookHeaders, ['status', 'booking status']);

    // Skip cancelled/no-show/rescheduled โ€” these don't count as "booked"
    const skipStatuses = ['cancelled', 'canceled', 'no-show', 'no show', 'noshow', 'rescheduled'];

    for (let i = 1; i < bookData.length; i++) {
      const status = String(bookData[i][bookStatusCol] || '').toLowerCase().trim();
      if (skipStatuses.includes(status)) continue;

      const name = String(bookData[i][bookNameCol] || '').trim().toLowerCase();
      const email = String(bookData[i][bookEmailCol] || '').trim().toLowerCase();
      if (name) bookedNames.add(name);
      if (email && email.includes('@')) bookedEmails.add(email);
    }
  }

  const paymentStatus = getPaymentStatus_();

  const unscheduled = [];
  for (let i = 1; i < regData.length; i++) {
    const name = String(regData[i][nameCol] || '').trim();
    const email = String(regData[i][emailCol] || '').trim();
    const pkg = String(regData[i][packageCol] || '').trim();
    const phone = phoneCol !== -1 ? String(regData[i][phoneCol] || '').trim() : '';
    if (!name || !email) continue;

    // Match: email first, then exact name, then fuzzy
    let isBooked = false;
    if (email && bookedEmails.has(email.toLowerCase())) isBooked = true;
    else if (bookedNames.has(name.toLowerCase())) isBooked = true;
    else if (isFuzzyTracked_(name.toLowerCase(), bookedNames)) isBooked = true;

    if (!isBooked) {
      const lessonsInPackage = extractLessonCount_(pkg);
      const pStatus = paymentStatus[email.toLowerCase()] || paymentStatus[name.toLowerCase()] || {};
      unscheduled.push({
        name: name, email: email, phone: phone, package: pkg,
        totalLessons: lessonsInPackage,
        lessonsRemaining: lessonsInPackage - (pStatus.lessonsCompleted || 0),
        balance: pStatus.balance || 0,
      });
    }
  }

  unscheduled.sort((a, b) => b.lessonsRemaining - a.lessonsRemaining);
  return unscheduled;
}


// ================================================================
//  STUDENT PREFERENCES
// ================================================================

function getStudentPreferences_() {
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const prefSheet = ss.getSheetByName('Student Preferences');
  if (!prefSheet) return {};

  const data = prefSheet.getDataRange().getValues();
  if (data.length < 2) return {};

  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const nameCol = findCol_(headers, ['student name', 'name', 'student']);
  const emailCol = findCol_(headers, ['email', 'student email']);
  const instCol = findCol_(headers, ['preferred instructor', 'instructor']);
  const timeCol = findCol_(headers, ['preferred time', 'time']);
  const daysCol = findCol_(headers, ['preferred days', 'days']);

  const prefs = {};
  for (let i = 1; i < data.length; i++) {
    const name = String(data[i][nameCol] || '').trim().toLowerCase();
    const email = String(data[i][emailCol] || '').trim().toLowerCase();
    if (!name && !email) continue;

    const pref = {
      preferredInstructor: String(data[i][instCol] || '').trim(),
      preferredTime: String(data[i][timeCol] || '').trim(),
      preferredDays: String(data[i][daysCol] || '').trim(),
    };
    // Index by both name and email for flexible lookup
    if (name) prefs[name] = pref;
    if (email) prefs[email] = pref;
  }
  return prefs;
}


// ================================================================
//  INSTRUCTOR LOAD BALANCING
// ================================================================

function getInstructorLoad_(startDate, days) {
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const bookingsSheet = ss.getSheetByName('Bookings') || ss.getSheets()[0];
  const data = bookingsSheet.getDataRange().getValues();
  if (data.length < 2) {
    const load = {};
    CONFIG.INSTRUCTORS.forEach(i => { load[i] = { booked: 0, total: days * (CONFIG.WORK_END - CONFIG.WORK_START), utilization: 0 }; });
    return load;
  }

  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const dateCol = findCol_(headers, ['date', 'lesson date', 'booking date']);
  const instructorCol = findCol_(headers, ['instructor', 'instructor name']);
  const statusCol = findCol_(headers, ['status', 'booking status']);

  const endDate = new Date(startDate);
  endDate.setDate(endDate.getDate() + days);

  // For load balancing, only skip cancelled/no-show โ€” we WANT to count
  // confirmed, pending, scheduled, upcoming as they occupy slots
  const skipStatuses = ['cancelled', 'canceled', 'no-show', 'no show', 'noshow'];

  const load = {};
  CONFIG.INSTRUCTORS.forEach(i => { load[i] = { booked: 0, total: days * (CONFIG.WORK_END - CONFIG.WORK_START) }; });

  for (let i = 1; i < data.length; i++) {
    const status = String(data[i][statusCol] || '').toLowerCase().trim();
    if (skipStatuses.includes(status)) continue;

    const rawDate = data[i][dateCol];
    const date = rawDate instanceof Date ? rawDate : new Date(rawDate);
    if (isNaN(date.getTime())) continue;

    const instructor = String(data[i][instructorCol] || '').trim();

    if (date >= startDate && date <= endDate && load[instructor]) {
      load[instructor].booked++;
    }
  }

  for (const inst of CONFIG.INSTRUCTORS) {
    load[inst].utilization = load[inst].total > 0 ? load[inst].booked / load[inst].total : 0;
  }
  return load;
}


// ================================================================
//  MATCH SCORING ENGINE
// ================================================================

function scoreMatches_(openSlots, students, preferences, loadData) {
  const matches = [];

  for (const student of students) {
    // Look up preferences by name first, then email
    const pref = preferences[student.name.toLowerCase()] || preferences[student.email.toLowerCase()] || {};

    for (const slot of openSlots) {
      let score = 50;

      // Instructor preference
      if (pref.preferredInstructor) {
        score += slot.instructor.toLowerCase() === pref.preferredInstructor.toLowerCase() ? 30 : -10;
      }

      // Time preference
      if (pref.preferredTime) {
        const prefHour = parseTimePreference_(pref.preferredTime);
        if (prefHour !== null) {
          const diff = Math.abs(slot.hour - prefHour);
          if (diff <= 1) score += 20;
          else if (diff <= 2) score += 10;
          else score -= 5;
        }
      }

      // Day preference
      if (pref.preferredDays) {
        const prefDays = pref.preferredDays.toLowerCase().split(/[,\s]+/);
        if (prefDays.some(d => slot.dayName.toLowerCase().startsWith(d.substring(0, 3)))) score += 15;
      }

      // Load balancing โ€” favor underutilized instructors
      if (loadData[slot.instructor]) {
        const util = loadData[slot.instructor].utilization;
        if (util < 0.3) score += 15;
        else if (util > 0.7) score -= 10;
      }

      // Lessons remaining priority โ€” more remaining = higher urgency
      if (student.lessonsRemaining > 10) score += 10;
      else if (student.lessonsRemaining > 5) score += 5;

      // Travel buffer โ€” penalize if instructor has back-to-back (no adjacent open slot)
      if (!hasAdjacentOpenSlot_(slot, openSlots)) score -= 5;

      // Sooner = better
      if (slot.date) {
        const daysOut = Math.floor((slot.date - new Date()) / (1000 * 60 * 60 * 24));
        if (daysOut <= 1) score += 10;
        else if (daysOut <= 3) score += 5;
      }

      matches.push({ student: student, slot: slot, score: score });
    }
  }

  matches.sort((a, b) => b.score - a.score);

  // Deduplicate: best match per student, and also per slot
  const seenStudents = new Set();
  const seenSlots = new Set();
  const deduped = [];
  for (const m of matches) {
    if (!seenStudents.has(m.student.email) && !seenSlots.has(m.slot.key)) {
      seenStudents.add(m.student.email);
      seenSlots.add(m.slot.key);
      deduped.push(m);
    }
  }
  return deduped;
}

/**
 * Check if an instructor has an adjacent open slot (travel buffer).
 * If adjacent hours are also open, there's breathing room. If not,
 * it means back-to-back bookings with no buffer.
 */
function hasAdjacentOpenSlot_(slot, allOpenSlots) {
  return allOpenSlots.some(s =>
    s.instructor === slot.instructor &&
    s.dateStr === slot.dateStr &&
    s.key !== slot.key &&
    (s.hour === slot.hour - 1 || s.hour === slot.hour + 1)
  );
}


// ================================================================
//  CANCELLATION BACKFILL
// ================================================================

function handleCancellation(instructor, date, time) {
  const settings = getSettings_();
  if (!settings.autoBackfill) return;
  if (settings.demoMode) { Logger.log('โš ๏ธ DEMO: Would backfill ' + instructor + ' ' + date + ' ' + time); return; }

  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const waitSheet = ss.getSheetByName('Waitlist');
  if (!waitSheet) return;

  const data = waitSheet.getDataRange().getValues();
  const dayName = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][
    (date instanceof Date ? date : new Date(date)).getDay()
  ];

  let backfilled = 0;
  for (let i = 1; i < data.length; i++) {
    const status = String(data[i][7] || '').toLowerCase();
    if (status === 'filled' || status === 'notified') continue;

    const prefInstructor = String(data[i][3] || '').trim();
    const prefDay = String(data[i][4] || '').trim();
    const email = String(data[i][1] || '').trim();
    const name = String(data[i][0] || '').trim();

    const instructorMatch = !prefInstructor || prefInstructor.toLowerCase() === instructor.toLowerCase();
    const dayMatch = !prefDay || prefDay.toLowerCase().startsWith(dayName.toLowerCase().substring(0, 3));

    if (instructorMatch && dayMatch && email) {
      sendBackfillEmail_(name, email, instructor, date, time, settings);
      waitSheet.getRange(i + 1, 8).setValue('Notified');
      logAction_('Backfill Email', name, instructor, formatDate_(date) + ' ' + time, 'Cancellation backfill');
      backfilled++;
      if (backfilled >= 3) break;
    }
  }
  return backfilled;
}

function scanCancellations() {
  const settings = getSettings_();
  if (settings.demoMode) { Logger.log('โš ๏ธ DEMO: scanCancellations skipped.'); return; }

  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const bookingsSheet = ss.getSheetByName('Bookings') || ss.getSheets()[0];
  const data = bookingsSheet.getDataRange().getValues();
  if (data.length < 2) return;

  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const dateCol = findCol_(headers, ['date', 'lesson date', 'booking date']);
  const timeCol = findCol_(headers, ['time', 'lesson time', 'start time', 'time slot']);
  const instructorCol = findCol_(headers, ['instructor', 'instructor name']);
  const statusCol = findCol_(headers, ['status', 'booking status']);

  const today = new Date();
  const processed = new Set(); // Dedup: don't process same slot twice

  for (let i = 1; i < data.length; i++) {
    const status = String(data[i][statusCol] || '').toLowerCase();
    if (status !== 'cancelled' && status !== 'canceled') continue;

    const rawDate = data[i][dateCol];
    const date = rawDate instanceof Date ? rawDate : new Date(rawDate);
    if (isNaN(date.getTime()) || date < today) continue;

    const instructor = String(data[i][instructorCol] || '').trim();
    const time = String(data[i][timeCol] || '').trim();
    const slotKey = formatDate_(date) + '|' + instructor + '|' + time;

    if (processed.has(slotKey)) continue;
    processed.add(slotKey);

    handleCancellation(instructor, date, time);
  }
}


// ================================================================
//  PAYMENT STATUS
// ================================================================

function getPaymentStatus_() {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.PAYMENT_TRACKER_ID);
    let balSheet = ss.getSheetByName('Student Balances');
    if (!balSheet) balSheet = ss.getSheets()[0];

    const data = balSheet.getDataRange().getValues();
    if (data.length < 2) return {};

    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const nameCol = findCol_(headers, ['student', 'student name', 'name']);
    const emailCol = findCol_(headers, ['email', 'student email']);
    const balCol = findCol_(headers, ['balance', 'remaining balance', 'amount due']);
    const lessonsCol = findCol_(headers, ['lessons completed', 'completed']);

    const status = {};
    for (let i = 1; i < data.length; i++) {
      const name = String(data[i][nameCol] || '').trim().toLowerCase();
      const email = emailCol !== -1 ? String(data[i][emailCol] || '').trim().toLowerCase() : '';
      if (!name && !email) continue;
      const entry = {
        balance: parseFloat(data[i][balCol]) || 0,
        lessonsCompleted: parseInt(data[i][lessonsCol]) || 0,
      };
      if (name) status[name] = entry;
      if (email) status[email] = entry;
    }
    return status;
  } catch (e) { Logger.log('Payment status error: ' + e.message); return {}; }
}


// ================================================================
//  WEEKLY OPTIMIZATION REPORT
// ================================================================

function weeklyReport() {
  try {
    const settings = getSettings_();
    const isDemo = settings.demoMode;
    const today = new Date();

    const openSlots = isDemo ? DEMO.openSlots : getOpenSlots_(today, 7);
    const unscheduled = isDemo ? DEMO.students : getUnscheduledStudents_();
    const loadData = isDemo ? DEMO.loadData : getInstructorLoad_(today, 7);

    const totalSlots = CONFIG.INSTRUCTORS.length * 7 * (CONFIG.WORK_END - CONFIG.WORK_START);
    const filledSlots = totalSlots - openSlots.length;
    const overallUtil = ((filledSlots / totalSlots) * 100).toFixed(1);
    const healthScore = calculateHealthScore_(filledSlots, totalSlots, loadData, unscheduled.length);

    const utils = CONFIG.INSTRUCTORS.map(i => ({
      name: i,
      util: ((loadData[i] ? loadData[i].utilization : 0) * 100).toFixed(1),
      booked: loadData[i] ? loadData[i].booked : 0,
    }));

    const maxUtil = Math.max(...utils.map(u => parseFloat(u.util)));
    const minUtil = Math.min(...utils.map(u => parseFloat(u.util)));
    const imbalanced = (maxUtil - minUtil) > (CONFIG.LOAD_BALANCE_THRESHOLD * 100);

    const gapsByDay = {};
    openSlots.forEach(s => { gapsByDay[s.dayName] = (gapsByDay[s.dayName] || 0) + 1; });

    const html = buildWeeklyReportEmail_({
      overallUtil: overallUtil,
      filledSlots: filledSlots,
      totalSlots: totalSlots,
      openSlotCount: openSlots.length,
      instructors: utils,
      imbalanced: imbalanced,
      gapsByDay: gapsByDay,
      unscheduledCount: unscheduled.length,
      topUnscheduled: unscheduled.slice(0, 5),
      healthScore: healthScore,
      isDemo: isDemo,
    });

    try {
      MailApp.sendEmail({
        to: CONFIG.SCHOOL_EMAIL,
        subject: (isDemo ? '[DEMO] ' : '') + '๐Ÿ“Š Weekly Schedule Report โ€” Health: ' + healthScore + '/100 โ€” ' + CONFIG.SCHOOL_NAME,
        htmlBody: html
      });
    } catch (e) { Logger.log('Error sending weekly report: ' + e.message); }

    logAction_('Weekly Report' + (isDemo ? ' [DEMO]' : ''), '-', '-', '-',
      'Health: ' + healthScore + '/100, Util: ' + overallUtil + '%, Open: ' + openSlots.length);

  } catch (e) {
    Logger.log('โŒ weeklyReport error: ' + e.message + '\n' + e.stack);
    logAction_('ERROR', '-', '-', '-', 'weeklyReport: ' + e.message);
  }
}


// ================================================================
//  EMAIL TEMPLATES โ€” Mission Control theme + Unsubscribe
// ================================================================

function missionControlWrapper_(title, content, unsubEmail) {
  const unsubLink = unsubEmail ? '<p style="margin:8px 0 0;color:#333;font-size:10px;"><a href="mailto:' + escHtml_(unsubEmail) + '?subject=Unsubscribe%20Schedule%20Notifications" style="color:#555;text-decoration:underline;">Unsubscribe</a></p>' : '';

  return '<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"></head>' +
'<body style="margin:0;padding:0;background:#000;font-family:-apple-system,BlinkMacSystemFont,\'SF Pro Display\',sans-serif;">' +
'<table width="100%" cellpadding="0" cellspacing="0" style="background:#000;padding:20px;">' +
'<tr><td align="center">' +
'<table width="560" cellpadding="0" cellspacing="0" style="background:#0d0d0d;border-radius:20px;border:1px solid rgba(255,255,255,0.06);overflow:hidden;box-shadow:0 8px 30px rgba(0,0,0,0.5);">' +
'<tr><td style="padding:30px 40px;border-bottom:1px solid rgba(255,255,255,0.06);">' +
'<table width="100%"><tr>' +
'<td style="color:#ff2d2d;font-size:22px;font-weight:700;">โšก ' + escHtml_(title) + '</td>' +
'<td align="right" style="color:#555;font-size:11px;">' + escHtml_(CONFIG.SCHOOL_NAME) + '</td>' +
'</tr></table></td></tr>' +
'<tr><td style="padding:30px 40px;">' + content + '</td></tr>' +
'<tr><td style="padding:16px 40px;border-top:1px solid rgba(255,255,255,0.06);text-align:center;">' +
'<p style="color:#333;font-size:11px;margin:0;">Powered by Smart Scheduling Optimizer โ€ข <span style="color:#ff2d2d;">' + escHtml_(CONFIG.SCHOOL_NAME) + '</span></p>' +
unsubLink +
'</td></tr></table></td></tr></table></body></html>';
}

function sendOpeningEmail_(match, settings) {
  const s = match.student;
  const sl = match.slot;
  const unsub = settings.unsubscribeEmail || CONFIG.SCHOOL_EMAIL;
  const firstName = escHtml_(s.name.split(' ')[0]);

  const content =
    '<p style="color:#ccc;font-size:16px;line-height:1.6;">Hi <strong style="color:#fff;">' + firstName + '</strong>,</p>' +
    '<p style="color:#aaa;font-size:15px;line-height:1.6;">Great news! We have a lesson opening that might work for you:</p>' +
    '<table width="100%" style="background:rgba(255,255,255,0.03);border-radius:14px;margin:20px 0;border:1px solid rgba(255,255,255,0.06);">' +
    '<tr><td style="padding:20px;">' +
    '<p style="color:#ff2d2d;font-size:12px;text-transform:uppercase;letter-spacing:1px;margin:0 0 8px;">Available Slot</p>' +
    '<p style="color:#fff;font-size:18px;font-weight:700;margin:0;">' + escHtml_(sl.dayName) + ', ' + escHtml_(sl.dateStr) + '</p>' +
    '<p style="color:#888;font-size:14px;margin:6px 0 0;">๐Ÿ• ' + escHtml_(sl.time) + ' โ€ข ๐Ÿ‘จโ€๐Ÿซ ' + escHtml_(sl.instructor) + '</p>' +
    '</td></tr></table>' +
    '<p style="color:#aaa;font-size:14px;line-height:1.6;">You have <strong style="color:#ff2d2d;">' + s.lessonsRemaining + ' lessons remaining</strong>. Reply or call to book!</p>' +
    '<table width="100%" cellpadding="0" cellspacing="0" style="margin:20px 0;"><tr><td align="center">' +
    '<a href="mailto:' + escHtml_(CONFIG.SCHOOL_EMAIL) + '?subject=Book%20Lesson%20' + encodeURIComponent(sl.dateStr + ' ' + sl.time) + '" ' +
    'style="display:inline-block;background:linear-gradient(135deg,#ff2d2d,#cc0000);color:#fff;padding:14px 32px;border-radius:12px;text-decoration:none;font-weight:700;font-size:15px;box-shadow:0 4px 16px rgba(255,45,45,0.3);">' +
    '๐Ÿ“… Book This Slot</a></td></tr></table>';

  const html = missionControlWrapper_('Lesson Opening Available', content, unsub);

  try {
    MailApp.sendEmail({
      to: s.email,
      subject: '๐Ÿ“… Lesson Opening: ' + sl.dayName + ' ' + sl.dateStr + ' at ' + sl.time + ' โ€” ' + CONFIG.SCHOOL_NAME,
      htmlBody: html
    });
  } catch (e) { Logger.log('Error sending opening email to ' + s.email + ': ' + e.message); }
}

function sendBackfillEmail_(name, email, instructor, date, time, settings) {
  const dateStr = formatDate_(date instanceof Date ? date : new Date(date));
  const dayName = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][
    (date instanceof Date ? date : new Date(date)).getDay()
  ];
  const unsub = (settings ? settings.unsubscribeEmail : '') || CONFIG.SCHOOL_EMAIL;
  const firstName = escHtml_(name.split(' ')[0]);

  const content =
    '<p style="color:#ccc;font-size:16px;line-height:1.6;">Hi <strong style="color:#fff;">' + firstName + '</strong>,</p>' +
    '<p style="color:#aaa;font-size:15px;line-height:1.6;">A lesson slot just opened up that matches your preferences!</p>' +
    '<table width="100%" style="background:rgba(255,255,255,0.03);border-radius:14px;margin:20px 0;border:1px solid rgba(255,255,255,0.06);">' +
    '<tr><td style="padding:20px;">' +
    '<p style="color:#ff2d2d;font-size:12px;text-transform:uppercase;letter-spacing:1px;margin:0 0 8px;">Just Opened</p>' +
    '<p style="color:#fff;font-size:18px;font-weight:700;margin:0;">' + escHtml_(dayName) + ', ' + escHtml_(dateStr) + '</p>' +
    '<p style="color:#888;font-size:14px;margin:6px 0 0;">๐Ÿ• ' + escHtml_(time) + ' โ€ข ๐Ÿ‘จโ€๐Ÿซ ' + escHtml_(instructor) + '</p>' +
    '</td></tr></table>' +
    '<p style="color:#aaa;font-size:14px;line-height:1.6;">This just cancelled โ€” you\'re first on the waitlist. Reply quickly to grab it!</p>' +
    '<table width="100%" cellpadding="0" cellspacing="0" style="margin:20px 0;"><tr><td align="center">' +
    '<a href="mailto:' + escHtml_(CONFIG.SCHOOL_EMAIL) + '?subject=Book%20Cancelled%20Slot%20' + encodeURIComponent(dateStr) + '" ' +
    'style="display:inline-block;background:linear-gradient(135deg,#ff2d2d,#cc0000);color:#fff;padding:14px 32px;border-radius:12px;text-decoration:none;font-weight:700;font-size:15px;box-shadow:0 4px 16px rgba(255,45,45,0.3);">' +
    'โšก Grab This Slot</a></td></tr></table>';

  const html = missionControlWrapper_('Slot Just Opened Up!', content, unsub);

  try {
    MailApp.sendEmail({
      to: email,
      subject: 'โšก Slot Just Opened: ' + dayName + ' ' + dateStr + ' at ' + time + ' โ€” ' + CONFIG.SCHOOL_NAME,
      htmlBody: html
    });
  } catch (e) { Logger.log('Error sending backfill email to ' + email + ': ' + e.message); }
}

function buildWeeklyReportEmail_(data) {
  let instructorRows = '';
  data.instructors.forEach(function(inst) {
    const barWidth = Math.max(5, parseInt(inst.util));
    const barColor = parseFloat(inst.util) > 70 ? '#22c55e' : parseFloat(inst.util) > 40 ? '#f59e0b' : '#ff2d2d';
    instructorRows +=
      '<tr><td style="color:#fff;padding:10px 0;font-size:14px;border-bottom:1px solid rgba(255,255,255,0.04);">' + escHtml_(inst.name) + '</td>' +
      '<td style="padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.04);">' +
      '<div style="background:rgba(255,255,255,0.06);border-radius:4px;overflow:hidden;height:18px;width:100%;">' +
      '<div style="background:' + barColor + ';height:100%;width:' + barWidth + '%;border-radius:4px;"></div></div></td>' +
      '<td style="color:#888;padding:10px 0 10px 12px;font-size:13px;border-bottom:1px solid rgba(255,255,255,0.04);white-space:nowrap;">' + inst.util + '% (' + inst.booked + ')</td></tr>';
  });

  let gapsList = '';
  Object.entries(data.gapsByDay).sort(function(a,b){ return b[1]-a[1]; }).forEach(function(entry) {
    gapsList += '<li style="color:#888;padding:3px 0;">' + escHtml_(entry[0]) + ': <strong style="color:#ff2d2d;">' + entry[1] + ' open</strong></li>';
  });

  let unscheduledList = '';
  data.topUnscheduled.forEach(function(s) {
    unscheduledList += '<li style="color:#888;padding:3px 0;">' + escHtml_(s.name) + ' โ€” ' + s.lessonsRemaining + ' lessons remaining</li>';
  });

  // Health score color
  const hsColor = data.healthScore >= 75 ? '#22c55e' : data.healthScore >= 50 ? '#f59e0b' : '#ff2d2d';

  const content =
    (data.isDemo ? '<div style="background:rgba(255,45,45,0.08);border:1px solid rgba(255,45,45,0.2);border-radius:12px;padding:10px 16px;margin-bottom:20px;text-align:center;"><span style="color:#ff6b6b;font-size:12px;font-weight:700;letter-spacing:1px;text-transform:uppercase;">โœฆ Demo Mode</span></div>' : '') +

    // Health Score
    '<div style="text-align:center;margin-bottom:24px;">' +
    '<p style="color:#555;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin:0 0 4px;">Schedule Health Score</p>' +
    '<p style="color:' + hsColor + ';font-size:48px;font-weight:800;margin:0;letter-spacing:-2px;">' + data.healthScore + '</p>' +
    '<p style="color:#555;font-size:12px;margin:0;">out of 100</p></div>' +

    // Stats row
    '<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:24px;"><tr>' +
    '<td width="33%" style="padding:14px;background:rgba(255,255,255,0.03);border-radius:12px 0 0 12px;text-align:center;border:1px solid rgba(255,255,255,0.04);">' +
    '<p style="color:#ff2d2d;font-size:26px;font-weight:800;margin:0;">' + data.overallUtil + '%</p>' +
    '<p style="color:#555;font-size:10px;margin:4px 0 0;text-transform:uppercase;letter-spacing:1px;">Utilization</p></td>' +
    '<td width="33%" style="padding:14px;background:rgba(255,255,255,0.03);text-align:center;border-top:1px solid rgba(255,255,255,0.04);border-bottom:1px solid rgba(255,255,255,0.04);">' +
    '<p style="color:#22c55e;font-size:26px;font-weight:800;margin:0;">' + data.filledSlots + '</p>' +
    '<p style="color:#555;font-size:10px;margin:4px 0 0;text-transform:uppercase;letter-spacing:1px;">Booked</p></td>' +
    '<td width="33%" style="padding:14px;background:rgba(255,255,255,0.03);border-radius:0 12px 12px 0;text-align:center;border:1px solid rgba(255,255,255,0.04);">' +
    '<p style="color:#f59e0b;font-size:26px;font-weight:800;margin:0;">' + data.openSlotCount + '</p>' +
    '<p style="color:#555;font-size:10px;margin:4px 0 0;text-transform:uppercase;letter-spacing:1px;">Open Slots</p></td>' +
    '</tr></table>' +

    (data.imbalanced ? '<div style="background:rgba(255,45,45,0.06);border:1px solid rgba(255,45,45,0.2);border-radius:10px;padding:12px 16px;margin-bottom:20px;"><p style="color:#ff6b6b;margin:0;font-size:13px;">โš ๏ธ <strong>Load Imbalance</strong> โ€” Consider redistributing bookings across instructors.</p></div>' : '') +

    '<p style="color:#fff;font-size:15px;font-weight:600;margin:24px 0 12px;">๐Ÿ‘จโ€๐Ÿซ Instructor Utilization</p>' +
    '<table width="100%" cellpadding="0" cellspacing="0">' + instructorRows + '</table>' +

    '<p style="color:#fff;font-size:15px;font-weight:600;margin:24px 0 12px;">๐Ÿ“… Open Slots by Day</p>' +
    '<ul style="padding-left:20px;margin:0;">' + gapsList + '</ul>' +

    (data.unscheduledCount > 0 ?
      '<p style="color:#fff;font-size:15px;font-weight:600;margin:24px 0 12px;">๐ŸŽฏ Unscheduled Students (' + data.unscheduledCount + ')</p>' +
      '<ul style="padding-left:20px;margin:0;">' + unscheduledList + '</ul>' +
      (data.unscheduledCount > 5 ? '<p style="color:#555;font-size:12px;margin-top:6px;">...and ' + (data.unscheduledCount - 5) + ' more</p>' : '')
      : '<p style="color:#22c55e;font-size:14px;margin:24px 0;">โœ… All students are scheduled!</p>');

  return missionControlWrapper_('Weekly Schedule Report', content, null);
}


// ================================================================
//  FUZZY NAME MATCHING
// ================================================================

function fuzzyNameMatch_(name1, name2) {
  if (!name1 || !name2) return false;
  const n1 = name1.toLowerCase().replace(/\s+/g, ' ').trim();
  const n2 = name2.toLowerCase().replace(/\s+/g, ' ').trim();
  if (n1 === n2) return true;
  if (n1.includes(n2) || n2.includes(n1)) return true;

  const p1 = n1.split(' ').filter(Boolean);
  const p2 = n2.split(' ').filter(Boolean);

  if (p1.length >= 2 && p2.length >= 2) {
    // Same last name + first 3 chars of first name match
    if (p1[p1.length-1] === p2[p2.length-1] && p1[0].substring(0,3) === p2[0].substring(0,3)) return true;
    // Swapped order detection
    if (p1[0] === p2[p2.length-1] && p1[p1.length-1] === p2[0]) return true;
  }
  if (levenshtein_(n1, n2) <= 2) return true;
  return false;
}

function isFuzzyTracked_(nameKey, trackedNames) {
  for (const tracked of trackedNames) {
    if (fuzzyNameMatch_(nameKey, tracked)) return true;
  }
  return false;
}

function levenshtein_(a, b) {
  if (a.length === 0) return b.length;
  if (b.length === 0) return a.length;
  const m = [];
  for (let i = 0; i <= b.length; i++) m[i] = [i];
  for (let j = 0; j <= a.length; j++) m[0][j] = j;
  for (let i = 1; i <= b.length; i++) {
    for (let j = 1; j <= a.length; j++) {
      m[i][j] = b.charAt(i-1) === a.charAt(j-1) ? m[i-1][j-1] : Math.min(m[i-1][j-1]+1, m[i][j-1]+1, m[i-1][j]+1);
    }
  }
  return m[b.length][a.length];
}


// ================================================================
//  UTILITY FUNCTIONS
// ================================================================

/**
 * Find a column index by matching header candidates.
 * Returns -1 if no match found (callers should handle gracefully).
 */
function findCol_(headers, candidates) {
  for (const c of candidates) {
    const idx = headers.findIndex(h => h.includes(c.toLowerCase()));
    if (idx !== -1) return idx;
  }
  return -1;
}

/** HTML-escape user input for safe email insertion */
function escHtml_(str) {
  if (!str) return '';
  return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

/** Normalize time strings for consistent slot key matching */
function normalizeTime_(timeStr) {
  if (!timeStr) return '';
  const s = String(timeStr).trim().toUpperCase();
  // Try to parse "2:00 PM", "14:00", "2 PM" etc. into our standard format
  const match12 = s.match(/^(\d{1,2}):?(\d{2})?\s*(AM|PM)$/i);
  if (match12) {
    let h = parseInt(match12[1]);
    const ampm = match12[3].toUpperCase();
    if (ampm === 'PM' && h < 12) h += 12;
    if (ampm === 'AM' && h === 12) h = 0;
    return formatHour_(h);
  }
  const match24 = s.match(/^(\d{1,2}):(\d{2})$/);
  if (match24) {
    return formatHour_(parseInt(match24[1]));
  }
  return s;
}

function sanitize_(input) {
  if (!input) return '';
  return String(input).trim().replace(/[<>{}()\[\]\\\/]/g, '').replace(/\s+/g, ' ').substring(0, 200);
}

function formatDate_(date) {
  if (!(date instanceof Date)) date = new Date(date);
  return (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear();
}

function formatHour_(hour) {
  const suffix = hour >= 12 ? 'PM' : 'AM';
  const h = hour > 12 ? hour - 12 : hour === 0 ? 12 : hour;
  return h + ':00 ' + suffix;
}

function formatSlot_(slot) {
  return slot.dateStr + ' ' + slot.time;
}

function extractLessonCount_(pkg) {
  if (!pkg) return 0;
  const match = pkg.toString().match(/(\d+)\s*lesson/i);
  if (match) return parseInt(match[1]);
  // Try standalone number
  const match2 = pkg.toString().match(/^(\d+)$/);
  if (match2) return parseInt(match2[1]);
  // Common package names
  const lower = pkg.toString().toLowerCase();
  if (lower.includes('beginner')) return 10;
  if (lower.includes('standard')) return 10;
  if (lower.includes('premium')) return 15;
  if (lower.includes('intensive')) return 20;
  return 0;
}

function parseTimePreference_(timePref) {
  if (!timePref) return null;
  const t = timePref.toLowerCase();
  if (t.includes('morning') || t.includes('early')) return 9;
  if (t.includes('midday') || t.includes('noon') || t.includes('lunch')) return 12;
  if (t.includes('afternoon')) return 14;
  if (t.includes('evening') || t.includes('late')) return 16;
  const match = t.match(/(\d{1,2})/);
  if (match) {
    let h = parseInt(match[1]);
    if (t.includes('pm') && h < 12) h += 12;
    return h;
  }
  return null;
}

function getSettings_() {
  const d = {
    demoMode: CONFIG.DEMO_MODE,
    autoSendOpenings: true,
    autoBackfill: true,
    maxEmails: CONFIG.MAX_EMAILS_PER_RUN,
    unsubscribeEmail: CONFIG.SCHOOL_EMAIL,
    smsEnabled: false,
    emailCooldownHours: CONFIG.EMAIL_COOLDOWN_HOURS,
  };

  try {
    const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
    const sheet = ss.getSheetByName('Optimizer Settings');
    if (!sheet) return d;

    const data = sheet.getDataRange().getValues();
    for (let i = 1; i < data.length; i++) {
      const key = String(data[i][0]).toLowerCase().trim();
      const val = String(data[i][1]).trim();

      if (key.includes('demo mode')) d.demoMode = val.toLowerCase() === 'yes';
      else if (key.includes('max emails')) d.maxEmails = parseInt(val) || 20;
      else if (key.includes('auto-send') || key.includes('opening email')) d.autoSendOpenings = val.toLowerCase() === 'yes';
      else if (key.includes('auto-backfill') || key.includes('backfill cancel')) d.autoBackfill = val.toLowerCase() === 'yes';
      else if (key.includes('unsubscribe')) d.unsubscribeEmail = val;
      else if (key.includes('sms')) d.smsEnabled = val.toLowerCase() === 'yes';
      else if (key.includes('cooldown')) d.emailCooldownHours = parseInt(val) || 48;
    }
  } catch (e) { Logger.log('Settings error: ' + e.message); }
  return d;
}

function logAction_(action, student, instructor, slot, details) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
    const logSheet = ss.getSheetByName('Optimization Log');
    if (logSheet) logSheet.appendRow([new Date(), action, student, instructor, slot, details]);
  } catch (e) { Logger.log('Log error: ' + e.message); }
}


// ================================================================
//  MANUAL FUNCTIONS
// ================================================================

function addToWaitlist(name, email, phone, preferredInstructor, preferredDay, preferredTime) {
  name = sanitize_(name);
  email = sanitize_(email);
  phone = sanitize_(phone);
  preferredInstructor = sanitize_(preferredInstructor);
  preferredDay = sanitize_(preferredDay);
  preferredTime = sanitize_(preferredTime);

  if (!name || !email) { Logger.log('Name and email required.'); return; }

  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  const sheet = ss.getSheetByName('Waitlist');
  if (!sheet) { Logger.log('Waitlist sheet not found'); return; }

  sheet.appendRow([name, email, phone, preferredInstructor, preferredDay, preferredTime, new Date(), 'Waiting']);
  Logger.log('โœ… Added ' + name + ' to waitlist');
}

function runOptimizationNow() {
  dailyOptimize();
  scanCancellations();
  Logger.log('โœ… Manual optimization complete');
}

function previewMatches() {
  const settings = getSettings_();
  const isDemo = settings.demoMode;
  const today = new Date();

  const openSlots = isDemo ? DEMO.openSlots : getOpenSlots_(today, 7);
  const unscheduled = isDemo ? DEMO.students : getUnscheduledStudents_();
  const preferences = isDemo ? DEMO.preferences : getStudentPreferences_();
  const loadData = isDemo ? DEMO.loadData : getInstructorLoad_(today, 7);

  Logger.log('Open slots: ' + openSlots.length);
  Logger.log('Unscheduled students: ' + unscheduled.length);

  const matches = scoreMatches_(openSlots, unscheduled, preferences, loadData);
  matches.slice(0, 10).forEach(function(m, i) {
    Logger.log((i+1) + '. ' + m.student.name + ' โ†’ ' + m.slot.instructor + ' on ' + m.slot.dateStr + ' at ' + m.slot.time + ' (score: ' + m.score + ')');
  });

  Logger.log('\nInstructor Load:');
  Object.entries(loadData).forEach(function(entry) {
    Logger.log('  ' + entry[0] + ': ' + (entry[1].utilization * 100).toFixed(1) + '% (' + entry[1].booked + ' booked)');
  });

  const totalSlots = CONFIG.INSTRUCTORS.length * 7 * (CONFIG.WORK_END - CONFIG.WORK_START);
  const healthScore = calculateHealthScore_(totalSlots - openSlots.length, totalSlots, loadData, unscheduled.length);
  Logger.log('\n๐Ÿฅ Schedule Health Score: ' + healthScore + '/100');
}
๐Ÿ† Student Certificate Generator 40KB ยท 882 linesGSโ–ถ
/**
 * =========================================================
 *  STUDENT CERTIFICATE GENERATOR โ€” Cherry on Top #3
 *  Flavors Driving School
 * =========================================================
 *  Auto-generates premium branded PDF certificates when
 *  students complete their lesson package.
 *
 *  - Detects completion from Schedule Board attendance
 *  - Generates a stunning certificate PDF (light theme โ€” Google PDF compatible)
 *  - Emails it to the student with a congrats message
 *  - Logs every certificate issued
 *  - Daily check at 6 PM (after lessons wrap up)
 *  - Certificate verification web page (doGet)
 *  - Demo mode for safe presentations
 * =========================================================
 */

const CONFIG = {
  // โ”€โ”€ Sheet IDs โ”€โ”€
  SCHEDULE_BOARD_ID: '1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE',
  REGISTRATION_ID:   '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',

  // โ”€โ”€ Branding โ”€โ”€
  SCHOOL_NAME:    'Flavors Driving School',
  SCHOOL_TAGLINE: 'Your Road to Freedom Starts Here',
  ADMIN_EMAIL:    '[email protected]',
  CERT_FOLDER_NAME: 'Flavors โ€” Student Certificates',

  // โ”€โ”€ Sheet tabs โ”€โ”€
  REGISTRATION_SHEET_TAB: '',
  BOOKINGS_SHEET_TAB:     'Bookings',

  // โ”€โ”€ Demo mode (true = no real emails/PDFs, fake data for presentations) โ”€โ”€
  DEMO_MODE: true,

  // โ”€โ”€ Statuses that count as confirmed attendance โ”€โ”€
  CONFIRMED_STATUSES: ['completed', 'on time', 'late', 'present', 'attended'],
  SKIP_STATUSES:      ['cancelled', 'canceled', 'no-show', 'no show', 'pending', 'scheduled', 'upcoming', 'rescheduled'],

  // โ”€โ”€ Named packages โ†’ lesson counts โ”€โ”€
  NAMED_PACKAGES: {
    'beginner':  10,
    'standard':  10,
    'premium':   15,
    'intensive': 20
  }
};

/* ================================================================
   DEMO DATA
   ================================================================ */
const DEMO_DATA = {
  completedStudents: [
    { name: 'Sarah Johnson',   email: '[email protected]',   pkg: 'Premium Package',   lessons: 15, instructor: 'Anisha' },
    { name: 'Marcus Williams', email: '[email protected]',  pkg: '10 Lessons',         lessons: 10, instructor: 'Carlos' }
  ],
  recentCerts: [
    { certNum: 'FDS-260219-A7K2', name: 'Sarah Johnson',   email: '[email protected]',   pkg: 'Premium Package', lessons: 15, instructor: 'Anisha',  date: 'February 19, 2026', url: '#', sent: 'Yes' },
    { certNum: 'FDS-260218-B3M9', name: 'Marcus Williams',  email: '[email protected]',  pkg: '10 Lessons',      lessons: 10, instructor: 'Carlos',  date: 'February 18, 2026', url: '#', sent: 'Yes' },
    { certNum: 'FDS-260215-C5P4', name: 'Emily Chen',       email: '[email protected]',   pkg: 'Beginner Package', lessons: 10, instructor: 'Nick',   date: 'February 15, 2026', url: '#', sent: 'Yes' }
  ]
};

/* ================================================================
   SETUP & AUTH
   ================================================================ */

/** Run once to trigger OAuth for all connected services. */
function forceAuth() {
  SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID).getSheetByName('test_auth_ignore');
  SpreadsheetApp.openById(CONFIG.REGISTRATION_ID).getSheetByName('test_auth_ignore');
  DriveApp.getRootFolder();
  MailApp.getRemainingDailyQuota();
  Logger.log('โœ… All permissions authorized. You can close this now.');
}

function fullSetup() {
  const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);

  // โ”€โ”€ Create "Certificates Issued" sheet โ”€โ”€
  let certSheet = ss.getSheetByName('Certificates Issued');
  if (!certSheet) {
    certSheet = ss.insertSheet('Certificates Issued');
    certSheet.appendRow([
      'Certificate #', 'Student Name', 'Email', 'Phone', 'Package', 'Lessons Completed',
      'Primary Instructor', 'Issue Date', 'PDF Link', 'Email Sent'
    ]);
    certSheet.getRange('1:1').setFontWeight('bold');
    certSheet.setFrozenRows(1);
    Logger.log('โœ… Created "Certificates Issued" sheet.');
  }

  // โ”€โ”€ Create Drive folder โ”€โ”€
  const folders = DriveApp.getFoldersByName(CONFIG.CERT_FOLDER_NAME);
  if (!folders.hasNext()) {
    DriveApp.createFolder(CONFIG.CERT_FOLDER_NAME);
    Logger.log('โœ… Created certificate folder in Drive.');
  }

  // โ”€โ”€ Daily trigger (delete old first to avoid duplicates) โ”€โ”€
  ScriptApp.getProjectTriggers().forEach(t => {
    if (t.getHandlerFunction() === 'dailyCertificateCheck') {
      ScriptApp.deleteTrigger(t);
    }
  });
  ScriptApp.newTrigger('dailyCertificateCheck')
    .timeBased()
    .everyDays(1)
    .atHour(18)
    .create();
  Logger.log('โœ… Daily trigger set for 6 PM.');

  Logger.log('โœ… Certificate Generator setup complete.');
}

/* ================================================================
   WEB APP โ€” CERTIFICATE VERIFICATION PAGE
   ================================================================ */

function doGet(e) {
  const certId = String(e && e.parameter && e.parameter.id ? e.parameter.id : '').trim();
  const html = getVerificationHTML_(certId);
  return HtmlService.createHtmlOutput(html)
    .setTitle('Verify Certificate โ€” ' + CONFIG.SCHOOL_NAME)
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function verifyCertificate(certId) {
  const id = String(certId || '').trim().toUpperCase();
  if (!id) return { found: false, error: 'Please enter a certificate number.' };

  if (CONFIG.DEMO_MODE) {
    const demo = DEMO_DATA.recentCerts.find(c => c.certNum.toUpperCase() === id);
    if (demo) {
      return {
        found: true,
        name: demo.name,
        pkg: demo.pkg,
        lessons: demo.lessons,
        instructor: demo.instructor,
        date: demo.date,
        certNum: demo.certNum
      };
    }
    return { found: false };
  }

  try {
    const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
    const certSheet = ss.getSheetByName('Certificates Issued');
    if (!certSheet || certSheet.getLastRow() <= 1) return { found: false };

    const data = certSheet.getDataRange().getValues();
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const cNumCol  = findCol_(headers, ['certificate #', 'cert #', 'certificate']);
    const cNameCol = findCol_(headers, ['student name', 'name']);
    const cPkgCol  = findCol_(headers, ['package']);
    const cLessCol = findCol_(headers, ['lessons completed', 'lessons']);
    const cInstCol = findCol_(headers, ['primary instructor', 'instructor']);
    const cDateCol = findCol_(headers, ['issue date', 'date']);

    for (let r = 1; r < data.length; r++) {
      const row = data[r];
      if (String(row[cNumCol] || '').trim().toUpperCase() === id) {
        return {
          found: true,
          name: String(row[cNameCol] || ''),
          pkg: String(row[cPkgCol] || ''),
          lessons: row[cLessCol] || 0,
          instructor: String(row[cInstCol] || ''),
          date: cDateCol >= 0 && row[cDateCol] ? Utilities.formatDate(new Date(row[cDateCol]), 'America/New_York', 'MMMM d, yyyy') : '',
          certNum: String(row[cNumCol] || '')
        };
      }
    }
    return { found: false };
  } catch (err) {
    Logger.log('verifyCertificate error: ' + err.message);
    return { found: false, error: 'Lookup failed. Please try again.' };
  }
}

/* ================================================================
   DAILY CHECK
   ================================================================ */

function dailyCertificateCheck() {
  try {
    if (CONFIG.DEMO_MODE) {
      Logger.log('๐ŸŽญ DEMO MODE โ€” skipping real certificate check.');
      Logger.log('Demo would generate certificates for: ' + DEMO_DATA.completedStudents.map(s => s.name).join(', '));
      return;
    }
    runDailyCertificateCheck_();
  } catch (e) {
    Logger.log('dailyCertificateCheck error: ' + (e.message || e.toString()));
    try {
      if (CONFIG.ADMIN_EMAIL) {
        MailApp.sendEmail(CONFIG.ADMIN_EMAIL,
          'Certificate Generator โ€” Daily check failed',
          'Error: ' + String(e.message || e.toString()).substring(0, 500)
        );
      }
    } catch (mailErr) {
      Logger.log('Could not send error email: ' + mailErr.message);
    }
    throw e;
  }
}

function runDailyCertificateCheck_() {
  const regSheet = getRegistrationSheet_();
  if (!regSheet || regSheet.getLastRow() <= 1) return;

  const regData = regSheet.getDataRange().getValues();
  const regHeaders = regData[0].map(h => String(h).toLowerCase().trim());

  const nameCol  = findCol_(regHeaders, ['student name', 'full name', 'name']);
  const emailCol = findCol_(regHeaders, ['email', 'student email', 'email address']);
  const pkgCol   = findCol_(regHeaders, ['package', 'lesson package', 'lessons']);
  const phoneCol = findCol_(regHeaders, ['phone', 'phone number', 'mobile', 'cell']);
  if (nameCol < 0 || emailCol < 0) return;

  const schedSheet = getScheduleSheet_();
  if (!schedSheet || schedSheet.getLastRow() <= 1) return;

  const schedData = schedSheet.getDataRange().getValues();
  const schedHeaders = schedData[0].map(h => String(h).toLowerCase().trim());

  const sNameCol   = findCol_(schedHeaders, ['student', 'student name', 'name']);
  const sEmailCol  = findCol_(schedHeaders, ['email', 'student email']);
  const sInstCol   = findCol_(schedHeaders, ['instructor', 'instructor name']);
  const sStatusCol = findCol_(schedHeaders, ['status', 'attendance', 'attended']);
  const sDateCol   = findCol_(schedHeaders, ['date', 'lesson date', 'scheduled date']);
  if (sNameCol < 0) return;

  const regSS = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  let certSheet = regSS.getSheetByName('Certificates Issued');
  if (!certSheet) {
    fullSetup();
    certSheet = regSS.getSheetByName('Certificates Issued');
  }
  if (!certSheet) return;

  // โ”€โ”€ Already-issued set (by email + name) โ”€โ”€
  const certData = certSheet.getDataRange().getValues();
  const issuedSet = new Set();
  for (let r = 1; r < certData.length; r++) {
    issuedSet.add(String(certData[r][1] || '').toLowerCase().trim());
    if (certData[r][2]) issuedSet.add(String(certData[r][2]).toLowerCase().trim());
  }

  // โ”€โ”€ Count confirmed lessons per student (email-first + name fallback) โ”€โ”€
  const now = new Date();
  const lessonsByEmail = {};
  const lessonsByName  = {};
  const instructorByEmail = {};
  const instructorByName  = {};

  for (let i = 1; i < schedData.length; i++) {
    const row = schedData[i];
    const status = String(row[sStatusCol] != null ? row[sStatusCol] : '').toLowerCase().trim();
    if (!isConfirmedStatus_(status)) continue;

    // Only count past lessons
    if (sDateCol >= 0 && row[sDateCol]) {
      const lessonDate = new Date(row[sDateCol]);
      if (!isNaN(lessonDate.getTime()) && lessonDate > now) continue;
    }

    const student = String(row[sNameCol] || '').trim();
    const sEmail  = sEmailCol >= 0 ? String(row[sEmailCol] || '').trim().toLowerCase() : '';
    const instructor = String(row[sInstCol] != null ? row[sInstCol] : '').trim() || 'Flavors Team';

    if (sEmail) {
      lessonsByEmail[sEmail] = (lessonsByEmail[sEmail] || 0) + 1;
      if (!instructorByEmail[sEmail]) instructorByEmail[sEmail] = {};
      instructorByEmail[sEmail][instructor] = (instructorByEmail[sEmail][instructor] || 0) + 1;
    }
    if (student) {
      const nk = student.toLowerCase();
      lessonsByName[nk] = (lessonsByName[nk] || 0) + 1;
      if (!instructorByName[nk]) instructorByName[nk] = {};
      instructorByName[nk][instructor] = (instructorByName[nk][instructor] || 0) + 1;
    }
  }

  // โ”€โ”€ Check each registered student โ”€โ”€
  let generated = 0;
  for (let i = 1; i < regData.length; i++) {
    const row = regData[i];
    const name  = String(row[nameCol] != null ? row[nameCol] : '').trim();
    const email = String(row[emailCol] != null ? row[emailCol] : '').trim();
    const pkg   = String(row[pkgCol] != null ? row[pkgCol] : '');
    const phone = phoneCol >= 0 ? String(row[phoneCol] || '') : '';

    if (!name || !email) continue;

    // Already issued? Check both email and name
    if (issuedSet.has(email.toLowerCase()) || issuedSet.has(name.toLowerCase())) continue;

    const required = extractLessonCount_(pkg);
    if (required == null) continue;

    // Email-first matching, then fuzzy name fallback
    const emailKey = email.toLowerCase();
    let completed = lessonsByEmail[emailKey] || 0;
    let instMap   = instructorByEmail[emailKey] || {};

    if (completed === 0) {
      // Fuzzy name match
      const match = fuzzyFindStudent_(name, Object.keys(lessonsByName));
      if (match) {
        completed = lessonsByName[match] || 0;
        instMap   = instructorByName[match] || {};
      }
    }

    if (completed >= required) {
      const instructor = getPrimaryInstructor_(instMap);
      const certNum = generateCertNumber_(certSheet);
      const pdf = generateCertificatePDF_(name, pkg, completed, instructor, certNum);
      const sent = sendCertificateEmail_(name, email, pkg, instructor, certNum, pdf);

      certSheet.appendRow([
        certNum, name, email, phone, pkg, completed,
        instructor, new Date(), pdf.getUrl(), sent ? 'Yes' : 'Failed'
      ]);
      generated++;
      issuedSet.add(emailKey);
      issuedSet.add(name.toLowerCase());
      Logger.log('โœ… Certificate issued: ' + name + ' (#' + certNum + ')');
    }
  }

  Logger.log(generated === 0
    ? 'No new completions found today.'
    : generated + ' certificate(s) generated and sent.');
}

/* ================================================================
   RE-SEND CERTIFICATE
   ================================================================ */

/** Re-send a certificate by cert number. Looks up the student and re-emails the PDF. */
function resendCertificate(certNumber) {
  const id = String(certNumber || '').trim().toUpperCase();
  if (!id) { Logger.log('No cert number provided.'); return; }

  const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  const certSheet = ss.getSheetByName('Certificates Issued');
  if (!certSheet || certSheet.getLastRow() <= 1) { Logger.log('No certificates found.'); return; }

  const data = certSheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h).toLowerCase().trim());
  const cNumCol  = findCol_(headers, ['certificate #', 'cert #']);
  const cNameCol = findCol_(headers, ['student name', 'name']);
  const cEmailCol = findCol_(headers, ['email']);
  const cPkgCol  = findCol_(headers, ['package']);
  const cInstCol = findCol_(headers, ['primary instructor', 'instructor']);
  const cUrlCol  = findCol_(headers, ['pdf link', 'pdf url', 'link']);

  for (let r = 1; r < data.length; r++) {
    if (String(data[r][cNumCol] || '').trim().toUpperCase() !== id) continue;

    const name  = String(data[r][cNameCol] || '');
    const email = String(data[r][cEmailCol] || '');
    const pkg   = String(data[r][cPkgCol] || '');
    const inst  = String(data[r][cInstCol] || 'Flavors Team');
    const pdfUrl = String(data[r][cUrlCol] || '');

    if (!email) { Logger.log('No email on file for ' + name); return; }

    // Try to get existing PDF from Drive
    let pdfFile = null;
    if (pdfUrl) {
      try {
        const match = pdfUrl.match(/[-\w]{25,}/);
        if (match) pdfFile = DriveApp.getFileById(match[0]);
      } catch (_) { /* regenerate */ }
    }

    // Regenerate if we can't find the original
    if (!pdfFile) {
      Logger.log('Original PDF not found, regenerating...');
      pdfFile = generateCertificatePDF_(name, pkg, 0, inst, id);
    }

    const sent = sendCertificateEmail_(name, email, pkg, inst, id, pdfFile);
    Logger.log('Re-send to ' + name + ' (' + email + '): ' + (sent ? 'Success' : 'Failed'));
    return;
  }

  Logger.log('Certificate ' + id + ' not found.');
}

/* ================================================================
   SHEET HELPERS
   ================================================================ */

function getRegistrationSheet_() {
  const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  if (CONFIG.REGISTRATION_SHEET_TAB) {
    const sheet = ss.getSheetByName(CONFIG.REGISTRATION_SHEET_TAB);
    if (sheet) return sheet;
  }
  return ss.getSheets()[0];
}

function getScheduleSheet_() {
  const ss = SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID);
  if (CONFIG.BOOKINGS_SHEET_TAB) {
    const sheet = ss.getSheetByName(CONFIG.BOOKINGS_SHEET_TAB);
    if (sheet) return sheet;
  }
  return ss.getSheets()[0];
}

/* ================================================================
   STUDENT MATCHING (email-first + fuzzy name)
   ================================================================ */

function isConfirmedStatus_(status) {
  const s = String(status || '').toLowerCase().trim();
  if (CONFIG.SKIP_STATUSES.some(skip => s === skip)) return false;
  return CONFIG.CONFIRMED_STATUSES.some(ok => s === ok);
}

function fuzzyFindStudent_(targetName, nameKeys) {
  const tName = String(targetName || '').toLowerCase().trim();
  if (!tName) return null;

  // Exact match first
  if (nameKeys.includes(tName)) return tName;

  // Fuzzy: Levenshtein โ‰ค 2, first 3 chars of first name + same last name, swapped order
  const tParts = tName.split(/\s+/);
  const tFirst = tParts[0] || '';
  const tLast  = tParts[tParts.length - 1] || '';

  for (const key of nameKeys) {
    const kParts = key.split(/\s+/);
    const kFirst = kParts[0] || '';
    const kLast  = kParts[kParts.length - 1] || '';

    // Levenshtein distance โ‰ค 2
    if (levenshtein_(tName, key) <= 2) return key;

    // First 3 chars of first name + same last name
    if (tFirst.length >= 3 && kFirst.length >= 3 &&
        tFirst.substring(0, 3) === kFirst.substring(0, 3) &&
        tLast === kLast) return key;

    // Swapped first/last
    if (tFirst === kLast && tLast === kFirst) return key;
  }
  return null;
}

function levenshtein_(a, b) {
  const m = a.length, n = b.length;
  const dp = Array.from({ length: m + 1 }, (_, i) => i);
  for (let j = 1; j <= n; j++) {
    let prev = dp[0];
    dp[0] = j;
    for (let i = 1; i <= m; i++) {
      const temp = dp[i];
      dp[i] = a[i - 1] === b[j - 1]
        ? prev
        : 1 + Math.min(prev, dp[i], dp[i - 1]);
      prev = temp;
    }
  }
  return dp[m];
}

function getPrimaryInstructor_(instMap) {
  if (!instMap) return 'Flavors Team';
  const entries = Object.entries(instMap).filter(e => e[0] && e[0].trim());
  if (entries.length === 0) return 'Flavors Team';
  entries.sort((a, b) => b[1] - a[1]);
  return entries[0][0] || 'Flavors Team';
}

/* ================================================================
   LESSON COUNT PARSING
   ================================================================ */

function extractLessonCount_(pkg) {
  const s = String(pkg || '').toLowerCase().trim();
  if (!s) return null;

  // 5-Hour Class = 1 completion (not driving lessons)
  if (s.includes('5-hour') || s.includes('5 hour')) return 1;

  // Named packages
  for (const [name, count] of Object.entries(CONFIG.NAMED_PACKAGES)) {
    if (s.includes(name)) return count;
  }

  // Numeric extraction
  const numMatch = s.match(/(\d+)/);
  if (numMatch) {
    const n = parseInt(numMatch[1], 10);
    if (n > 0 && n <= 100) return n;
  }

  return null;
}

/* ================================================================
   CERTIFICATE NUMBER
   ================================================================ */

function generateCertNumber_(certSheet) {
  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
  const datePart = Utilities.formatDate(new Date(), 'America/New_York', 'yyMMdd');

  const existing = new Set();
  if (certSheet && certSheet.getLastRow() >= 1) {
    const data = certSheet.getRange(1, 1, certSheet.getLastRow(), 1).getValues();
    for (let r = 0; r < data.length; r++) existing.add(String(data[r][0]));
  }

  for (let tries = 0; tries < 50; tries++) {
    let code = 'FDS-' + datePart + '-';
    for (let i = 0; i < 4; i++) code += chars[Math.floor(Math.random() * chars.length)];
    if (!existing.has(code)) return code;
  }

  // Fallback with timestamp
  return 'FDS-' + datePart + '-' + Date.now().toString(36).toUpperCase().slice(-4);
}

/* ================================================================
   PDF GENERATION (light theme โ€” Google PDF compatible)
   ================================================================ */

function generateCertificatePDF_(studentName, pkg, lessonsCompleted, instructor, certNum) {
  const issueDate = Utilities.formatDate(new Date(), 'America/New_York', 'MMMM d, yyyy');
  const schoolName = esc_(CONFIG.SCHOOL_NAME);
  const tagline    = esc_(CONFIG.SCHOOL_TAGLINE);
  const name       = esc_(studentName);
  const pkgSafe    = esc_(pkg);
  const inst       = esc_(instructor || 'Flavors Team');
  const num        = esc_(certNum);
  const issue      = esc_(issueDate);
  const lessonsTxt = esc_(String(lessonsCompleted));

  const html = '<!DOCTYPE html><html><head><meta charset="utf-8">'
    + '<style>@page{size:11in 8.5in;margin:0}*{margin:0;padding:0}'
    + 'html,body{margin:0;padding:0;width:11in;height:8.5in;overflow:hidden;font-family:Georgia,"Times New Roman",serif}</style>'
    + '</head><body>'
    + '<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#faf8f5;height:8.5in;">'
    + '<tr><td align="center" valign="middle" style="padding:14px;">'
    + '<table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0" style="border:3px solid #8B0000;">'
    + '<tr><td style="padding:4px;">'
    + '<table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0" style="border:1px solid #C8A96E;">'
    + '<tr><td style="padding:3px;">'
    + '<table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0" style="border:1px solid #8B0000;">'
    + '<tr><td align="center" valign="middle" style="padding:16px 40px;">'
    + '<table width="100%" cellpadding="0" cellspacing="0" border="0">'
    // โ”€โ”€ Top ornament โ”€โ”€
    + '<tr><td align="center" style="padding-bottom:4px;"><table cellpadding="0" cellspacing="0" border="0"><tr>'
    + '<td style="width:100px;height:2px;background-color:#C8A96E;"></td>'
    + '<td style="padding:0 12px;color:#8B0000;font-size:18px;">&#10038;</td>'
    + '<td style="width:60px;height:2px;background-color:#8B0000;"></td>'
    + '<td style="padding:0 8px;color:#C8A96E;font-size:22px;">&#9733;</td>'
    + '<td style="width:60px;height:2px;background-color:#8B0000;"></td>'
    + '<td style="padding:0 12px;color:#8B0000;font-size:18px;">&#10038;</td>'
    + '<td style="width:100px;height:2px;background-color:#C8A96E;"></td>'
    + '</tr></table></td></tr>'
    // โ”€โ”€ Title โ”€โ”€
    + '<tr><td align="center" style="padding:2px 0;"><span style="font-size:34px;font-weight:bold;color:#8B0000;letter-spacing:6px;">CERTIFICATE</span></td></tr>'
    + '<tr><td align="center" style="padding-bottom:2px;"><span style="font-size:14px;color:#666;letter-spacing:4px;font-style:italic;">of Completion</span></td></tr>'
    // โ”€โ”€ Diamond divider โ”€โ”€
    + '<tr><td align="center" style="padding:4px 0;"><table cellpadding="0" cellspacing="0" border="0"><tr>'
    + '<td style="width:80px;height:1px;background-color:#C8A96E;"></td>'
    + '<td style="padding:0 10px;color:#C8A96E;font-size:10px;">&#9830; &#9830; &#9830;</td>'
    + '<td style="width:80px;height:1px;background-color:#C8A96E;"></td>'
    + '</tr></table></td></tr>'
    // โ”€โ”€ School name & tagline โ”€โ”€
    + '<tr><td align="center" style="padding:4px 0 1px;"><span style="font-size:20px;color:#333;font-weight:bold;letter-spacing:2px;">' + schoolName + '</span></td></tr>'
    + '<tr><td align="center" style="padding-bottom:6px;"><span style="font-size:10px;color:#999;font-style:italic;letter-spacing:1px;">' + tagline + '</span></td></tr>'
    // โ”€โ”€ Awarded to โ”€โ”€
    + '<tr><td align="center" style="padding-bottom:2px;"><span style="font-family:Arial,Helvetica,sans-serif;font-size:10px;color:#999;letter-spacing:4px;text-transform:uppercase;">THIS CERTIFICATE IS PROUDLY AWARDED TO</span></td></tr>'
    + '<tr><td align="center" style="padding:4px 0 2px;"><span style="font-size:42px;color:#1a1a1a;font-weight:bold;font-style:italic;">' + name + '</span></td></tr>'
    // โ”€โ”€ Name underline โ”€โ”€
    + '<tr><td align="center" style="padding-bottom:6px;"><table cellpadding="0" cellspacing="0" border="0"><tr>'
    + '<td style="width:40px;height:1px;background-color:#C8A96E;"></td>'
    + '<td style="padding:0 6px;"><table cellpadding="0" cellspacing="0" border="0"><tr><td style="width:6px;height:6px;background-color:#8B0000;"></td></tr></table></td>'
    + '<td style="width:240px;height:2px;background-color:#8B0000;"></td>'
    + '<td style="padding:0 6px;"><table cellpadding="0" cellspacing="0" border="0"><tr><td style="width:6px;height:6px;background-color:#8B0000;"></td></tr></table></td>'
    + '<td style="width:40px;height:1px;background-color:#C8A96E;"></td>'
    + '</tr></table></td></tr>'
    // โ”€โ”€ Body text โ”€โ”€
    + '<tr><td align="center" style="padding:0 70px 10px;"><span style="font-size:12px;color:#555;line-height:19px;">'
    + 'For successfully completing <span style="color:#8B0000;font-weight:bold;">' + lessonsTxt + ' driving lessons</span> in the '
    + '<span style="color:#8B0000;font-weight:bold;">' + pkgSafe + '</span> program. '
    + 'Your dedication to becoming a safe and confident driver is commendable. The open road awaits &mdash; drive with pride.</span></td></tr>'
    // โ”€โ”€ Star divider โ”€โ”€
    + '<tr><td align="center" style="padding-bottom:10px;"><table cellpadding="0" cellspacing="0" border="0"><tr>'
    + '<td style="width:180px;height:1px;background-color:#e0d5c5;"></td>'
    + '<td style="padding:0 10px;color:#C8A96E;font-size:12px;">&#9733;</td>'
    + '<td style="width:180px;height:1px;background-color:#e0d5c5;"></td>'
    + '</tr></table></td></tr>'
    // โ”€โ”€ Signature row โ”€โ”€
    + '<tr><td align="center"><table width="92%" cellpadding="0" cellspacing="0" border="0"><tr>'
    + signatureCell_(inst, 'PRIMARY INSTRUCTOR')
    + signatureCell_(issue, 'DATE ISSUED')
    + signatureCell_(schoolName, 'AUTHORIZED BY')
    + '</tr></table></td></tr>'
    // โ”€โ”€ Footer row โ”€โ”€
    + '<tr><td align="center" style="padding-top:10px;"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr>'
    + '<td width="30%" align="left" style="font-family:Arial,sans-serif;font-size:7px;color:#ccc;letter-spacing:1px;">' + num + '</td>'
    + '<td width="40%" align="center"><table cellpadding="0" cellspacing="0" border="0"><tr>'
    + '<td style="width:30px;height:1px;background-color:#C8A96E;"></td>'
    + '<td style="padding:0 8px;color:#8B0000;font-size:14px;">&#10038;</td>'
    + '<td style="width:20px;height:1px;background-color:#8B0000;"></td>'
    + '<td style="padding:0 6px;color:#C8A96E;font-size:16px;">&#9733;</td>'
    + '<td style="width:20px;height:1px;background-color:#8B0000;"></td>'
    + '<td style="padding:0 8px;color:#8B0000;font-size:14px;">&#10038;</td>'
    + '<td style="width:30px;height:1px;background-color:#C8A96E;"></td>'
    + '</tr></table></td>'
    + '<td width="30%" align="right" style="font-family:Arial,sans-serif;font-size:7px;color:#ccc;letter-spacing:1px;">EST. 2024</td>'
    + '</tr></table></td></tr>'
    + '</table>'
    + '</td></tr></table></td></tr></table></td></tr></table></td></tr></table>'
    + '</body></html>';

  const blob = Utilities.newBlob(html, 'text/html', 'cert.html');
  const tempFile = DriveApp.createFile(blob);
  const pdfBlob = tempFile.getAs('application/pdf');
  tempFile.setTrashed(true);

  const safeName = (CONFIG.SCHOOL_NAME + ' โ€” Certificate โ€” ' + studentName + '.pdf').replace(/[<>:"/\\|?*]/g, '');
  pdfBlob.setName(safeName);

  let folder = null;
  const folders = DriveApp.getFoldersByName(CONFIG.CERT_FOLDER_NAME);
  if (folders.hasNext()) folder = folders.next();
  else folder = DriveApp.createFolder(CONFIG.CERT_FOLDER_NAME);

  const pdfFile = folder.createFile(pdfBlob);
  pdfFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
  return pdfFile;
}

function signatureCell_(value, label) {
  return '<td width="33%" align="center" valign="top" style="padding:0 8px;">'
    + '<table cellpadding="0" cellspacing="0" border="0" width="100%">'
    + '<tr><td align="center" style="font-size:16px;color:#333;font-style:italic;padding-bottom:4px;">' + value + '</td></tr>'
    + '<tr><td align="center" style="padding-bottom:4px;"><table cellpadding="0" cellspacing="0" border="0"><tr>'
    + '<td style="width:140px;height:2px;background-color:#8B0000;"></td></tr></table></td></tr>'
    + '<tr><td align="center" style="font-family:Arial,sans-serif;font-size:8px;color:#888;letter-spacing:2px;text-transform:uppercase;">' + label + '</td></tr>'
    + '</table></td>';
}

/* ================================================================
   EMAIL (MailApp + unsubscribe + Mission Control theme)
   ================================================================ */

function sendCertificateEmail_(studentName, email, pkg, instructor, certNum, pdfFile) {
  try {
    const firstName = (studentName || '').split(' ')[0] || studentName;
    const f   = esc_(firstName);
    const p   = esc_(pkg);
    const ins = esc_(instructor || 'Flavors Team');
    const num = esc_(certNum);
    const sch = esc_(CONFIG.SCHOOL_NAME);
    const tag = esc_(CONFIG.SCHOOL_TAGLINE);
    const url = pdfFile.getUrl();
    const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe&body=Please%20remove%20' + encodeURIComponent(email) + '%20from%20certificate%20emails.';

    const subject = 'Congratulations, ' + firstName + '! Your Certificate from ' + CONFIG.SCHOOL_NAME;

    const htmlBody = '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body style="margin:0;padding:0;background:#000;font-family:Arial,Helvetica,sans-serif;">'
      + '<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#000;"><tr><td align="center"><table width="600" cellpadding="0" cellspacing="0" border="0">'
      // Header
      + '<tr><td style="background:#0d0d0d;border-bottom:2px solid #ff2d2d;padding:30px 24px;text-align:center;">'
      + '<h1 style="color:#fff;font-size:20px;margin:0 0 4px;">' + sch + '</h1>'
      + '<p style="color:rgba(255,255,255,0.5);font-size:12px;margin:0;">' + tag + '</p>'
      + '</td></tr>'
      // Body
      + '<tr><td style="background:#0d0d0d;padding:32px 24px;text-align:center;">'
      + '<div style="font-size:48px;margin-bottom:16px;">๐ŸŽ“</div>'
      + '<div style="font-size:28px;font-weight:800;color:#fff;margin-bottom:8px;">Congratulations, ' + f + '!</div>'
      + '<p style="font-size:14px;color:rgba(255,255,255,0.6);line-height:1.7;max-width:440px;margin:0 auto 24px;">'
      + 'You did it! You\'ve successfully completed your <strong style="color:#ff2d2d;">' + p + '</strong> program with ' + sch + '. '
      + 'Your dedication and hard work have paid off โ€” you\'re officially road-ready.</p>'
      // Stat boxes
      + '<table cellpadding="0" cellspacing="8" border="0" align="center"><tr>'
      + statBox_(p, 'Program') + statBox_(ins, 'Instructor')
      + '</tr></table>'
      // CTA
      + '<div style="margin:24px 0;"><a href="' + url + '" style="display:inline-block;background:#ff2d2d;color:#fff;text-decoration:none;padding:14px 32px;border-radius:8px;font-size:14px;font-weight:700;">View Your Certificate</a></div>'
      + '<p style="font-size:11px;color:rgba(255,255,255,0.3);">Your certificate is also attached to this email as a PDF. Certificate #' + num + '</p>'
      + '</td></tr>'
      // Footer
      + '<tr><td style="background:#000;border-top:1px solid rgba(255,255,255,0.05);padding:20px 24px;text-align:center;">'
      + '<p style="font-size:11px;color:rgba(255,255,255,0.25);margin:2px 0;">' + sch + '</p>'
      + '<p style="font-size:11px;color:rgba(255,255,255,0.25);margin:2px 0;">Thank you for choosing us. Drive safe!</p>'
      + '<p style="font-size:10px;color:rgba(255,255,255,0.15);margin:8px 0 0;"><a href="' + unsub + '" style="color:rgba(255,255,255,0.15);text-decoration:underline;">Unsubscribe</a></p>'
      + '</td></tr>'
      + '</table></td></tr></table></body></html>';

    const plainBody = 'Congratulations, ' + firstName + '! You\'ve completed your ' + pkg + ' program at '
      + CONFIG.SCHOOL_NAME + '. Your certificate is attached. Certificate #' + certNum;

    MailApp.sendEmail({
      to: email,
      subject: subject,
      body: plainBody,
      htmlBody: htmlBody,
      attachments: [pdfFile.getAs('application/pdf')],
      name: CONFIG.SCHOOL_NAME
    });
    return true;
  } catch (e) {
    Logger.log('Email failed for ' + studentName + ': ' + e.message);
    return false;
  }
}

function statBox_(value, label) {
  return '<td style="background:rgba(255,45,45,0.08);border:1px solid rgba(255,45,45,0.2);border-radius:12px;padding:16px 20px;text-align:center;">'
    + '<div style="font-size:18px;font-weight:800;color:#ff2d2d;">' + value + '</div>'
    + '<div style="font-size:10px;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:1px;margin-top:4px;">' + label + '</div></td>';
}

/* ================================================================
   VERIFICATION PAGE HTML
   ================================================================ */

function getVerificationHTML_(certId) {
  const sch = esc_(CONFIG.SCHOOL_NAME);
  const escaped = esc_(certId);
  return '<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
    + '<title>Verify Certificate โ€” ' + sch + '</title>'
    + '<style>'
    + '*{margin:0;padding:0;box-sizing:border-box}'
    + 'body{background:#000;color:#fff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center}'
    + '.card{background:#0d0d0d;border:1px solid rgba(255,45,45,0.15);border-radius:16px;padding:40px;max-width:500px;width:90%;text-align:center}'
    + 'h1{font-size:20px;margin-bottom:4px}'
    + '.tagline{color:rgba(255,255,255,0.4);font-size:12px;margin-bottom:24px}'
    + 'h2{font-size:16px;color:#ff2d2d;margin-bottom:16px}'
    + 'input[type=text]{width:100%;padding:12px 16px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;color:#fff;font-size:14px;text-align:center;letter-spacing:2px;margin-bottom:12px}'
    + 'input:focus{outline:none;border-color:#ff2d2d}'
    + 'button{background:#ff2d2d;color:#fff;border:none;padding:12px 32px;border-radius:8px;font-size:14px;font-weight:700;cursor:pointer;width:100%}'
    + 'button:hover{background:#e02525}'
    + '.result{margin-top:24px;padding:20px;border-radius:12px;text-align:left}'
    + '.found{background:rgba(34,197,94,0.08);border:1px solid rgba(34,197,94,0.2)}'
    + '.not-found{background:rgba(255,45,45,0.08);border:1px solid rgba(255,45,45,0.2)}'
    + '.field{margin-bottom:8px}.field-label{font-size:10px;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:1px}'
    + '.field-value{font-size:14px;color:#fff;margin-top:2px}'
    + '.badge{display:inline-block;background:rgba(34,197,94,0.15);color:#22c55e;padding:4px 12px;border-radius:20px;font-size:12px;font-weight:600;margin-bottom:16px}'
    + '.spinner{display:none;margin:16px auto}'
    + '</style></head><body>'
    + '<div class="card">'
    + '<h1>' + sch + '</h1>'
    + '<p class="tagline">Certificate Verification Portal</p>'
    + '<h2>๐Ÿ” Verify a Certificate</h2>'
    + '<p style="color:rgba(255,255,255,0.5);font-size:13px;margin-bottom:16px;">Enter the certificate number printed on the document.</p>'
    + '<input type="text" id="certInput" placeholder="FDS-XXXXXX-XXXX" value="' + escaped + '">'
    + '<button onclick="verify()">Verify Certificate</button>'
    + '<div id="spinner" class="spinner" style="color:rgba(255,255,255,0.4);font-size:13px;">Checking...</div>'
    + '<div id="result"></div>'
    + '</div>'
    + '<script>'
    + 'function verify(){'
    + 'var id=document.getElementById("certInput").value.trim();'
    + 'if(!id){alert("Please enter a certificate number.");return;}'
    + 'document.getElementById("spinner").style.display="block";'
    + 'document.getElementById("result").innerHTML="";'
    + 'google.script.run.withSuccessHandler(showResult).withFailureHandler(showError).verifyCertificate(id);'
    + '}'
    + 'function showResult(r){'
    + 'document.getElementById("spinner").style.display="none";'
    + 'var el=document.getElementById("result");'
    + 'if(r&&r.found){'
    + 'el.className="result found";'
    + 'el.innerHTML=\'<div class="badge">โœ… Verified</div>\''
    + '+field("Student Name",r.name)+field("Program",r.pkg)+field("Lessons Completed",r.lessons)'
    + '+field("Primary Instructor",r.instructor)+field("Date Issued",r.date)+field("Certificate #",r.certNum);'
    + '}else{'
    + 'el.className="result not-found";'
    + 'el.innerHTML="<p style=\\"color:#ff2d2d;font-size:14px;text-align:center;\\">โŒ Certificate not found. Please check the number and try again.</p>";'
    + '}}'
    + 'function showError(e){'
    + 'document.getElementById("spinner").style.display="none";'
    + 'document.getElementById("result").className="result not-found";'
    + 'document.getElementById("result").innerHTML="<p style=\\"color:#ff2d2d;\\">Error: "+(e.message||"Please try again.")+"</p>";'
    + '}'
    + 'function field(l,v){'
    + 'return\'<div class="field"><div class="field-label">\'+l+\'</div><div class="field-value">\'+(v||"โ€”")+\'</div></div>\';'
    + '}'
    + 'document.getElementById("certInput").addEventListener("keypress",function(e){if(e.key==="Enter")verify();});'
    + (certId ? 'window.onload=function(){verify();};' : '')
    + '</script></body></html>';
}

/* ================================================================
   SHARED HELPERS
   ================================================================ */

/** Header contains any candidate (case-insensitive, .includes()). Returns 0-based index or -1. */
function findCol_(headers, candidates) {
  for (let i = 0; i < headers.length; i++) {
    const h = String(headers[i] || '').toLowerCase().trim();
    for (const kw of candidates) {
      if (kw && h.includes(String(kw).toLowerCase().trim())) return i;
    }
  }
  return -1;
}

function esc_(str) {
  if (str == null) return '';
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

/* ================================================================
   MANUAL TOOLS
   ================================================================ */

/** Generate a demo certificate (no real data). */
function generateDemoCertificate() {
  const demo = DEMO_DATA.completedStudents[0];
  const regSS = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  let certSheet = regSS.getSheetByName('Certificates Issued');
  if (!certSheet) { fullSetup(); certSheet = regSS.getSheetByName('Certificates Issued'); }

  const certNum = generateCertNumber_(certSheet);
  const pdf = generateCertificatePDF_(demo.name, demo.pkg, demo.lessons, demo.instructor, certNum);
  Logger.log('๐ŸŽญ Demo certificate generated: ' + pdf.getUrl());
  Logger.log('Certificate #: ' + certNum);
}

/** Manually generate a certificate for a specific student. Edit values below. */
function generateManualCertificate() {
  if (CONFIG.DEMO_MODE) {
    Logger.log('๐ŸŽญ DEMO MODE โ€” running demo certificate instead.');
    generateDemoCertificate();
    return;
  }

  const studentName = 'Marcus Johnson';
  const email = '[email protected]';
  const pkg = '10 Lessons';
  const instructor = 'Anisha';
  const lessonsCompleted = 10;

  const regSS = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  let certSheet = regSS.getSheetByName('Certificates Issued');
  if (!certSheet) { fullSetup(); certSheet = regSS.getSheetByName('Certificates Issued'); }

  const certNum = generateCertNumber_(certSheet);
  const pdf = generateCertificatePDF_(studentName, pkg, lessonsCompleted, instructor, certNum);
  const sent = sendCertificateEmail_(studentName, email, pkg, instructor, certNum, pdf);

  Logger.log('Certificate: ' + pdf.getUrl());
  Logger.log('Email sent: ' + sent);

  certSheet.appendRow([certNum, studentName, email, '', pkg, lessonsCompleted, instructor, new Date(), pdf.getUrl(), sent ? 'Yes' : 'Failed']);
}

/** Test: generate a preview PDF only (no email). */
function testPreviewCertificate() {
  const regSS = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  let certSheet = regSS.getSheetByName('Certificates Issued');
  if (!certSheet) { fullSetup(); certSheet = regSS.getSheetByName('Certificates Issued'); }

  const certNum = generateCertNumber_(certSheet);
  const pdf = generateCertificatePDF_('Sarah Johnson', 'Premium Package', 15, 'Anisha', certNum);
  Logger.log('Preview certificate: ' + pdf.getUrl());
}
๐ŸŽ“ Student Portal 40KB ยท 1,149 linesGSโ–ถ
/**
 * Student Portal โ€” Flavors Driving School
 * 
 * A web app where students can look up their:
 *   - Upcoming lessons (from Instructor Schedule Board)
 *   - Payment balance (from Payment Tracker)
 *   - Progress (lessons completed vs purchased)
 *   - Package info (from Student Registration)
 * 
 * FEATURES:
 *   - Demo mode with fake sample data for presentations
 *   - Email-first matching with fuzzy name fallback
 *   - Phone verification step before showing data (production)
 *   - Input sanitization
 *   - 100% completion celebration
 * 
 * CONNECTED SHEETS:
 *   - Student Registration: 1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY
 *   - Instructor Schedule Board: 1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE
 *   - Payment Tracker: 1XWNU7PygQX9jV6mFUUrgHYU_vrhS7NP1OHwNUNWRoOk
 */

// ============ FORCE AUTH โ€” hits ALL 3 sheets ============
function forceAuth() {
  const reg = SpreadsheetApp.openById('1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY');
  Logger.log('Registration: ' + reg.getName());
  const sched = SpreadsheetApp.openById('1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE');
  Logger.log('Schedule: ' + sched.getName());
  const pay = SpreadsheetApp.openById('1XWNU7PygQX9jV6mFUUrgHYU_vrhS7NP1OHwNUNWRoOk');
  Logger.log('Payments: ' + pay.getName());
  Logger.log('โœ… Auth complete โ€” all 3 sheets accessible.');
}

const PORTAL_CONFIG = {
  REGISTRATION_ID: '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',
  SCHEDULE_ID: '1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE',
  PAYMENT_ID: '1XWNU7PygQX9jV6mFUUrgHYU_vrhS7NP1OHwNUNWRoOk',
  SCHOOL_NAME: 'Flavors Driving School',
  
  // DEMO MODE โ€” set to true for presentations, false for production
  DEMO_MODE: true,
  
  // VERIFICATION โ€” set to true to require phone last 4 digits before showing data
  REQUIRE_VERIFICATION: false,
  
  PACKAGES: {
    '3 Lessons': 3, '5 Lessons': 5, '10 Lessons': 10,
    '15 Lessons': 15, '25 Lessons': 25, '5-Hour Class': 1
  }
};

// ============ DEMO DATA ============
const DEMO_STUDENTS = {
  'sarah johnson': {
    name: 'Sarah Johnson',
    email: '[email protected]',
    phone: '5551234',
    package: '10 Lessons',
    totalPurchased: 10,
    completedLessons: 7,
    progressPct: 70,
    upcoming: [
      { date: 'Mon, Jun 30', time: '10:00 AM', instructor: 'Carlos' },
      { date: 'Wed, Jul 2', time: '2:00 PM', instructor: 'Anisha' },
      { date: 'Fri, Jul 4', time: '11:00 AM', instructor: 'Carlos' }
    ],
    balance: 150,
    totalPaid: 550,
    totalDue: 700,
    recentPayments: [
      { amount: 200, date: 'Jun 1, 2025' },
      { amount: 200, date: 'Jun 10, 2025' },
      { amount: 150, date: 'Jun 20, 2025' }
    ]
  },
  'mike rivera': {
    name: 'Mike Rivera',
    email: '[email protected]',
    phone: '5555678',
    package: '5 Lessons',
    totalPurchased: 5,
    completedLessons: 5,
    progressPct: 100,
    upcoming: [],
    balance: 0,
    totalPaid: 445,
    totalDue: 445,
    recentPayments: [
      { amount: 245, date: 'May 15, 2025' },
      { amount: 200, date: 'Jun 5, 2025' }
    ]
  },
  'demo student': {
    name: 'Demo Student',
    email: '[email protected]',
    phone: '0000',
    package: '5 Lessons',
    totalPurchased: 5,
    completedLessons: 3,
    progressPct: 60,
    upcoming: [
      { date: 'Tue, Jul 1', time: '9:00 AM', instructor: 'Nick' },
      { date: 'Thu, Jul 3', time: '1:00 PM', instructor: 'Anisha' }
    ],
    balance: 100,
    totalPaid: 345,
    totalDue: 445,
    recentPayments: [
      { amount: 200, date: 'Jun 1, 2025' },
      { amount: 145, date: 'Jun 15, 2025' }
    ]
  }
};

// ============ WEB APP ============
function doGet(e) {
  return HtmlService.createHtmlOutput(buildPortalHTML())
    .setTitle('Student Portal โ€” ' + PORTAL_CONFIG.SCHOOL_NAME)
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

// ============ API: Get Config ============
function getPortalConfig() {
  return {
    demoMode: PORTAL_CONFIG.DEMO_MODE,
    requireVerification: PORTAL_CONFIG.REQUIRE_VERIFICATION,
    schoolName: PORTAL_CONFIG.SCHOOL_NAME
  };
}

// ============ API: Student Lookup ============
function lookupStudent(query) {
  // Sanitize input
  query = sanitize(query);
  if (!query || query.length < 3) return { error: 'Please enter at least 3 characters.' };
  
  // === DEMO MODE ===
  if (PORTAL_CONFIG.DEMO_MODE) {
    return lookupDemoStudent(query);
  }
  
  // === PRODUCTION MODE ===
  // 1. Find student in Registration (email-first, then fuzzy name)
  const student = findInRegistration(query);
  if (!student) return { error: 'Student not found. Please check your name or email and try again.' };
  
  // 2. If verification required, return partial data and ask for verification
  if (PORTAL_CONFIG.REQUIRE_VERIFICATION && !student.verified) {
    return {
      needsVerification: true,
      name: student.name,
      maskedPhone: maskPhone(student.phone)
    };
  }
  
  // 3. Get full data
  return buildStudentResponse(student);
}

// ============ API: Verify Student ============
function verifyStudent(query, lastFour) {
  query = sanitize(query);
  lastFour = sanitize(lastFour).replace(/\D/g, '');
  
  if (!lastFour || lastFour.length !== 4) return { error: 'Please enter the last 4 digits of your phone number.' };
  
  if (PORTAL_CONFIG.DEMO_MODE) {
    const demo = lookupDemoStudent(query);
    if (demo.error) return demo;
    if (demo.phone && demo.phone.slice(-4) === lastFour) {
      return demo;
    }
    return { error: 'Verification failed. Please check the last 4 digits of your phone number.' };
  }
  
  const student = findInRegistration(query);
  if (!student) return { error: 'Student not found.' };
  
  const phoneDigits = (student.phone || '').replace(/\D/g, '');
  if (phoneDigits.length >= 4 && phoneDigits.slice(-4) === lastFour) {
    return buildStudentResponse(student);
  }
  
  return { error: 'Verification failed. Please check the last 4 digits of your phone number.' };
}

// ============ DEMO LOOKUP ============
function lookupDemoStudent(query) {
  const q = query.toLowerCase();
  
  // Try exact key match
  if (DEMO_STUDENTS[q]) return Object.assign({}, DEMO_STUDENTS[q]);
  
  // Try partial match
  for (const [key, student] of Object.entries(DEMO_STUDENTS)) {
    if (key.includes(q) || student.email.toLowerCase().includes(q) ||
        student.name.toLowerCase().includes(q)) {
      return Object.assign({}, student);
    }
  }
  
  return { error: 'Demo student not found. Try "Sarah Johnson", "Mike Rivera", or "Demo Student".' };
}

// ============ BUILD RESPONSE ============
function buildStudentResponse(student) {
  const schedule = getStudentSchedule(student.name, student.email);
  const payments = getStudentPayments(student.name, student.email);
  
  const now = new Date();
  const completedLessons = schedule.filter(s => s.date < now && !isSkipStatus(s.status)).length;
  const upcomingLessons = schedule.filter(s => s.date >= now && !isSkipStatus(s.status));
  const totalPurchased = student.lessonCount || 0;
  const progressPct = totalPurchased > 0 ? Math.min(100, Math.round((completedLessons / totalPurchased) * 100)) : 0;
  
  return {
    name: student.name,
    email: student.email,
    package: student.package,
    totalPurchased: totalPurchased,
    completedLessons: completedLessons,
    progressPct: progressPct,
    upcoming: upcomingLessons.map(s => ({
      date: Utilities.formatDate(s.date, 'America/New_York', 'EEE, MMM d'),
      time: Utilities.formatDate(s.date, 'America/New_York', 'h:mm a'),
      instructor: s.instructor
    })),
    balance: payments.balance,
    totalPaid: payments.totalPaid,
    totalDue: payments.totalDue,
    recentPayments: payments.recent
  };
}

// ============ DATA FUNCTIONS ============
function findInRegistration(query) {
  try {
    const ss = SpreadsheetApp.openById(PORTAL_CONFIG.REGISTRATION_ID);
    const sheet = ss.getSheets()[0];
    const data = sheet.getDataRange().getValues();
    if (data.length < 2) return null;
    
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const nameCol = findCol(headers, ['name', 'full name', 'student name']);
    const emailCol = findCol(headers, ['email', 'student email', 'email address']);
    const phoneCol = findCol(headers, ['phone', 'phone number', 'cell', 'mobile', 'contact number']);
    const pkgCol = findCol(headers, ['package', 'lesson package', 'selected package', 'number of lessons']);
    
    const isEmail = query.includes('@');
    let bestMatch = null;
    let bestScore = 0;
    
    for (let i = 1; i < data.length; i++) {
      const name = nameCol !== -1 ? String(data[i][nameCol]).trim() : '';
      const email = emailCol !== -1 ? String(data[i][emailCol]).trim() : '';
      const phone = phoneCol !== -1 ? String(data[i][phoneCol]).trim() : '';
      
      if (!name && !email) continue;
      
      let score = 0;
      
      // Email exact match = highest priority
      if (isEmail && email.toLowerCase() === query) {
        score = 100;
      }
      // Exact name match
      else if (name.toLowerCase() === query) {
        score = 90;
      }
      // Fuzzy name match
      else if (!isEmail && fuzzyNameMatch(query, name.toLowerCase())) {
        score = 70;
      }
      
      if (score > bestScore) {
        bestScore = score;
        const pkgRaw = pkgCol !== -1 ? String(data[i][pkgCol]).trim() : '';
        let pkg = 'Unknown';
        let lessonCount = 0;
        for (const [key, count] of Object.entries(PORTAL_CONFIG.PACKAGES)) {
          if (pkgRaw.toLowerCase().includes(key.toLowerCase())) {
            pkg = key; lessonCount = count; break;
          }
        }
        bestMatch = { name, email, phone, package: pkg, lessonCount };
      }
    }
    
    return bestMatch;
  } catch (e) { Logger.log('Registration error: ' + e.message); }
  return null;
}

function getStudentSchedule(studentName, studentEmail) {
  const lessons = [];
  try {
    const ss = SpreadsheetApp.openById(PORTAL_CONFIG.SCHEDULE_ID);
    const data = ss.getSheets()[0].getDataRange().getValues();
    if (data.length < 2) return lessons;
    
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const nameCol = findCol(headers, ['student name', 'student', 'name']);
    const emailCol = findCol(headers, ['email', 'student email']);
    const dateCol = findCol(headers, ['date', 'lesson date', 'booking date']);
    const timeCol = findCol(headers, ['time', 'lesson time', 'start time', 'time slot']);
    const instrCol = findCol(headers, ['instructor', 'instructor name']);
    const statusCol = findCol(headers, ['status', 'booking status']);
    
    const nameKey = studentName.toLowerCase().trim();
    const emailKey = (studentEmail || '').toLowerCase().trim();
    
    for (let i = 1; i < data.length; i++) {
      const rowName = nameCol !== -1 ? String(data[i][nameCol]).trim().toLowerCase() : '';
      const rowEmail = emailCol !== -1 ? String(data[i][emailCol]).trim().toLowerCase() : '';
      
      // Match by email first, then exact name, then fuzzy name
      let matched = false;
      if (emailKey && rowEmail === emailKey) matched = true;
      else if (rowName === nameKey) matched = true;
      else if (fuzzyNameMatch(nameKey, rowName)) matched = true;
      
      if (!matched) continue;
      
      let lessonDate = dateCol !== -1 ? data[i][dateCol] : null;
      if (!(lessonDate instanceof Date)) continue;
      
      // Merge time if separate column
      if (timeCol !== -1 && data[i][timeCol]) {
        const timeVal = data[i][timeCol];
        if (timeVal instanceof Date) {
          lessonDate.setHours(timeVal.getHours(), timeVal.getMinutes());
        } else {
          const match = String(timeVal).match(/(\d{1,2}):?(\d{2})?\s*(am|pm)?/i);
          if (match) {
            let h = parseInt(match[1]);
            const m = parseInt(match[2] || '0');
            const ampm = (match[3] || '').toLowerCase();
            if (ampm === 'pm' && h < 12) h += 12;
            if (ampm === 'am' && h === 12) h = 0;
            lessonDate.setHours(h, m);
          }
        }
      }
      
      lessons.push({
        date: lessonDate,
        instructor: instrCol !== -1 ? String(data[i][instrCol]).trim() : 'TBD',
        status: statusCol !== -1 ? String(data[i][statusCol]).trim() : 'Confirmed'
      });
    }
    
    lessons.sort((a, b) => a.date - b.date);
  } catch (e) { Logger.log('Schedule error: ' + e.message); }
  return lessons;
}

function getStudentPayments(studentName, studentEmail) {
  const result = { balance: 0, totalPaid: 0, totalDue: 0, recent: [] };
  try {
    const ss = SpreadsheetApp.openById(PORTAL_CONFIG.PAYMENT_ID);
    const data = ss.getSheets()[0].getDataRange().getValues();
    if (data.length < 2) return result;
    
    const headers = data[0].map(h => String(h).toLowerCase().trim());
    const nameCol = findCol(headers, ['student name', 'student', 'name']);
    const emailCol = findCol(headers, ['email', 'student email']);
    const amountCol = findCol(headers, ['amount', 'payment amount', 'amount paid']);
    const dateCol = findCol(headers, ['date', 'payment date']);
    const balanceCol = findCol(headers, ['balance', 'remaining balance', 'balance due']);
    const totalCol = findCol(headers, ['total', 'total due', 'package price', 'total cost']);
    
    const nameKey = studentName.toLowerCase().trim();
    const emailKey = (studentEmail || '').toLowerCase().trim();
    
    for (let i = 1; i < data.length; i++) {
      const rowName = nameCol !== -1 ? String(data[i][nameCol]).trim().toLowerCase() : '';
      const rowEmail = emailCol !== -1 ? String(data[i][emailCol]).trim().toLowerCase() : '';
      
      // Match by email first, then exact name, then fuzzy
      let matched = false;
      if (emailKey && rowEmail === emailKey) matched = true;
      else if (rowName === nameKey) matched = true;
      else if (fuzzyNameMatch(nameKey, rowName)) matched = true;
      
      if (!matched) continue;
      
      const amount = amountCol !== -1 ? parseFloat(data[i][amountCol]) || 0 : 0;
      const date = dateCol !== -1 ? data[i][dateCol] : null;
      
      if (balanceCol !== -1) result.balance = parseFloat(data[i][balanceCol]) || 0;
      if (totalCol !== -1) result.totalDue = parseFloat(data[i][totalCol]) || 0;
      
      result.totalPaid += amount;
      
      if (amount > 0) {
        result.recent.push({
          amount: amount,
          date: date instanceof Date ? Utilities.formatDate(date, 'America/New_York', 'MMM d, yyyy') : 'N/A'
        });
      }
    }
    
    result.recent = result.recent.slice(-5);
    if (result.totalDue === 0 && result.balance > 0) result.totalDue = result.totalPaid + result.balance;
    
  } catch (e) { Logger.log('Payment error: ' + e.message); }
  return result;
}

// ============ HELPERS ============
function sanitize(input) {
  if (!input) return '';
  return String(input)
    .trim()
    .replace(/[<>{}()\[\]\\\/]/g, '')  // Strip dangerous chars
    .replace(/\s+/g, ' ')              // Collapse whitespace
    .substring(0, 100)                  // Max length
    .toLowerCase();
}

function isSkipStatus(status) {
  const skip = ['cancelled', 'canceled', 'no-show', 'no show', 'noshow', 'rescheduled'];
  return skip.includes((status || '').toLowerCase().trim());
}

function maskPhone(phone) {
  const digits = (phone || '').replace(/\D/g, '');
  if (digits.length < 4) return '****';
  return '***-***-' + digits.slice(-4);
}

function fuzzyNameMatch(query, name) {
  if (!query || !name) return false;
  const q = query.toLowerCase().replace(/\s+/g, ' ').trim();
  const n = name.toLowerCase().replace(/\s+/g, ' ').trim();
  
  if (q === n) return true;
  if (q.includes(n) || n.includes(q)) return true;
  
  const qParts = q.split(' ').filter(Boolean);
  const nParts = n.split(' ').filter(Boolean);
  
  // Same last name + similar first name (3+ chars)
  if (qParts.length >= 2 && nParts.length >= 2) {
    const qLast = qParts[qParts.length - 1];
    const nLast = nParts[nParts.length - 1];
    const qFirst = qParts[0];
    const nFirst = nParts[0];
    
    if (qLast === nLast && qFirst.substring(0, 3) === nFirst.substring(0, 3)) return true;
    if (qFirst === nLast && qLast === nFirst) return true; // Swapped order
  }
  
  // Levenshtein distance <= 2
  if (levenshtein(q, n) <= 2) return true;
  
  return false;
}

function levenshtein(a, b) {
  if (a.length === 0) return b.length;
  if (b.length === 0) return a.length;
  const matrix = [];
  for (let i = 0; i <= b.length; i++) matrix[i] = [i];
  for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
  for (let i = 1; i <= b.length; i++) {
    for (let j = 1; j <= a.length; j++) {
      if (b.charAt(i - 1) === a.charAt(j - 1)) {
        matrix[i][j] = matrix[i - 1][j - 1];
      } else {
        matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
      }
    }
  }
  return matrix[b.length][a.length];
}

function findCol(headers, keywords) {
  for (const kw of keywords) {
    const idx = headers.findIndex(h => h.includes(kw));
    if (idx !== -1) return idx;
  }
  return -1;
}

// ============ HTML TEMPLATE ============
function buildPortalHTML() {
  return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Student Portal โ€” ${PORTAL_CONFIG.SCHOOL_NAME}</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
  
  * { margin:0; padding:0; box-sizing:border-box; }
  
  :root {
    --bg: #050505;
    --surface: rgba(255,255,255,0.04);
    --glass: rgba(255,255,255,0.06);
    --glass-border: rgba(255,255,255,0.1);
    --glass-hover: rgba(255,255,255,0.12);
    --red: #ff2d2d;
    --red-glow: rgba(255,45,45,0.3);
    --text: #ffffff;
    --text-dim: #888888;
    --text-muted: #555555;
    --success: #22c55e;
    --warning: #f59e0b;
  }
  
  body {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    background: var(--bg);
    color: var(--text);
    min-height: 100vh;
    overflow-x: hidden;
  }
  
  body::before {
    content: '';
    position: fixed;
    top: -50%; left: -50%;
    width: 200%; height: 200%;
    background: radial-gradient(circle at 30% 20%, rgba(255,45,45,0.06) 0%, transparent 50%),
                radial-gradient(circle at 70% 80%, rgba(255,45,45,0.03) 0%, transparent 50%);
    pointer-events: none; z-index: 0;
  }
  
  .container {
    max-width: 800px;
    margin: 0 auto;
    padding: 40px 20px;
    position: relative; z-index: 1;
  }
  
  .header { text-align: center; margin-bottom: 40px; }
  .logo { font-size: 48px; margin-bottom: 8px; }
  
  .header h1 {
    font-size: 28px; font-weight: 800; letter-spacing: -0.5px;
    background: linear-gradient(135deg, #fff 0%, #ccc 100%);
    -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  }
  
  .header p { color: var(--text-dim); font-size: 14px; margin-top: 4px; }
  .school-name { color: var(--red) !important; -webkit-text-fill-color: var(--red) !important; font-weight: 600; }
  
  /* Demo Banner */
  .demo-banner {
    text-align: center;
    margin-bottom: 20px;
    padding: 10px 20px;
    background: rgba(255,45,45,0.08);
    border: 1px solid rgba(255,45,45,0.2);
    border-radius: 14px;
  }
  .demo-badge {
    display: inline-block;
    color: #ff6b6b; font-size: 12px; font-weight: 700;
    letter-spacing: 1px; text-transform: uppercase;
  }
  .demo-hint {
    color: #666; font-size: 11px; margin-top: 4px;
  }
  
  /* Glass Card */
  .glass-card {
    background: var(--glass);
    backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
    border: 1px solid var(--glass-border);
    border-radius: 24px; padding: 32px; margin-bottom: 20px;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  }
  
  .glass-card:hover {
    background: var(--glass-hover);
    border-color: rgba(255,255,255,0.15);
    transform: translateY(-2px);
    box-shadow: 0 20px 60px rgba(0,0,0,0.3);
  }
  
  .search-card { padding: 40px; }
  
  .search-wrapper { position: relative; margin-top: 20px; }
  
  .search-input {
    width: 100%; padding: 16px 24px 16px 52px;
    background: rgba(255,255,255,0.06);
    border: 1px solid rgba(255,255,255,0.1);
    border-radius: 16px; color: #fff; font-size: 16px;
    font-family: 'Inter', sans-serif; outline: none;
    transition: all 0.3s ease;
  }
  
  .search-input:focus {
    border-color: var(--red);
    box-shadow: 0 0 30px var(--red-glow);
    background: rgba(255,255,255,0.08);
  }
  
  .search-input::placeholder { color: var(--text-muted); }
  .search-icon { position: absolute; left: 20px; top: 50%; transform: translateY(-50%); font-size: 20px; opacity: 0.4; }
  
  .search-btn {
    width: 100%; margin-top: 16px; padding: 14px;
    background: linear-gradient(135deg, #ff2d2d, #cc0000);
    border: none; border-radius: 14px; color: #fff;
    font-size: 16px; font-weight: 700; font-family: 'Inter', sans-serif;
    cursor: pointer; transition: all 0.3s ease;
    box-shadow: 0 4px 20px var(--red-glow);
  }
  
  .search-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 30px var(--red-glow); }
  .search-btn:active { transform: translateY(0); }
  .search-label { color: var(--text-dim); font-size: 13px; margin-bottom: 8px; display: block; }
  
  .error-msg {
    color: var(--red); text-align: center; padding: 12px; margin-top: 12px;
    font-size: 14px; border-radius: 12px; background: rgba(255,45,45,0.08);
    display: none;
  }
  
  .loading { text-align: center; padding: 40px; display: none; }
  
  .spinner {
    width: 40px; height: 40px;
    border: 3px solid rgba(255,255,255,0.1);
    border-top-color: var(--red); border-radius: 50%;
    animation: spin 0.8s linear infinite; margin: 0 auto 16px;
  }
  
  @keyframes spin { to { transform: rotate(360deg); } }
  
  /* Verification */
  .verify-card { padding: 40px; text-align: center; display: none; }
  .verify-card h3 { font-size: 18px; margin-bottom: 8px; }
  .verify-card p { color: var(--text-dim); font-size: 13px; margin-bottom: 20px; }
  .verify-code {
    width: 160px; padding: 14px; text-align: center;
    background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1);
    border-radius: 14px; color: #fff; font-size: 24px;
    font-family: 'Inter', sans-serif; letter-spacing: 8px;
    outline: none; transition: all 0.3s;
  }
  .verify-code:focus { border-color: var(--red); box-shadow: 0 0 20px var(--red-glow); }
  .verify-code::placeholder { letter-spacing: 4px; font-size: 16px; }
  .verify-phone-hint { color: var(--text-muted); font-size: 12px; margin-top: 8px; }
  
  /* Welcome */
  .welcome { text-align: center; padding: 24px; }
  .welcome h2 { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
  .package-badge {
    display: inline-block; background: rgba(255,45,45,0.15);
    color: var(--red); padding: 6px 16px; border-radius: 20px;
    font-size: 13px; font-weight: 600; margin-top: 8px;
  }
  
  /* Bubble Icons */
  .bubble-icon {
    width: 56px; height: 56px; border-radius: 18px;
    display: flex; align-items: center; justify-content: center;
    font-size: 26px; flex-shrink: 0;
    transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
    cursor: default; position: relative;
    box-shadow: 0 4px 15px rgba(0,0,0,0.2);
  }
  
  .bubble-icon::after {
    content: ''; position: absolute; inset: 0; border-radius: 18px;
    background: linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 50%);
    pointer-events: none;
  }
  
  .glass-card:hover .bubble-icon {
    transform: scale(1.15) translateY(-4px);
    box-shadow: 0 12px 30px rgba(0,0,0,0.3);
  }
  
  .bubble-progress { background: linear-gradient(135deg, #1a3a1a, #0a2e0a); }
  .bubble-schedule { background: linear-gradient(135deg, #1a1a3a, #0a0a2e); }
  .bubble-payment { background: linear-gradient(135deg, #3a2a1a, #2e1a0a); }
  .bubble-history { background: linear-gradient(135deg, #2a1a2a, #1e0a1e); }
  
  /* Stats Grid */
  .stats-grid {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px; margin-bottom: 20px;
  }
  
  .stat-card { display: flex; align-items: center; gap: 16px; padding: 20px; }
  
  .stat-info h3 {
    font-size: 12px; font-weight: 500; color: var(--text-dim);
    text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;
  }
  .stat-info .value { font-size: 28px; font-weight: 800; letter-spacing: -1px; }
  .stat-info .sub { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
  
  /* Progress */
  .progress-section { padding: 24px; }
  .progress-header { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
  .progress-header h3 { font-size: 16px; font-weight: 600; }
  
  .progress-track {
    width: 100%; height: 12px; background: rgba(255,255,255,0.06);
    border-radius: 6px; overflow: hidden; margin-bottom: 8px;
  }
  
  .progress-fill {
    height: 100%; border-radius: 6px;
    transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
    box-shadow: 0 0 20px var(--red-glow);
  }
  
  .progress-fill.complete {
    background: linear-gradient(90deg, var(--success), #4ade80) !important;
    box-shadow: 0 0 20px rgba(34,197,94,0.4) !important;
  }
  
  .progress-fill.in-progress {
    background: linear-gradient(90deg, var(--red), #ff6b6b);
  }
  
  .progress-label {
    display: flex; justify-content: space-between;
    font-size: 13px; color: var(--text-dim);
  }
  
  /* Completion Celebration */
  .completion-banner {
    display: none;
    text-align: center;
    padding: 24px;
    margin-bottom: 20px;
    background: linear-gradient(135deg, rgba(34,197,94,0.1), rgba(34,197,94,0.03));
    border: 1px solid rgba(34,197,94,0.2);
    border-radius: 24px;
    animation: celebratePulse 2s ease-in-out infinite;
  }
  
  @keyframes celebratePulse {
    0%, 100% { box-shadow: 0 0 20px rgba(34,197,94,0.1); }
    50% { box-shadow: 0 0 40px rgba(34,197,94,0.2); }
  }
  
  .completion-banner .trophy { font-size: 56px; margin-bottom: 8px; }
  .completion-banner h2 { color: var(--success); font-size: 22px; font-weight: 800; }
  .completion-banner p { color: var(--text-dim); font-size: 14px; margin-top: 4px; }
  
  /* Confetti */
  .confetti-container {
    position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    pointer-events: none; z-index: 1000; overflow: hidden;
  }
  
  .confetti {
    position: absolute; top: -10px;
    width: 10px; height: 10px;
    opacity: 0; border-radius: 2px;
    animation: confettiFall 3s ease-in forwards;
  }
  
  @keyframes confettiFall {
    0% { opacity: 1; top: -10px; transform: rotate(0deg) scale(1); }
    100% { opacity: 0; top: 110vh; transform: rotate(720deg) scale(0.5); }
  }
  
  /* Section */
  .section-header { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
  .section-header h3 { font-size: 16px; font-weight: 600; }
  
  .lesson-item {
    display: flex; align-items: center; gap: 16px; padding: 16px;
    background: rgba(255,255,255,0.03); border-radius: 14px;
    margin-bottom: 10px; border: 1px solid rgba(255,255,255,0.04);
    transition: all 0.2s ease;
  }
  .lesson-item:hover { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.08); }
  
  .lesson-date { min-width: 60px; text-align: center; }
  .lesson-date .day { font-size: 22px; font-weight: 800; color: var(--red); line-height: 1; }
  .lesson-date .month {
    font-size: 11px; color: var(--text-dim); text-transform: uppercase;
    font-weight: 600; letter-spacing: 0.5px;
  }
  .lesson-details { flex: 1; }
  .lesson-details .time { font-size: 15px; font-weight: 600; }
  .lesson-details .instructor { font-size: 13px; color: var(--text-dim); margin-top: 2px; }
  
  .no-data { text-align: center; color: var(--text-muted); padding: 24px; font-size: 14px; }
  
  .payment-row {
    display: flex; justify-content: space-between; align-items: center;
    padding: 12px 16px; background: rgba(255,255,255,0.03);
    border-radius: 12px; margin-bottom: 8px;
  }
  .payment-row .date { color: var(--text-dim); font-size: 13px; }
  .payment-row .amount { font-weight: 700; color: var(--success); }
  
  .balance-display {
    text-align: center; padding: 20px; margin-bottom: 16px;
    background: rgba(255,255,255,0.03); border-radius: 16px;
  }
  .balance-display .label {
    font-size: 12px; color: var(--text-dim);
    text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px;
  }
  .balance-display .amount { font-size: 36px; font-weight: 800; letter-spacing: -1px; }
  .balance-zero { color: var(--success); }
  .balance-owed { color: var(--warning); }
  
  #dashboard { display: none; }
  
  @media (max-width: 600px) {
    .container { padding: 20px 12px; }
    .glass-card { padding: 20px; border-radius: 18px; }
    .search-card { padding: 24px; }
    .stats-grid { grid-template-columns: 1fr; }
    .header h1 { font-size: 22px; }
    .welcome h2 { font-size: 20px; }
  }
</style>
</head>
<body>

<div class="container">
  <div class="header">
    <div class="logo">๐Ÿš—</div>
    <h1><span class="school-name">${PORTAL_CONFIG.SCHOOL_NAME}</span></h1>
    <p>Student Portal</p>
  </div>
  
  <!-- Demo Banner -->
  <div class="demo-banner" id="demoBanner" style="display:none;">
    <div class="demo-badge">โœฆ Demo Mode</div>
    <p class="demo-hint">Try: "Sarah Johnson", "Mike Rivera", or "Demo Student"</p>
  </div>
  
  <!-- Search -->
  <div id="searchSection" class="glass-card search-card">
    <span class="search-label">Enter your name or email to view your dashboard</span>
    <div class="search-wrapper">
      <span class="search-icon">๐Ÿ”</span>
      <input type="text" id="searchInput" class="search-input" placeholder="e.g. John Smith or [email protected]" autocomplete="off" maxlength="100">
    </div>
    <button id="searchBtn" class="search-btn" onclick="doSearch()">View My Dashboard</button>
    <div id="errorMsg" class="error-msg"></div>
  </div>
  
  <!-- Verification -->
  <div id="verifySection" class="glass-card verify-card">
    <div style="font-size:40px;margin-bottom:12px;">๐Ÿ”</div>
    <h3>Verify Your Identity</h3>
    <p>Enter the last 4 digits of your phone number</p>
    <p class="verify-phone-hint" id="verifyPhoneHint"></p>
    <div style="margin:16px 0;">
      <input type="text" id="verifyInput" class="verify-code" placeholder="โ€ขโ€ขโ€ขโ€ข" maxlength="4" inputmode="numeric">
    </div>
    <button class="search-btn" style="max-width:300px;margin:0 auto;" onclick="doVerify()">Verify</button>
    <div id="verifyError" class="error-msg" style="margin-top:12px;"></div>
    <button onclick="goBack()" style="background:none;border:none;color:var(--text-muted);font-size:13px;cursor:pointer;margin-top:16px;font-family:inherit;">โ† Back</button>
  </div>
  
  <!-- Loading -->
  <div id="loading" class="loading">
    <div class="spinner"></div>
    <p style="color:var(--text-dim);font-size:14px;">Looking up your information...</p>
  </div>
  
  <!-- Dashboard -->
  <div id="dashboard">
    <!-- Completion Celebration -->
    <div class="completion-banner" id="completionBanner">
      <div class="trophy">๐Ÿ†</div>
      <h2>Package Complete!</h2>
      <p>Congratulations! You've finished all your lessons. Safe driving! ๐Ÿš—</p>
    </div>
    
    <!-- Welcome -->
    <div class="glass-card welcome">
      <h2>Welcome back, <span id="studentName"></span>! ๐Ÿ‘‹</h2>
      <div class="package-badge" id="packageBadge"></div>
    </div>
    
    <!-- Progress -->
    <div class="glass-card progress-section">
      <div class="progress-header">
        <div class="bubble-icon bubble-progress">๐Ÿ“Š</div>
        <div>
          <h3>Your Progress</h3>
          <p style="font-size:13px;color:var(--text-dim);" id="progressText"></p>
        </div>
      </div>
      <div class="progress-track">
        <div class="progress-fill in-progress" id="progressBar" style="width:0%"></div>
      </div>
      <div class="progress-label">
        <span id="progressLeft"></span>
        <span id="progressRight"></span>
      </div>
    </div>
    
    <!-- Stats -->
    <div class="stats-grid">
      <div class="glass-card stat-card">
        <div class="bubble-icon bubble-schedule">๐Ÿ“…</div>
        <div class="stat-info">
          <h3>Upcoming</h3>
          <div class="value" id="upcomingCount">0</div>
          <div class="sub">lessons scheduled</div>
        </div>
      </div>
      <div class="glass-card stat-card">
        <div class="bubble-icon bubble-payment">๐Ÿ’ฐ</div>
        <div class="stat-info">
          <h3>Balance</h3>
          <div class="value" id="balanceAmount">$0</div>
          <div class="sub" id="balanceSub">remaining</div>
        </div>
      </div>
    </div>
    
    <!-- Upcoming Lessons -->
    <div class="glass-card" id="scheduleCard">
      <div class="section-header">
        <div class="bubble-icon bubble-schedule">๐Ÿ—“๏ธ</div>
        <h3>Upcoming Lessons</h3>
      </div>
      <div id="lessonList"></div>
    </div>
    
    <!-- Payments -->
    <div class="glass-card" id="paymentCard">
      <div class="section-header">
        <div class="bubble-icon bubble-history">๐Ÿงพ</div>
        <h3>Payment History</h3>
      </div>
      <div class="balance-display">
        <div class="label">Total Paid</div>
        <div class="amount balance-zero" id="totalPaid">$0</div>
      </div>
      <div id="paymentList"></div>
    </div>
    
    <!-- Back button -->
    <div style="text-align:center;margin-top:20px;">
      <button class="search-btn" style="max-width:300px;" onclick="goBack()">โ† Search Again</button>
    </div>
  </div>
</div>

<!-- Confetti container -->
<div class="confetti-container" id="confettiContainer"></div>

<script>
  let currentQuery = '';
  let portalConfig = {};
  
  // Load config on start
  google.script.run
    .withSuccessHandler(function(cfg) {
      portalConfig = cfg || {};
      if (portalConfig.demoMode) {
        document.getElementById('demoBanner').style.display = 'block';
      }
    })
    .getPortalConfig();
  
  document.getElementById('searchInput').addEventListener('keydown', function(e) {
    if (e.key === 'Enter') doSearch();
  });
  
  document.getElementById('verifyInput').addEventListener('keydown', function(e) {
    if (e.key === 'Enter') doVerify();
  });
  
  function doSearch() {
    const query = document.getElementById('searchInput').value.trim();
    if (!query || query.length < 3) { showError('errorMsg', 'Please enter at least 3 characters.'); return; }
    
    currentQuery = query;
    hideError('errorMsg');
    document.getElementById('searchSection').style.display = 'none';
    document.getElementById('loading').style.display = 'block';
    
    google.script.run
      .withSuccessHandler(handleResult)
      .withFailureHandler(function() { handleGenericError('searchSection'); })
      .lookupStudent(query);
  }
  
  function doVerify() {
    const code = document.getElementById('verifyInput').value.trim();
    if (!code || code.length !== 4) { showError('verifyError', 'Please enter 4 digits.'); return; }
    
    hideError('verifyError');
    document.getElementById('verifySection').style.display = 'none';
    document.getElementById('loading').style.display = 'block';
    
    google.script.run
      .withSuccessHandler(function(data) {
        document.getElementById('loading').style.display = 'none';
        if (data.error) {
          document.getElementById('verifySection').style.display = 'block';
          showError('verifyError', data.error);
          return;
        }
        showDashboard(data);
      })
      .withFailureHandler(function() { handleGenericError('verifySection'); })
      .verifyStudent(currentQuery, code);
  }
  
  function handleResult(data) {
    document.getElementById('loading').style.display = 'none';
    
    if (data.error) {
      document.getElementById('searchSection').style.display = 'block';
      showError('errorMsg', data.error);
      return;
    }
    
    // Needs verification?
    if (data.needsVerification) {
      document.getElementById('verifyPhoneHint').textContent = 'Phone on file: ' + data.maskedPhone;
      document.getElementById('verifySection').style.display = 'block';
      document.getElementById('verifyInput').value = '';
      document.getElementById('verifyInput').focus();
      return;
    }
    
    showDashboard(data);
  }
  
  function showDashboard(data) {
    // Welcome
    document.getElementById('studentName').textContent = data.name.split(' ')[0];
    document.getElementById('packageBadge').textContent = data.package + (data.totalPurchased > 1 ? ' Package' : '');
    
    // Progress
    const isComplete = data.progressPct >= 100;
    document.getElementById('progressText').textContent = data.completedLessons + ' of ' + data.totalPurchased + ' lessons completed';
    
    const bar = document.getElementById('progressBar');
    bar.className = 'progress-fill ' + (isComplete ? 'complete' : 'in-progress');
    setTimeout(function() { bar.style.width = data.progressPct + '%'; }, 100);
    
    document.getElementById('progressLeft').textContent = data.completedLessons + ' done';
    document.getElementById('progressRight').textContent = data.progressPct + '%';
    
    // Completion celebration
    if (isComplete) {
      document.getElementById('completionBanner').style.display = 'block';
      setTimeout(launchConfetti, 500);
    } else {
      document.getElementById('completionBanner').style.display = 'none';
    }
    
    // Stats
    document.getElementById('upcomingCount').textContent = data.upcoming.length;
    var bal = data.balance || 0;
    document.getElementById('balanceAmount').textContent = '$' + bal.toFixed(0);
    document.getElementById('balanceAmount').className = 'value ' + (bal > 0 ? 'balance-owed' : 'balance-zero');
    document.getElementById('balanceSub').textContent = bal > 0 ? 'remaining' : 'all paid!';
    
    // Upcoming lessons
    var lessonDiv = document.getElementById('lessonList');
    if (data.upcoming.length === 0) {
      lessonDiv.innerHTML = '<div class="no-data">No upcoming lessons scheduled</div>';
    } else {
      lessonDiv.innerHTML = data.upcoming.map(function(l) {
        var parts = l.date.split(', ');
        var dayParts = (parts[1] || parts[0]).split(' ');
        return '<div class="lesson-item">' +
          '<div class="lesson-date"><div class="month">' + (dayParts[0] || '') + '</div><div class="day">' + (dayParts[1] || '') + '</div></div>' +
          '<div class="lesson-details"><div class="time">' + l.time + '</div><div class="instructor">with ' + l.instructor + '</div></div>' +
        '</div>';
      }).join('');
    }
    
    // Payments
    document.getElementById('totalPaid').textContent = '$' + (data.totalPaid || 0).toFixed(0);
    var payDiv = document.getElementById('paymentList');
    if (!data.recentPayments || data.recentPayments.length === 0) {
      payDiv.innerHTML = '<div class="no-data">No payment records found</div>';
    } else {
      payDiv.innerHTML = data.recentPayments.map(function(p) {
        return '<div class="payment-row"><span class="date">' + p.date + '</span><span class="amount">+$' + p.amount.toFixed(0) + '</span></div>';
      }).join('');
    }
    
    document.getElementById('dashboard').style.display = 'block';
  }
  
  function launchConfetti() {
    var container = document.getElementById('confettiContainer');
    container.innerHTML = '';
    var colors = ['#ff2d2d', '#22c55e', '#f59e0b', '#3b82f6', '#a855f7', '#ec4899', '#ffffff'];
    for (var i = 0; i < 60; i++) {
      var conf = document.createElement('div');
      conf.className = 'confetti';
      conf.style.left = Math.random() * 100 + '%';
      conf.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
      conf.style.animationDelay = (Math.random() * 2) + 's';
      conf.style.animationDuration = (2 + Math.random() * 2) + 's';
      conf.style.width = (6 + Math.random() * 8) + 'px';
      conf.style.height = (6 + Math.random() * 8) + 'px';
      container.appendChild(conf);
    }
    setTimeout(function() { container.innerHTML = ''; }, 5000);
  }
  
  function handleGenericError(showSection) {
    document.getElementById('loading').style.display = 'none';
    document.getElementById(showSection).style.display = 'block';
    var errId = showSection === 'verifySection' ? 'verifyError' : 'errorMsg';
    showError(errId, 'Something went wrong. Please try again.');
  }
  
  function showError(id, msg) {
    var el = document.getElementById(id);
    el.textContent = msg;
    el.style.display = 'block';
  }
  
  function hideError(id) {
    document.getElementById(id).style.display = 'none';
  }
  
  function goBack() {
    document.getElementById('dashboard').style.display = 'none';
    document.getElementById('verifySection').style.display = 'none';
    document.getElementById('completionBanner').style.display = 'none';
    document.getElementById('confettiContainer').innerHTML = '';
    document.getElementById('searchSection').style.display = 'block';
    document.getElementById('searchInput').value = '';
    document.getElementById('progressBar').style.width = '0%';
    document.getElementById('searchInput').focus();
  }
</script>

</body>
</html>`;
}
๐Ÿ“‹ Student Registration Processor 26KB ยท 719 linesGSโ–ถ
/**
 * =========================================================
 *  STUDENT REGISTRATION PROCESSOR
 *  Flavors Driving School
 * =========================================================
 *  Auto-processes new form responses:
 *  - Normalizes dates (YYYY-MM-DD) and phone (XXX-XXX-XXXX)
 *  - Validates required fields + email format
 *  - Calculates age from DOB, flags underage students
 *  - Detects duplicate registrations
 *  - Generates unique Student ID (FDS-0001)
 *  - Sends branded welcome email with confirmation number
 *  - Notifies admin of new registrations
 *  - Payment OK? flag with smart logic
 *  - Demo mode for safe presentations
 * =========================================================
 */

const CONFIG = {
  // โ”€โ”€ Sheet IDs โ”€โ”€
  REGISTRATION_ID: '1HTqF_DqZW2yJu6ChutR6j1YCvfCEt1D_hS6Du-T0sEY',
  SCHEDULE_BOARD_ID: '1ORLiNB_u0BkbbhLAa6B0OCTbgrJIO1AVQZ1L1Ex37VE',

  // โ”€โ”€ Sheet tabs (empty = first sheet) โ”€โ”€
  REGISTRATION_SHEET_TAB: '',

  // โ”€โ”€ Branding โ”€โ”€
  SCHOOL_NAME:    'Flavors Driving School',
  SCHOOL_TAGLINE: 'Your Road to Freedom Starts Here',
  ADMIN_EMAIL:    '[email protected]',
  SCHOOL_PHONE:   '(718) 555-0100',
  SCHOOL_ADDRESS: 'Queens, NY',

  // โ”€โ”€ Timezone โ”€โ”€
  TIMEZONE: 'America/New_York',

  // โ”€โ”€ Age rules โ”€โ”€
  MIN_DRIVING_AGE: 16,
  MINOR_AGE:       18,

  // โ”€โ”€ Demo mode โ”€โ”€
  DEMO_MODE: true
};

// โ”€โ”€ Column header keywords โ”€โ”€
const COL_KEYWORDS = {
  TIMESTAMP:      ['timestamp', 'date submitted', 'submitted'],
  NAME:           ['student name', 'full name', 'name'],
  EMAIL:          ['email', 'student email', 'email address'],
  PHONE:          ['phone', 'phone number', 'mobile', 'cell'],
  DOB:            ['date of birth', 'dob', 'birth date', 'birthday'],
  START_DATE:     ['start date', 'preferred start', 'start'],
  PACKAGE:        ['package', 'lesson package', 'program'],
  PAYMENT_STATUS: ['payment status', 'payment', 'status'],
  AMOUNT_PAID:    ['amount paid', 'amount', 'paid'],
  PAYMENT_OK:     ['payment ok', 'payment ok?'],
  STUDENT_ID:     ['student id', 'id', 'student #'],
  AGE:            ['age', 'student age'],
  FLAGS:          ['flags', 'notes', 'alert'],
  PARENT_NAME:    ['parent name', 'guardian', 'parent/guardian'],
  PARENT_EMAIL:   ['parent email', 'guardian email'],
  PARENT_PHONE:   ['parent phone', 'guardian phone'],
  WELCOME_SENT:   ['welcome sent', 'email sent', 'confirmation sent'],
  UNSUBSCRIBE:    ['unsubscribe', 'opt out', 'opted out']
};

/* ================================================================
   DEMO DATA
   ================================================================ */
const DEMO = {
  newStudent: {
    name: 'Sarah Johnson',
    email: '[email protected]',
    phone: '(917) 555-0123',
    dob: '2008-03-15',
    package: 'Premium Package',
    startDate: '2026-03-01',
    age: 17,
    studentId: 'FDS-0048'
  },
  summary: {
    processed: 1,
    welcomeEmails: 1,
    duplicatesFound: 0,
    underage: 1,
    flagged: 1
  }
};

/* ================================================================
   SETUP & AUTH
   ================================================================ */

function forceAuth() {
  SpreadsheetApp.openById(CONFIG.REGISTRATION_ID).getSheetByName('test_auth_ignore');
  SpreadsheetApp.openById(CONFIG.SCHEDULE_BOARD_ID).getSheetByName('test_auth_ignore');
  MailApp.getRemainingDailyQuota();
  Logger.log('โœ… All permissions authorized. You can close this now.');
}

function fullSetup() {
  const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  const sheet = getRegistrationSheet_();
  if (!sheet) { Logger.log('No registration sheet found.'); return; }

  // โ”€โ”€ Ensure required columns exist โ”€โ”€
  const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0].map(h => String(h || '').trim());
  const ensureCols = [
    { keywords: COL_KEYWORDS.STUDENT_ID, label: 'Student ID' },
    { keywords: COL_KEYWORDS.AGE, label: 'Age' },
    { keywords: COL_KEYWORDS.FLAGS, label: 'Flags' },
    { keywords: COL_KEYWORDS.PAYMENT_OK, label: 'Payment OK?' },
    { keywords: COL_KEYWORDS.WELCOME_SENT, label: 'Welcome Sent' },
    { keywords: COL_KEYWORDS.UNSUBSCRIBE, label: 'Unsubscribe' }
  ];

  const lowerHeaders = headers.map(h => h.toLowerCase().trim());
  let added = 0;
  for (const col of ensureCols) {
    if (findCol_(lowerHeaders, col.keywords) < 0) {
      const nextCol = sheet.getLastColumn() + 1;
      sheet.getRange(1, nextCol).setValue(col.label).setFontWeight('bold');
      lowerHeaders.push(col.label.toLowerCase());
      added++;
    }
  }
  if (added > 0) Logger.log('โœ… Added ' + added + ' new column(s) to registration sheet.');

  // โ”€โ”€ Create "Registration Log" sheet โ”€โ”€
  let logSheet = ss.getSheetByName('Registration Log');
  if (!logSheet) {
    logSheet = ss.insertSheet('Registration Log');
    logSheet.appendRow([
      'Timestamp', 'Student Name', 'Email', 'Student ID', 'Package',
      'Age', 'Flags', 'Welcome Email', 'Admin Notified'
    ]);
    logSheet.getRange('1:1').setFontWeight('bold');
    logSheet.setFrozenRows(1);
    Logger.log('โœ… Created "Registration Log" sheet.');
  }

  // โ”€โ”€ Triggers (clean old first) โ”€โ”€
  ScriptApp.getProjectTriggers().forEach(t => {
    if (['onFormSubmit', 'processAllRows'].includes(t.getHandlerFunction())) {
      ScriptApp.deleteTrigger(t);
    }
  });

  // Form submit trigger
  ScriptApp.newTrigger('onFormSubmit')
    .forSpreadsheet(CONFIG.REGISTRATION_ID)
    .onFormSubmit()
    .create();
  Logger.log('โœ… Form submit trigger created.');

  Logger.log('โœ… Student Registration setup complete.');
}

/* ================================================================
   FORM SUBMIT HANDLER
   ================================================================ */

function onFormSubmit(e) {
  try {
    if (CONFIG.DEMO_MODE) {
      Logger.log('๐ŸŽญ DEMO MODE โ€” would process new registration: ' + DEMO.newStudent.name);
      return;
    }

    let sheet = null;
    let newRowIndex = -1;

    if (e && e.range) {
      sheet = e.range.getSheet();
      newRowIndex = e.range.getRow();
    }

    if (!sheet) {
      sheet = getRegistrationSheet_();
    }

    if (!sheet || sheet.getLastRow() < 2) return;

    if (newRowIndex > 1) {
      // Process only the new row
      processRow_(sheet, newRowIndex);
    } else {
      // Fallback: process all unprocessed rows
      processAllRows();
    }
  } catch (err) {
    Logger.log('onFormSubmit error: ' + (err.message || err));
    notifyAdmin_('Registration Error', 'onFormSubmit failed: ' + String(err.message || err).substring(0, 500));
    throw err;
  }
}

/** Process all rows that haven't been processed yet (no Student ID). */
function processAllRows() {
  if (CONFIG.DEMO_MODE) {
    Logger.log('๐ŸŽญ DEMO MODE โ€” would process all unprocessed rows.');
    return;
  }

  const sheet = getRegistrationSheet_();
  if (!sheet || sheet.getLastRow() < 2) return;

  const data = sheet.getDataRange().getValues();
  const headers = data[0].map(h => String(h || '').toLowerCase().trim());
  const idCol = findCol_(headers, COL_KEYWORDS.STUDENT_ID);

  let processed = 0;
  for (let r = 1; r < data.length; r++) {
    const hasId = idCol >= 0 && String(data[r][idCol] || '').trim();
    if (!hasId) {
      processRow_(sheet, r + 1);
      processed++;
    }
  }
  Logger.log('Processed ' + processed + ' new row(s).');
}

/* ================================================================
   CORE: PROCESS A SINGLE ROW
   ================================================================ */

function processRow_(sheet, rowIndex) {
  // Re-read headers fresh (columns may have been added by fullSetup)
  const headerRow = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
  const headers = headerRow.map(h => String(h || '').toLowerCase().trim());
  const row = sheet.getRange(rowIndex, 1, 1, sheet.getLastColumn()).getValues()[0];

  // โ”€โ”€ Find columns โ”€โ”€
  const cols = {};
  for (const [key, keywords] of Object.entries(COL_KEYWORDS)) {
    cols[key] = findCol_(headers, keywords);
  }

  // โ”€โ”€ Read values โ”€โ”€
  const getVal = (colKey) => cols[colKey] >= 0 ? String(row[cols[colKey]] || '').trim() : '';
  const getRaw = (colKey) => cols[colKey] >= 0 ? row[cols[colKey]] : null;

  const name  = sanitize_(getVal('NAME'));
  const email = getVal('EMAIL').toLowerCase();
  const phone = getVal('PHONE');
  const dob   = getRaw('DOB');
  const pkg   = getVal('PACKAGE');
  const startDate = getRaw('START_DATE');
  const paymentStatus = getVal('PAYMENT_STATUS').toLowerCase();
  const amountPaid = parseAmount_(getRaw('AMOUNT_PAID'));

  // โ”€โ”€ Skip if already has Student ID โ”€โ”€
  if (getVal('STUDENT_ID')) return;

  // โ”€โ”€ Skip if welcome already sent โ”€โ”€
  if (getVal('WELCOME_SENT').toUpperCase() === 'YES') return;

  const flags = [];
  const tz = CONFIG.TIMEZONE;

  // โ”€โ”€ Validate required fields โ”€โ”€
  if (!name) flags.push('โš ๏ธ Missing name');
  if (!email) flags.push('โš ๏ธ Missing email');
  else if (!isValidEmail_(email)) flags.push('โš ๏ธ Invalid email format');
  if (!phone) flags.push('โš ๏ธ Missing phone');

  // โ”€โ”€ Normalize dates โ”€โ”€
  if (cols.TIMESTAMP >= 0 && getRaw('TIMESTAMP')) {
    const norm = normalizeDate_(getRaw('TIMESTAMP'), tz);
    if (norm) setCellValue_(sheet, rowIndex, cols.TIMESTAMP, norm);
  }
  if (cols.DOB >= 0 && dob) {
    const norm = normalizeDate_(dob, tz);
    if (norm) setCellValue_(sheet, rowIndex, cols.DOB, norm);
  }
  if (cols.START_DATE >= 0 && startDate) {
    const norm = normalizeDate_(startDate, tz);
    if (norm) setCellValue_(sheet, rowIndex, cols.START_DATE, norm);
  }

  // โ”€โ”€ Normalize phone โ”€โ”€
  if (cols.PHONE >= 0 && phone) {
    setCellValue_(sheet, rowIndex, cols.PHONE, normalizePhone_(phone));
  }

  // โ”€โ”€ Calculate age โ”€โ”€
  let age = null;
  if (dob) {
    age = calculateAge_(dob);
    if (age !== null && cols.AGE >= 0) {
      setCellValue_(sheet, rowIndex, cols.AGE, age);
    }
    if (age !== null && age < CONFIG.MIN_DRIVING_AGE) {
      flags.push('๐Ÿ”ด Under ' + CONFIG.MIN_DRIVING_AGE + ' โ€” cannot drive');
    } else if (age !== null && age < CONFIG.MINOR_AGE) {
      flags.push('๐ŸŸก Minor (under ' + CONFIG.MINOR_AGE + ') โ€” needs parent/guardian');
      if (!getVal('PARENT_NAME') && !getVal('PARENT_EMAIL')) {
        flags.push('โš ๏ธ No parent/guardian info on file');
      }
    }
  }

  // โ”€โ”€ Duplicate detection โ”€โ”€
  const isDuplicate = checkDuplicate_(sheet, headers, rowIndex, email, name);
  if (isDuplicate) {
    flags.push('๐Ÿ”ต Possible duplicate registration');
  }

  // โ”€โ”€ Payment OK? โ”€โ”€
  let paymentOk = 'Pending';
  if (paymentStatus === 'paid' && amountPaid > 0) {
    paymentOk = 'Yes';
  } else if (paymentStatus === 'paid' && amountPaid <= 0) {
    paymentOk = 'Check: Paid but $0';
  } else if (paymentStatus === 'unpaid' || paymentStatus === 'no') {
    paymentOk = 'No';
  } else if (paymentStatus === 'pending' || paymentStatus === 'partial') {
    paymentOk = 'Pending';
  } else if (!paymentStatus) {
    paymentOk = 'Pending';
  }
  if (cols.PAYMENT_OK >= 0) {
    setCellValue_(sheet, rowIndex, cols.PAYMENT_OK, paymentOk);
  }

  // โ”€โ”€ Generate Student ID โ”€โ”€
  const studentId = generateStudentId_(sheet, headers);
  if (cols.STUDENT_ID >= 0) {
    setCellValue_(sheet, rowIndex, cols.STUDENT_ID, studentId);
  }

  // โ”€โ”€ Set flags โ”€โ”€
  if (cols.FLAGS >= 0) {
    setCellValue_(sheet, rowIndex, cols.FLAGS, flags.join(' | '));
  }

  // โ”€โ”€ Send welcome email โ”€โ”€
  let welcomeSent = false;
  if (email && isValidEmail_(email) && !isDuplicate) {
    const unsubVal = getVal('UNSUBSCRIBE').toLowerCase();
    if (unsubVal !== 'yes' && unsubVal !== 'true') {
      welcomeSent = sendWelcomeEmail_(name, email, pkg, studentId, startDate, age);
    }
  }
  if (cols.WELCOME_SENT >= 0) {
    setCellValue_(sheet, rowIndex, cols.WELCOME_SENT, welcomeSent ? 'Yes' : (isDuplicate ? 'Duplicate' : 'No'));
  }

  // โ”€โ”€ Admin notification โ”€โ”€
  const adminNotified = sendAdminNotification_(name, email, phone, pkg, studentId, age, flags, isDuplicate);

  // โ”€โ”€ Log it โ”€โ”€
  logRegistration_(name, email, studentId, pkg, age, flags, welcomeSent, adminNotified);

  Logger.log('โœ… Processed: ' + name + ' (' + studentId + ')' + (flags.length ? ' โ€” ' + flags.join(', ') : ''));
}

/* ================================================================
   STUDENT ID GENERATOR
   ================================================================ */

function generateStudentId_(sheet, headers) {
  const idCol = findCol_(headers, COL_KEYWORDS.STUDENT_ID);
  let maxNum = 0;

  if (idCol >= 0 && sheet.getLastRow() > 1) {
    const ids = sheet.getRange(2, idCol + 1, sheet.getLastRow() - 1, 1).getValues();
    for (const row of ids) {
      const match = String(row[0] || '').match(/FDS-(\d+)/);
      if (match) {
        const num = parseInt(match[1], 10);
        if (num > maxNum) maxNum = num;
      }
    }
  }

  const next = maxNum + 1;
  return 'FDS-' + String(next).padStart(4, '0');
}

/* ================================================================
   DUPLICATE DETECTION
   ================================================================ */

function checkDuplicate_(sheet, headers, currentRow, email, name) {
  if (!email && !name) return false;

  const emailCol = findCol_(headers, COL_KEYWORDS.EMAIL);
  const nameCol  = findCol_(headers, COL_KEYWORDS.NAME);
  if (emailCol < 0 && nameCol < 0) return false;

  const lastRow = sheet.getLastRow();
  if (lastRow <= 1) return false;

  const data = sheet.getRange(2, 1, lastRow - 1, sheet.getLastColumn()).getValues();

  for (let r = 0; r < data.length; r++) {
    const dataRowIndex = r + 2;
    if (dataRowIndex === currentRow) continue;

    // Email match
    if (email && emailCol >= 0) {
      const existingEmail = String(data[r][emailCol] || '').toLowerCase().trim();
      if (existingEmail === email) return true;
    }

    // Fuzzy name match
    if (name && nameCol >= 0) {
      const existingName = String(data[r][nameCol] || '').toLowerCase().trim();
      if (existingName === name.toLowerCase()) return true;
      if (levenshtein_(existingName, name.toLowerCase()) <= 2) return true;
    }
  }

  return false;
}

/* ================================================================
   WELCOME EMAIL (Mission Control theme)
   ================================================================ */

function sendWelcomeEmail_(studentName, email, pkg, studentId, startDate, age) {
  try {
    const firstName = (studentName || '').split(' ')[0] || studentName;
    const f    = esc_(firstName);
    const sch  = esc_(CONFIG.SCHOOL_NAME);
    const tag  = esc_(CONFIG.SCHOOL_TAGLINE);
    const p    = esc_(pkg || 'Your selected package');
    const sid  = esc_(studentId);
    const unsub = 'mailto:' + CONFIG.ADMIN_EMAIL + '?subject=Unsubscribe&body=Please%20remove%20' + encodeURIComponent(email) + '%20from%20emails.';

    let startStr = '';
    if (startDate) {
      const d = startDate instanceof Date ? startDate : new Date(startDate);
      if (!isNaN(d.getTime())) {
        startStr = Utilities.formatDate(d, CONFIG.TIMEZONE, 'MMMM d, yyyy');
      }
    }

    const isMinor = age !== null && age < CONFIG.MINOR_AGE;

    const subject = 'Welcome to ' + CONFIG.SCHOOL_NAME + ', ' + firstName + '! ๐ŸŽ‰';

    const htmlBody = '<!DOCTYPE html><html><head><meta charset="utf-8"></head>'
      + '<body style="margin:0;padding:0;background:#000;font-family:Arial,Helvetica,sans-serif;">'
      + '<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#000;"><tr><td align="center">'
      + '<table width="600" cellpadding="0" cellspacing="0" border="0">'
      // Header
      + '<tr><td style="background:#0d0d0d;border-bottom:2px solid #ff2d2d;padding:30px 24px;text-align:center;">'
      + '<h1 style="color:#fff;font-size:20px;margin:0 0 4px;">' + sch + '</h1>'
      + '<p style="color:rgba(255,255,255,0.5);font-size:12px;margin:0;">' + tag + '</p>'
      + '</td></tr>'
      // Body
      + '<tr><td style="background:#0d0d0d;padding:32px 24px;">'
      + '<div style="text-align:center;margin-bottom:24px;">'
      + '<div style="font-size:48px;margin-bottom:12px;">๐Ÿš—</div>'
      + '<div style="font-size:26px;font-weight:800;color:#fff;">Welcome, ' + f + '!</div>'
      + '<p style="font-size:14px;color:rgba(255,255,255,0.5);margin:8px 0 0;">Your journey to the open road starts here.</p>'
      + '</div>'
      // Confirmation card
      + '<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:rgba(255,45,45,0.06);border:1px solid rgba(255,45,45,0.15);border-radius:12px;">'
      + '<tr><td style="padding:20px 24px;">'
      + '<div style="font-size:12px;color:#ff2d2d;font-weight:700;text-transform:uppercase;letter-spacing:2px;margin-bottom:12px;">Registration Confirmed</div>'
      + confirmRow_('Student ID', sid)
      + confirmRow_('Program', p)
      + (startStr ? confirmRow_('Start Date', esc_(startStr)) : '')
      + (age !== null ? confirmRow_('Age', esc_(String(age))) : '')
      + '</td></tr></table>'
      // What to expect
      + '<div style="margin-top:24px;">'
      + '<div style="font-size:14px;font-weight:700;color:#fff;margin-bottom:12px;">๐Ÿ“‹ What\'s Next</div>'
      + '<table width="100%" cellpadding="0" cellspacing="6" border="0">'
      + stepRow_('1', 'We\'ll reach out to schedule your first lesson')
      + stepRow_('2', 'Bring your learner permit and a valid photo ID')
      + stepRow_('3', 'Arrive 10 minutes early for your first session')
      + (isMinor ? stepRow_('โš ๏ธ', 'Since you\'re under 18, a parent/guardian must sign the enrollment form') : '')
      + '</table>'
      + '</div>'
      // Save your ID
      + '<div style="margin-top:24px;padding:16px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);border-radius:8px;text-align:center;">'
      + '<p style="font-size:12px;color:rgba(255,255,255,0.5);margin:0;">๐Ÿ“Œ Save your Student ID: <strong style="color:#ff2d2d;font-size:14px;">' + sid + '</strong></p>'
      + '<p style="font-size:11px;color:rgba(255,255,255,0.3);margin:4px 0 0;">Use this to check your progress in the Student Portal.</p>'
      + '</div>'
      + '</td></tr>'
      // Footer
      + '<tr><td style="background:#000;border-top:1px solid rgba(255,255,255,0.05);padding:20px 24px;text-align:center;">'
      + '<p style="font-size:12px;color:rgba(255,255,255,0.3);margin:0;">Questions? Contact us at ' + esc_(CONFIG.SCHOOL_PHONE) + '</p>'
      + '<p style="font-size:11px;color:rgba(255,255,255,0.25);margin:4px 0 0;">' + sch + ' โ€” ' + esc_(CONFIG.SCHOOL_ADDRESS) + '</p>'
      + '<p style="font-size:10px;color:rgba(255,255,255,0.15);margin:8px 0 0;"><a href="' + unsub + '" style="color:rgba(255,255,255,0.15);text-decoration:underline;">Unsubscribe</a></p>'
      + '</td></tr>'
      + '</table></td></tr></table></body></html>';

    const plainBody = 'Welcome to ' + CONFIG.SCHOOL_NAME + ', ' + firstName + '!\n\n'
      + 'Your registration is confirmed.\n'
      + 'Student ID: ' + studentId + '\n'
      + 'Program: ' + (pkg || 'TBD') + '\n'
      + (startStr ? 'Start Date: ' + startStr + '\n' : '')
      + '\nWe\'ll be in touch to schedule your first lesson.\n'
      + 'Questions? Call ' + CONFIG.SCHOOL_PHONE + '\n\n'
      + 'โ€” ' + CONFIG.SCHOOL_NAME;

    MailApp.sendEmail({
      to: email,
      subject: subject,
      body: plainBody,
      htmlBody: htmlBody,
      name: CONFIG.SCHOOL_NAME
    });
    return true;
  } catch (e) {
    Logger.log('Welcome email failed for ' + studentName + ': ' + e.message);
    return false;
  }
}

function confirmRow_(label, value) {
  return '<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-bottom:8px;"><tr>'
    + '<td width="35%" style="font-size:12px;color:rgba(255,255,255,0.4);">' + label + '</td>'
    + '<td style="font-size:13px;color:#fff;font-weight:600;">' + value + '</td>'
    + '</tr></table>';
}

function stepRow_(num, text) {
  return '<tr><td style="padding:8px 12px;background:rgba(255,255,255,0.03);border-radius:8px;">'
    + '<table cellpadding="0" cellspacing="0" border="0"><tr>'
    + '<td style="width:28px;vertical-align:top;">'
    + '<div style="width:22px;height:22px;background:#ff2d2d;border-radius:50%;text-align:center;line-height:22px;font-size:11px;color:#fff;font-weight:700;">' + num + '</div>'
    + '</td>'
    + '<td style="font-size:13px;color:rgba(255,255,255,0.6);padding-left:8px;">' + esc_(text) + '</td>'
    + '</tr></table></td></tr>';
}

/* ================================================================
   ADMIN NOTIFICATION
   ================================================================ */

function sendAdminNotification_(name, email, phone, pkg, studentId, age, flags, isDuplicate) {
  if (!CONFIG.ADMIN_EMAIL) return false;
  try {
    const subject = (isDuplicate ? '๐Ÿ”ต Duplicate? ' : '๐Ÿ†• ') + 'New Registration: ' + name + ' (' + studentId + ')';
    const flagText = flags.length ? '\n\nโš ๏ธ Flags:\n' + flags.join('\n') : '\n\nโœ… No flags.';

    const body = 'New student registration:\n\n'
      + 'Name: ' + name + '\n'
      + 'Email: ' + email + '\n'
      + 'Phone: ' + phone + '\n'
      + 'Package: ' + (pkg || 'Not specified') + '\n'
      + 'Student ID: ' + studentId + '\n'
      + (age !== null ? 'Age: ' + age + '\n' : '')
      + flagText;

    MailApp.sendEmail({
      to: CONFIG.ADMIN_EMAIL,
      subject: subject,
      body: body,
      name: CONFIG.SCHOOL_NAME
    });
    return true;
  } catch (e) {
    Logger.log('Admin notification failed: ' + e.message);
    return false;
  }
}

/* ================================================================
   REGISTRATION LOG
   ================================================================ */

function logRegistration_(name, email, studentId, pkg, age, flags, welcomeSent, adminNotified) {
  try {
    const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
    let logSheet = ss.getSheetByName('Registration Log');
    if (!logSheet) return;

    logSheet.appendRow([
      new Date(), name, email, studentId, pkg || '',
      age || '', flags.join(' | '), welcomeSent ? 'Yes' : 'No', adminNotified ? 'Yes' : 'No'
    ]);
  } catch (e) {
    Logger.log('Logging failed: ' + e.message);
  }
}

/* ================================================================
   HELPERS
   ================================================================ */

function getRegistrationSheet_() {
  const ss = SpreadsheetApp.openById(CONFIG.REGISTRATION_ID);
  if (CONFIG.REGISTRATION_SHEET_TAB) {
    const sheet = ss.getSheetByName(CONFIG.REGISTRATION_SHEET_TAB);
    if (sheet) return sheet;
  }
  return ss.getSheets()[0];
}

function setCellValue_(sheet, rowIndex, colIndex, value) {
  sheet.getRange(rowIndex, colIndex + 1).setValue(value);
}

/** Header contains any candidate (.includes() matching). Returns 0-based index or -1. */
function findCol_(headers, candidates) {
  for (let i = 0; i < headers.length; i++) {
    const h = String(headers[i] || '').toLowerCase().trim();
    for (const kw of candidates) {
      if (kw && h.includes(String(kw).toLowerCase().trim())) return i;
    }
  }
  return -1;
}

function normalizeDate_(value, tz) {
  if (value == null || value === '') return '';
  tz = tz || CONFIG.TIMEZONE;

  if (value instanceof Date) {
    return isNaN(value.getTime()) ? '' : Utilities.formatDate(value, tz, 'yyyy-MM-dd');
  }

  const s = String(value).trim();
  let d = new Date(s);
  if (!isNaN(d.getTime())) return Utilities.formatDate(d, tz, 'yyyy-MM-dd');

  const match = s.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/);
  if (match) {
    d = new Date(parseInt(match[3], 10), parseInt(match[1], 10) - 1, parseInt(match[2], 10));
    if (!isNaN(d.getTime())) return Utilities.formatDate(d, tz, 'yyyy-MM-dd');
  }
  return '';
}

function normalizePhone_(phone) {
  if (!phone) return '';
  const digits = String(phone).replace(/\D/g, '');
  if (digits.length === 10) return digits.slice(0, 3) + '-' + digits.slice(3, 6) + '-' + digits.slice(6);
  if (digits.length === 11 && digits[0] === '1') return digits.slice(1, 4) + '-' + digits.slice(4, 7) + '-' + digits.slice(7);
  return String(phone).trim();
}

function calculateAge_(dob) {
  let d = dob instanceof Date ? dob : new Date(dob);
  if (isNaN(d.getTime())) return null;
  const now = new Date();
  let age = now.getFullYear() - d.getFullYear();
  const m = now.getMonth() - d.getMonth();
  if (m < 0 || (m === 0 && now.getDate() < d.getDate())) age--;
  return age >= 0 && age < 120 ? age : null;
}

function isValidEmail_(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email || ''));
}

function parseAmount_(value) {
  if (value == null || value === '') return 0;
  const n = parseFloat(String(value).replace(/[\$,]/g, '').trim());
  return isNaN(n) ? 0 : n;
}

function sanitize_(str) {
  return String(str || '').replace(/[<>{}()\[\]\\\/]/g, '').substring(0, 150).trim();
}

function esc_(str) {
  if (str == null) return '';
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

function levenshtein_(a, b) {
  const m = a.length, n = b.length;
  const dp = Array.from({ length: m + 1 }, (_, i) => i);
  for (let j = 1; j <= n; j++) {
    let prev = dp[0];
    dp[0] = j;
    for (let i = 1; i <= m; i++) {
      const temp = dp[i];
      dp[i] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[i], dp[i - 1]);
      prev = temp;
    }
  }
  return dp[m];
}

function notifyAdmin_(subject, body) {
  if (!CONFIG.ADMIN_EMAIL) return;
  try {
    MailApp.sendEmail(CONFIG.ADMIN_EMAIL, subject, body, { name: CONFIG.SCHOOL_NAME });
  } catch (_) { /* silent */ }
}

/* ================================================================
   MANUAL TOOLS
   ================================================================ */

/** Re-process all rows without a Student ID. */
function reprocessMissing() {
  if (CONFIG.DEMO_MODE) {
    Logger.log('๐ŸŽญ DEMO MODE โ€” would reprocess all rows missing Student ID.');
    return;
  }
  processAllRows();
}

/** Test: log demo registration. */
function testDemoRegistration() {
  Logger.log('๐ŸŽญ Demo registration:');
  Logger.log('  Student: ' + DEMO.newStudent.name);
  Logger.log('  Email: ' + DEMO.newStudent.email);
  Logger.log('  Student ID: ' + DEMO.newStudent.studentId);
  Logger.log('  Package: ' + DEMO.newStudent.package);
  Logger.log('  Age: ' + DEMO.newStudent.age + ' (minor โ€” needs parent/guardian)');
  Logger.log('  Welcome email would be sent โœ…');
  Logger.log('  Admin notification would be sent โœ…');
}