Source Code Viewer

Google Apps Script - Session Audit Tool


const ALLOWED_USERS = [EMAIL ADDRESSES HERE];

function isUserAuthorized() {
  const currentUser = Session.getActiveUser().getEmail();
  return ALLOWED_USERS.includes(currentUser);
}

function restrictedFunction() {
  if (!isUserAuthorized()) {
    console.error('You are not authorized to run this function');
    return;
  }

  createButton();
}

// Main function to create the custom menu
function createButton() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu('Session Audit Tool')
    .addItem('Begin Audit', 'showSidebar')
    .addToUi();
}

// Function to display the custom sidebar
function showSidebar() {
  const html = HtmlService.createHtmlOutputFromFile('Session Audit')
    .setTitle('Session Audit Tool');
  SpreadsheetApp.getUi().showSidebar(html);
}

function runAudit() {

  var sheetId = 'SPREADSHEET ID';
  var targetSpreadsheet = SpreadsheetApp.openById(sheetId);
  var resultSheet = targetSpreadsheet.getSheetByName('SHEET NAME');
  var expectedHeaders = ['Tutor ID', 'Due Date', 'Notes'];
  var headersMissing = true;
  var defaultPriorityTutors = new Map();

  if (resultSheet) {

    var existingData = resultSheet.getDataRange().getValues();
    existingData.slice(1).forEach(row => {
      var tutorId = row[0];
      var dueDate = new Date(row[1]);
      var notes = row[2];

      if (notes === 'Default priority: No valid QA or session audit data found.') {
        defaultPriorityTutors.set(tutorId, { dueDate, notes });
        Logger.log("Default Priority Tutors Map: " + JSON.stringify([...defaultPriorityTutors]));
      }
    });
    var existingHeaders = resultSheet.getRange(1, 1, 1, resultSheet.getLastColumn()).getValues()[0];
    headersMissing = expectedHeaders.some(header => !existingHeaders.includes(header));

    if (resultSheet.getLastRow() > 1) {
      resultSheet.getRange(2, 1, resultSheet.getLastRow() - 1, resultSheet.getLastColumn()).clearContent();
    }
  }

  // Add headers to "Audit_Results" sheet if missing
  if (headersMissing) {
    resultSheet.insertRowBefore(1);
    resultSheet.getRange(1, 1, 1, expectedHeaders.length).setValues([expectedHeaders]);
  }
  var sheetId = 'SPREADSHEET ID';
  var targetSpreadsheet = SpreadsheetApp.openById(sheetId);
  var resultSheet = targetSpreadsheet.getSheetByName('SHEET NAME');
  if (resultSheet) {
    var lastRowExternal = resultSheet.getLastRow();
    if (lastRowExternal > 1) {
      resultSheet.getRange(2, 1, lastRowExternal - 1, resultSheet.getLastColumn()).clearContent();
    }
  }


  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const districtTabs = [LIST OF SCHOOL DISTRICTS];

  Logger.log('Processing district tabs: ' + districtTabs.join(', '));

  // Call updateStatusAudit for each district tab
  for (let i = 0; i < districtTabs.length; i++) {
    Logger.log(`Calling updateStatusAudit for sheet: ${districtTabs[i]}`);
    updateStatusAudit(districtTabs[i]);
  }

  const tutorAuditResults = new Map(); // Stores results for each tutor

  // Step 1: Process each district tab and collect session audit data
  districtTabs.forEach(tab => {
    const sheet = ss.getSheetByName(tab);
    if (!sheet) return;

    const data = sheet.getDataRange().getValues();

    if (data.length < 2 || data.slice(2).every(row => row.every(cell => cell === ""))) {
      Logger.log(`Skipping empty or header-only sheet: ${tab}`);
      return;
    }
    const headers = data[1];
    const rows = data.slice(2);

    const tutorIdIndex = headers.indexOf('Tutor ID');
    const contractStartDateIndex = headers.indexOf('Contract Start Date');
    const nextSessionAuditIndex = headers.indexOf('Next Session Audit');
    const statusAuditIndex = headers.indexOf('Status of Audit'); // Get the index of the 'Status of Audit' column

    Logger.log(`Headers in ${tab}: ${headers.join(', ')}`);
    Logger.log(`Status of Audit column index: ${statusAuditIndex}`); // Log the index of the column

    if (tutorIdIndex === -1 || contractStartDateIndex === -1 || nextSessionAuditIndex === -1 || statusAuditIndex === -1) {
      Logger.log(`Headers missing required columns in sheet: ${tab}`);
      return;
    }

    const processedTutors = processRows(rows, tutorIdIndex, contractStartDateIndex, ss, defaultPriorityTutors);

    // Store the earliest audit due date for each tutor
    processedTutors.forEach(({ tutorId, dueDate, priorityLevel, notes }) => {
      if (!tutorAuditResults.has(tutorId) || tutorAuditResults.get(tutorId).dueDate > dueDate) {
        tutorAuditResults.set(tutorId, { dueDate, priorityLevel, notes });
      }
    });
  });

  // Step 2: Apply the calculated next session audit dates and status back to district tabs
  districtTabs.forEach(tab => {
    const sheet = ss.getSheetByName(tab);
    if (!sheet) return;

    const data = sheet.getDataRange().getValues();
    if (data.length < 2) return;

    const headers = data[1];
    const rows = data.slice(2);

    const tutorIdIndex = headers.indexOf('Tutor ID');
    const nextSessionAuditIndex = headers.indexOf('Next Session Audit');
    const statusAuditIndex = headers.indexOf('Status of Audit');
    const notesIndex = headers.indexOf('Notes'); // Add this line to capture the 'Notes' column index

    if (tutorIdIndex === -1 || nextSessionAuditIndex === -1 || statusAuditIndex === -1) {
      Logger.log(`Headers missing required columns in sheet: ${tab}`);
      return;
    }

    const updates = [];
    const statusUpdates = [];
    const notesUpdates = []; // To hold updates for 'Notes' column

    rows.forEach((row, rowIndex) => {
      const tutorId = row[tutorIdIndex];
      const tutorAudit = tutorAuditResults.get(tutorId);

      if (!tutorId || !tutorAudit) {
        updates.push([row[nextSessionAuditIndex]]); // Keep existing value
        statusUpdates.push(['Not Started']);
        notesUpdates.push(['']); // Empty note if no audit found
      } else {
        updates.push([tutorAudit.dueDate]); // Update with next session audit date
        statusUpdates.push(['Not Started']);
        notesUpdates.push([tutorAudit.notes]); // Add the notes for this tutor
      }

      // Log the row update for debugging
      Logger.log(`Row ${rowIndex + 1}: Tutor ID: ${tutorId}, Next Session Audit: ${tutorAudit ? tutorAudit.dueDate : 'N/A'}, Status: Not Started, Notes: ${tutorAudit ? tutorAudit.notes : ''}`);
    });

    const startRow = 3; // Data starts from row 3 (excluding headers)

    // Update 'Next Session Audit' column
    const nextSessionAuditRange = sheet.getRange(startRow, nextSessionAuditIndex + 1, rows.length, 1);
    nextSessionAuditRange.setValues(updates);

    // **Force the update of the 'Status of Audit' column explicitly**
    const statusAuditRange = sheet.getRange(startRow, statusAuditIndex + 1, rows.length, 1);
    Logger.log(`Updating Status of Audit column at range: ${statusAuditRange.getA1Notation()}`); // Log the exact range being updated
    statusAuditRange.setValues(statusUpdates);

    // Update 'Notes' column with the notes for each tutor
    if (notesIndex !== -1) {
      const notesRange = sheet.getRange(startRow, notesIndex + 1, rows.length, 1);
      notesRange.setValues(notesUpdates); // Set notes for each row
    }

    // Log the range being updated for debugging
    Logger.log(`Updated status and notes columns in range: ${statusAuditRange.getA1Notation()}`);
  });

  processSessionAudits();
  defaultPriorityTutorList();
}

function updateStatusAudit(sheetName) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(sheetName);  // Use sheet name passed as an argument

  if (!sheet) {
    Logger.log('Sheet not found for ' + sheetName);
    return;
  }

  Logger.log('Processing sheet: ' + sheetName);  // Log when processing starts

  const data = sheet.getDataRange().getValues();
  const headers = data[1];

  const statusAuditIndex = headers.indexOf('Status of Audit');

  if (statusAuditIndex === -1) {
    Logger.log('Status of Audit column not found in ' + sheetName);
    return;
  }

  const rows = data.slice(2); // Skip header row
  const statusUpdates = rows.map(() => ['Not Started']);  // Set all values to 'Not Started'

  Logger.log('Updating "Status of Audit" column in sheet: ' + sheetName);

  const startRow = 3;  // Data starts from row 3 (excluding headers)
  const statusAuditRange = sheet.getRange(startRow, statusAuditIndex + 1, rows.length, 1);

  // Perform the update
  statusAuditRange.setValues(statusUpdates);

  Logger.log('Updated "Status of Audit" column successfully in sheet: ' + sheetName);
}

// Function to process rows and extract tutor data, including tutor name
function processRows(rows, tutorIdIndex, contractStartDateIndex, ss, defaultPriorityTutors) {
  const now = new Date();
  const tutorQAData = ss.getSheetByName('SHEET NAME').getDataRange().getValues();
  const qaSessionAuditData = ss.getSheetByName('SHEET NAME').getDataRange().getValues();
  const tutorConcernsData = ss.getSheetByName('SHEET NAME').getDataRange().getValues();

  const qaHeaders = tutorQAData[0];
  const sessionAuditHeaders = qaSessionAuditData[0];
  const concernHeaders = tutorConcernsData[0];

  const tutorQA = tutorQAData.slice(1);
  const sessionAudits = qaSessionAuditData.slice(1);
  const tutorConcerns = tutorConcernsData.slice(1);

  const averageScoreIndex = qaHeaders.indexOf('Average Session Audit Score (out of 12)');
  const latestScoreIndex = qaHeaders.indexOf('Latest Session Audit Score (out of 12)');
  const sessionDateIndex = sessionAuditHeaders.indexOf('Timestamp');
  const qaTutorIdIndex = qaHeaders.indexOf('Tutor ID');
  const sessionAuditTutorIdIndex = sessionAuditHeaders.indexOf('Tutor ID of the tutor receiving the audit');
  const concernTutorIdIndex = concernHeaders.indexOf('1099 Tutor ID');
  const concernDateIndex = concernHeaders.indexOf('Date of Concern');

  const tutorResults = new Map();

  // Create a map of tutor names based on Tutor ID from 'Tutor_QA'
  const tutorNameMap = new Map();
  tutorQA.forEach(row => {
    const tutorId = row[qaTutorIdIndex];
    const firstName = row[qaHeaders.indexOf('Tutor First Name')];
    const lastName = row[qaHeaders.indexOf('Tutor Last Name')];
    tutorNameMap.set(tutorId, `${firstName} ${lastName}`);
  });

  rows.forEach(row => {
    const tutorId = row[tutorIdIndex];
    const rawContractDate = row[contractStartDateIndex];
    if (!rawContractDate || isNaN(new Date(rawContractDate))) {
      Logger.log(`Invalid contract start date for tutor ${tutorId}: ${rawContractDate}`);
      return;
    }
    const contractStartDate = new Date(rawContractDate);


    if (!tutorId || isNaN(contractStartDate)) return;

    let dueDate = null;
    let priorityLevel = 'Low';
    let notes = '';

    // Check for entries in QA_Tutor_Concerns sheet
    const tutorConcernRecord = tutorConcerns.find(r => r[concernTutorIdIndex] === tutorId && r[concernDateIndex]);
    if (tutorConcernRecord) {
      dueDate = new Date(now);
      dueDate.setDate(dueDate.getDate() + (7 - dueDate.getDay())); // Set to next Sunday
      priorityLevel = 'High';
      notes = 'Audit due based on concern entry. Scheduled every week on Sunday.';
    }

    // Proceed with other checks if no concerns entry found
    if (!dueDate) {
      const contractDueDate = new Date(contractStartDate);
      contractDueDate.setDate(contractDueDate.getDate() + 7);

      const tutorQARecord = tutorQA.find(r => r[qaTutorIdIndex] === tutorId);
      const sessionAuditRecord = sessionAudits
        .filter(r => r[sessionAuditTutorIdIndex] === tutorId)
        .sort((a, b) => new Date(b[sessionDateIndex]) - new Date(a[sessionDateIndex]))[0];

      if (tutorQARecord && sessionAuditRecord) {
        const averageScore = parseFloat(tutorQARecord[averageScoreIndex]);
        const latestScore = parseFloat(tutorQARecord[latestScoreIndex]);
        const rawLastAuditDate = sessionAuditRecord[sessionDateIndex];
        if (!rawLastAuditDate || isNaN(new Date(rawLastAuditDate))) {
          Logger.log(`Invalid last audit date for tutor ${tutorId}: ${rawLastAuditDate}`);
          return;
        }
        const lastAuditDate = new Date(rawLastAuditDate);


        Logger.log(`Tutor ID: ${tutorId} - Average Score: ${averageScore} - Latest Score: ${latestScore} - Last Audit Date: ${lastAuditDate}`);

        if (latestScore === 12) {
          const threeWeeksDueDate = new Date(lastAuditDate);
          threeWeeksDueDate.setDate(threeWeeksDueDate.getDate() + 21);
          dueDate = threeWeeksDueDate;
          notes = 'Latest session audit was 12. Next audit due in 3 weeks.';
        } else if (latestScore < 12 && latestScore >= 10 && averageScore >= 10) {
          const twoWeeksDueDate = new Date(lastAuditDate);
          twoWeeksDueDate.setDate(twoWeeksDueDate.getDate() + 14);
          dueDate = twoWeeksDueDate;
          notes = 'Latest session audit > 10 but < 12. Average score > 10. Next audit due in 2 weeks.';
        } else {
          const nextAuditDates = [];
          if (averageScore < 10) {
            const avgDueDate = new Date(lastAuditDate);
            avgDueDate.setDate(avgDueDate.getDate() + 14);
            nextAuditDates.push(avgDueDate);
            notes += 'Average score < 10. ';
          }
          if (latestScore < 10) {
            const latestDueDate = new Date(lastAuditDate);
            latestDueDate.setDate(latestDueDate.getDate() + 7);
            nextAuditDates.push(latestDueDate);
            notes += 'Latest score < 10. ';
          }
          if (nextAuditDates.length > 0) {
            dueDate = nextAuditDates.sort((a, b) => a - b)[0];
            priorityLevel = 'High';
          }
        }
      }

      if (!dueDate) {
        if (defaultPriorityTutors.has(tutorId)) {
          const defaultPriorityData = defaultPriorityTutors.get(tutorId);
          if (!defaultPriorityData || isNaN(new Date(defaultPriorityData.dueDate))) {
            Logger.log(`Invalid default priority due date for tutor ${tutorId}: ${defaultPriorityData.dueDate}`);
            return;
          }
          dueDate = new Date(defaultPriorityData.dueDate);

          notes = defaultPriorityData.notes;
          Logger.log(`Using default priority data for Tutor ${tutorId}: Due Date - ${dueDate}, Notes - ${notes}`);
        } else {
          dueDate = new Date();
          dueDate.setDate(now.getDate() + 14);
          priorityLevel = 'High';
          notes = 'Default priority: No valid QA or session audit data found.';
        }
      }

    }

    Logger.log("Calculated Due Date for Tutor " + tutorId + ": " + dueDate);
    tutorResults.set(tutorId, { tutorId, dueDate, priorityLevel, notes, tutorName: tutorNameMap.get(tutorId) });
  });

  // Append results to "Audit_Results" sheet
  const sheetId = 'SHEET ID';
  const targetSpreadsheet = SpreadsheetApp.openById(sheetId);

  // Access the "Audit_Results" sheet (create it if it doesn't exist)
  const resultSheet = targetSpreadsheet.getSheetByName('SHEET NAME') || targetSpreadsheet.insertSheet('Audit_Results');

  if (resultSheet.getLastRow() === 0) {
    resultSheet.appendRow(['Tutor ID', 'Tutor Name', 'Next Session Audit', 'Priority Level', 'Notes']);
  }

  // Create a set of existing tutor IDs in "Audit_Results"
  const lastRow = resultSheet.getLastRow();
  const existingTutorIds = new Set();
  if (lastRow > 1) {
    const tutorIdRange = resultSheet.getRange(2, 1, lastRow - 1, 1).getValues();
    tutorIdRange.forEach(row => existingTutorIds.add(row[0]));
  }

  const auditResults = Array.from(tutorResults.values());
  auditResults.forEach(result => {
    if (!existingTutorIds.has(result.tutorId)) {
      resultSheet.appendRow([result.tutorId, result.tutorName, result.dueDate.toISOString().split('T')[0], result.priorityLevel, result.notes]);
      existingTutorIds.add(result.tutorId); // Ensure tutor ID is added to the existing set
    }
  });

  // Sort the results by 'Next Session Audit' date
  const range = resultSheet.getRange(2, 1, resultSheet.getLastRow() - 1, resultSheet.getLastColumn());
  range.sort({ column: 3, ascending: true });

  return auditResults;
}

function processSessionAudits() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();

  // Load data from 'QA_Session_Audit_Form_Response' tab
  const formSheet = ss.getSheetByName("SHEET NAME");
  if (!formSheet) {
    Logger.log("The 'SHEET NAME' sheet is missing.");
    return;
  }

  const formData = formSheet.getDataRange().getValues();

  const formHeaders = formData[0];
  const tutorIdIndex = formHeaders.indexOf('Tutor ID of the tutor receiving the audit');
  const contractIndex = formHeaders.indexOf('Session Audit Contract');
  const pssNameIndex = formHeaders.indexOf('PSS/Auditor Name');

  if (tutorIdIndex === -1 || contractIndex === -1 || pssNameIndex === -1) {
    Logger.log("One or more required columns are missing in the QA form.");
    return;
  }

  // Updated contract-to-tab mapping
  const contractToTabMap = {CONTRACT NAME: SHEET NAME};

  const familyTutoringPSS = "SPECIFIC USER";

  let auditCounts = {};
  let pssCounts = {};

  // Process each row from the form responses to count Tutor ID + Contract combinations
  for (let i = 1; i < formData.length; i++) {
    let tutorId = formData[i][tutorIdIndex].toString().trim();
    let contract = formData[i][contractIndex].toString().trim();
    let pssName = formData[i][pssNameIndex].toString().trim();

    if (!tutorId || !contract || !pssName) continue;

    // Create a key from Tutor ID + Contract to count occurrences
    let key = tutorId + "_" + contract;
    if (!auditCounts[key]) auditCounts[key] = 0;

    // Increment the count for this Tutor ID + Contract combination
    auditCounts[key]++;

    // Track PSS counts
    if (!pssCounts[contract]) pssCounts[contract] = {};
    if (!pssCounts[contract][pssName]) pssCounts[contract][pssName] = {};
    if (!pssCounts[contract][pssName][tutorId]) pssCounts[contract][pssName][tutorId] = 0;

    pssCounts[contract][pssName][tutorId]++;
  }

  // Load the other Google Sheet for PSS information
  const pssSheet = SpreadsheetApp.openById("SPREADSHEET ID");
  const pssData = pssSheet.getSheetByName("SHEET NAME");
  if (!pssData) {
    Logger.log("The 'active_operations' sheet in the PSS document is missing.");
    return;
  }

  // Headers are in row 2
  const pssValues = pssData.getRange(2, 1, pssData.getLastRow() - 1, pssData.getLastColumn()).getValues();
  const pssHeaders = pssValues[0];
  const contractIndexPSS = pssHeaders.indexOf('Contract');
  const pssNameIndexPSS = pssHeaders.indexOf('Partnership Success Specialist');

  if (contractIndexPSS === -1 || pssNameIndexPSS === -1) {
    Logger.log("Required columns are missing in the PSS sheet.");
    return;
  }

  let contractToPSSMap = {};

  for (let i = 1; i < pssValues.length; i++) {
    let contract = pssValues[i][contractIndexPSS].toString().trim();
    let pssName = pssValues[i][pssNameIndexPSS].toString().trim();

    if (!contract || !pssName) continue;

    if (contract === "Family Tutoring") {
      contractToPSSMap[contract] = familyTutoringPSS;
    } else {
      contractToPSSMap[contract] = pssName;
    }
  }

  // Update the corresponding audit sheet with the counts
  for (let contract in contractToTabMap) {
    let auditSheetName = contractToTabMap[contract];
    const auditSheet = ss.getSheetByName(auditSheetName);

    if (!auditSheet) continue;

    const auditData = auditSheet.getDataRange().getValues();
    const auditHeaders = auditData[1];
    const tutorIdIndexInAudit = auditHeaders.indexOf('Tutor ID');
    const auditCountIndex = auditHeaders.indexOf('Total Session Audits');
    const auditCountByPssIndex = auditHeaders.indexOf('Session Audits by PSS');

    if (tutorIdIndexInAudit === -1 || auditCountIndex === -1 || auditCountByPssIndex === -1) {
      Logger.log(`Required columns are missing in the ${auditSheetName} tab.`);
      continue;
    }

    // Process each row in the audit sheet
    for (let i = 2; i < auditData.length; i++) {
      let tutorId = auditData[i][tutorIdIndexInAudit].toString().trim();
      let key = tutorId + "_" + contract;
      let count = auditCounts[key] || 0;
      let pssCount = 0;
      let pssName = contract === "Family Tutoring" ? familyTutoringPSS : contractToPSSMap[contract];

      if (pssCounts[contract] && pssCounts[contract][pssName] && pssCounts[contract][pssName][tutorId]) {
        pssCount = pssCounts[contract][pssName][tutorId];
      }

      // Log the match and count for debugging
      Logger.log(`Tutor ID: ${tutorId} - Contract: ${contract} - Count: ${count}`);
      Logger.log(`Tutor ID: ${tutorId} - PSS: ${pssName} - Count: ${pssCount}`);

      // Set the Session Audits column
      auditSheet.getRange(i + 1, auditCountIndex + 1).setValue(count);
      auditSheet.getRange(i + 1, auditCountByPssIndex + 1).setValue(`${pssCount} - ${pssName}`);
    }
  }
}

function defaultPriorityTutorList() {
  var sheetId = 'SPREADSHEET ID';
  var targetSpreadsheet = SpreadsheetApp.openById(sheetId);
  var resultSheet = targetSpreadsheet.getSheetByName('Audit_Results');
  var outputSheet = targetSpreadsheet.getSheetByName('DefaultPriorityTutors');
  var expectedHeaders = ['Tutor ID', 'Due Date', 'Notes'];
  var defaultPriorityTutors = new Map();

  if (resultSheet && outputSheet) {
    var existingData = resultSheet.getDataRange().getValues();

    existingData.slice(1).forEach(row => {
      var tutorId = row[0];
      var dueDate = new Date(row[2]);
      var priority = row[3];
      var notes = row[4];

      if (notes === 'Default priority: No valid QA or session audit data found.') {
        defaultPriorityTutors.set(tutorId, [tutorId, dueDate, notes]);
      }
    });

    // Convert Map to an array for writing to the sheet
    var dataToWrite = Array.from(defaultPriorityTutors.values());

    // Clear existing data (optional, if you want to overwrite)
    outputSheet.clearContents();

    // Write headers
    outputSheet.getRange(1, 1, 1, expectedHeaders.length).setValues([expectedHeaders]);

    // Write collected data if any
    if (dataToWrite.length > 0) {
      outputSheet.getRange(2, 1, dataToWrite.length, expectedHeaders.length).setValues(dataToWrite);
    }

    Logger.log("Data written to DefaultPriorityTutors: " + JSON.stringify(dataToWrite));
  } else {
    Logger.log("One or both sheets not found.");
  }
}