Jump to content

User:Invinciblejeremy08

From Wikipedia, the free encyclopedia
LILO

Code.gs

/**
 * @OnlyCurrentDoc
 */

// --- Configuration ---
var SHEET_NAME = "LILO"; // Target sheet tab name
var HEADERS = ["Date", "Logout", "Login", "Logout Reason", "Remarks", "Duration"]; // Header row
// Timezone for formatting dates/times in the sheet. Adjust if needed.
// Uses the sheet's setting if available, otherwise defaults to Eastern Time.
var TIMEZONE = SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone() || "America/New_York";
// --- End Configuration ---

// Function that runs when the web app URL is accessed
function doGet(e) {
  return HtmlService.createHtmlOutputFromFile('Index')
      .setTitle('LILO Logger')
      .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

// Function to get the dropdown options from your screenshot
function getDropdownOptions() {
  return [
    "Bio Break", "BREAK 1", "BREAK 2", "BREAK 3", "Coaching/RME listening",
    "CORP EVENT", "LUNCH", "Other Not Plotted (Specify in REMARKS)", "Other Plotted",
    "PGMT", "PREP", "SYSTEM/Tech-related incident",
    "SYSTEM/Tech-related incident (Not plotted)", "TEAM MEETING"
  ];
}

// Function to get the target sheet, creating it and the header if necessary
function getSheet() {
  const ss = SpreadsheetApp.getActiveSpreadsheet(); // Gets the sheet this script is bound to
  let sheet = ss.getSheetByName(SHEET_NAME);
  if (!sheet) {
    sheet = ss.insertSheet(SHEET_NAME);
    Logger.log("Created sheet: " + SHEET_NAME);
    sheet.appendRow(HEADERS);
    sheet.setFrozenRows(1);
    // Optional: Adjust column widths if needed
    // sheet.setColumnWidth(1, 100); // Example: Set Date column width
    Logger.log("Added header row to sheet: " + SHEET_NAME);
  } else if (sheet.getLastRow() === 0) {
    // If sheet exists but is empty, add header
    sheet.appendRow(HEADERS);
    sheet.setFrozenRows(1);
    Logger.log("Sheet existed but was empty. Added header row.");
  }
  // Optional: Add more robust check to ensure headers match if sheet already has data
  return sheet;
}

// Function to log data received from the HTML form
function logData(formData) {
  try {
    var sheet = getSheet(); // Get the target sheet in the active spreadsheet

    // Get data from the form object
    var dateStr = formData.logDate;
    var logoutTimeStr = formData.logoutTime;
    var loginTimeStr = formData.loginTime;
    var reason = formData.logoutReason;
    var remarks = formData.remarks;

    // --- Data Validation ---
    if (!logoutTimeStr || !loginTimeStr || !dateStr || !reason) {
       throw new Error("Date, Logout Time, Login Time, and Reason are required.");
    }

    // --- Duration Calculation ---
    var dateBase = new Date(dateStr);
    // Use UTC calculations for duration math to avoid DST/timezone shifts affecting the difference
    var logoutDateTimeUTC = new Date(Date.UTC(dateBase.getFullYear(), dateBase.getMonth(), dateBase.getDate(), parseInt(logoutTimeStr.split(':')[0]), parseInt(logoutTimeStr.split(':')[1])));
    var loginDateTimeUTC = new Date(Date.UTC(dateBase.getFullYear(), dateBase.getMonth(), dateBase.getDate(), parseInt(loginTimeStr.split(':')[0]), parseInt(loginTimeStr.split(':')[1])));

    // Handle crossing midnight
    if (loginDateTimeUTC.getTime() <= logoutDateTimeUTC.getTime()) {
      loginDateTimeUTC.setUTCDate(loginDateTimeUTC.getUTCDate() + 1); // Add 24 hours in UTC
    }

    var durationMillis = loginDateTimeUTC.getTime() - logoutDateTimeUTC.getTime();
    var totalMinutes = Math.round(durationMillis / (1000 * 60));
    var hours = Math.floor(totalMinutes / 60);
    var minutes = totalMinutes % 60;
    // Format duration as text HH:MM - Sheets will interpret this correctly as duration
    var durationFormatted = Utilities.formatString('%02d:%02d', hours, minutes);

    // --- Formatting for Sheet ---
    // Create Date objects based on local time strings for formatting display values
     var displayLogoutDateTime = new Date(dateStr + ' ' + logoutTimeStr);
     var displayLoginDateTime = new Date(dateStr + ' ' + loginTimeStr);
     // Adjust day for display if login time crossed midnight, relative to the logout date
     if (loginDateTimeUTC.getUTCDate() !== logoutDateTimeUTC.getUTCDate()) {
          displayLoginDateTime.setDate(displayLoginDateTime.getDate() + 1);
     }

    // Format using the determined TIMEZONE
    var formattedDate = Utilities.formatDate(new Date(dateStr), TIMEZONE, 'MM/dd/yyyy');
    var formattedLogoutTime = Utilities.formatDate(displayLogoutDateTime, TIMEZONE, 'hh:mm a');
    var formattedLoginTime = Utilities.formatDate(displayLoginDateTime, TIMEZONE, 'hh:mm a');

    // Append the data row
    sheet.appendRow([
      formattedDate,
      formattedLogoutTime,
      formattedLoginTime,
      reason,
      remarks || "", // Ensure remarks is not null/undefined
      durationFormatted // Append calculated duration string
    ]);

    return { status: "success", message: "Data logged successfully!" };

  } catch (e) {
    Logger.log("Error in logData: " + e.toString() + "\nStack: " + e.stack);
    return { status: "error", message: "Error: " + e.message };
  }
}

=====================================================
.html

=====================================================

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
      body { font-family: sans-serif; padding: 15px; max-width: 500px; margin: 10px auto;} /* Center content */
      h3 { text-align: center; color: #444; }
      label { display: block; margin-top: 12px; margin-bottom: 3px; font-weight: bold; color: #555;}
      input[type="date"], input[type="time"], select, textarea {
        width: 100%; /* Full width */
        padding: 10px;
        margin-top: 2px;
        border: 1px solid #ccc;
        border-radius: 4px;
        box-sizing: border-box; /* Include padding in width */
        font-size: 1rem;
      }
      textarea { resize: vertical; min-height: 80px; }
      button {
        background-color: #007BFF; /* Blue */
        color: white;
        padding: 12px 20px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        margin-top: 20px;
        width: 100%; /* Full width */
        font-size: 1.1rem;
      }
      button:hover { background-color: #0056b3; }
      #status { margin-top: 15px; font-weight: bold; text-align: center; padding: 10px; border-radius: 4px;}
      .success { color: #155724; background-color: #d4edda; border: 1px solid #c3e6cb;}
      .error { color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb;}
      /* Simple form layout */
       form div { margin-bottom: 10px; }
    </style>
  </head>
  <body>
    <h3>LILO Logger</h3>
    <form id="liloForm">
      <div>
        <label for="logDate">Date:</label>
        <input type="date" id="logDate" name="logDate" required>
      </div>

      <div>
        <label for="logoutTime">Logout Time:</label>
        <input type="time" id="logoutTime" name="logoutTime" required>
      </div>

      <div>
        <label for="loginTime">Login Time:</label>
        <input type="time" id="loginTime" name="loginTime" required>
      </div>

      <div>
        <label for="logoutReason">Logout Reason:</label>
        <select id="logoutReason" name="logoutReason" required>
          <option value="" disabled selected>Loading reasons...</option>
          </select>
      </div>

     <div>
        <label for="remarks">Remarks (if applicable):</label>
        <textarea id="remarks" name="remarks"></textarea>
     </div>

      <button type="submit" id="submitButton">Log Entry</button>
    </form>

    <div id="status"></div>

    <script>
      // Set default date to today
      document.getElementById('logDate').valueAsDate = new Date();

      // Function to populate the dropdown menu
      function populateDropdown(options) {
        var select = document.getElementById('logoutReason');
        select.innerHTML = '<option value="" disabled selected>-- Select Reason --</option>';
        options.forEach(function(option) {
          var opt = document.createElement('option');
          opt.value = option;
          opt.innerHTML = option;
          select.appendChild(opt);
        });
      }

      // Function to handle form submission
      function handleFormSubmit(event) {
        event.preventDefault();
        var statusDiv = document.getElementById('status');
        var submitButton = document.getElementById('submitButton');

        statusDiv.textContent = "Logging...";
        statusDiv.className = "";
        submitButton.disabled = true; // Disable button

        // Pass the form object directly to the server-side function
        google.script.run
          .withSuccessHandler(handleSubmitSuccess)
          .withFailureHandler(handleSubmitFailure)
          .logData(this);
      }

      // Function on successful submission
      function handleSubmitSuccess(response) {
         var statusDiv = document.getElementById('status');
         var submitButton = document.getElementById('submitButton');
         submitButton.disabled = false; // Re-enable button

         if (response && response.status === "success") {
           statusDiv.textContent = response.message || 'Log successful!';
           statusDiv.className = "success";
           document.getElementById('liloForm').reset(); // Clear form
           document.getElementById('logDate').valueAsDate = new Date(); // Reset date
           // Clear status message after 5 seconds
           setTimeout(function(){ statusDiv.textContent = ''; statusDiv.className = ''; }, 5000);
         } else {
           // Handle cases where server function returns success but indicates an error logically
           handleSubmitFailure(response || { message: "Unknown success response format."});
         }
      }

      // Function on failed submission
      function handleSubmitFailure(error) {
        var statusDiv = document.getElementById('status');
        var submitButton = document.getElementById('submitButton');
        submitButton.disabled = false; // Re-enable button

        statusDiv.textContent = 'Error: ' + (error && error.message ? error.message : 'Script execution failed.');
        statusDiv.className = "error";
        console.error("Form Submission Error: ", error);
      }

      // Add form submission listener
      document.getElementById('liloForm').addEventListener('submit', handleFormSubmit);

      // Fetch dropdown options when the page loads
      google.script.run.withSuccessHandler(populateDropdown).getDropdownOptions();

    </script>
  </body>
</html>





=====================================================
Call Details Capturing 


Code.gs


/**
 * @OnlyCurrentDoc
 */

const SHEET_NAME = "Call Logs";
const HEADER_ROW = ["Date", "Account ID", "Contact/Sage ID", "Call Details", "Timestamp"];

function doGet(e) {
  return HtmlService.createHtmlOutputFromFile('Index')
      .setTitle('Call Log & Search')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.DEFAULT);
}

function getSheet() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName(SHEET_NAME);
  if (!sheet) {
    sheet = ss.insertSheet(SHEET_NAME);
    if (sheet.getLastRow() === 0) {
       sheet.appendRow(HEADER_ROW);
       sheet.setFrozenRows(1);
       sheet.setColumnWidths(1, HEADER_ROW.length, 150);
       sheet.setColumnWidth(4, 300);
    }
  }
  if (sheet.getLastRow() === 0 || sheet.getRange(1, 1, 1, HEADER_ROW.length).getValues()[0].join("") === "") {
     sheet.getRange(1, 1, 1, HEADER_ROW.length).setValues([HEADER_ROW]);
     if(sheet.getFrozenRows() < 1) sheet.setFrozenRows(1);
  }
  return sheet;
}

function submitData(formData) {
  try {
    if (!formData.logDate || !formData.accountId || !formData.callDetails) {
       return { status: "error", message: "Date, Account ID, and Call Details are required." };
    }
    if (!/^\d+$/.test(formData.accountId)) {
      return { status: "error", message: "Account ID must contain only numbers." };
    }

    const sheet = getSheet();
    const timestamp = new Date();
    // Store date as YYYY-MM-DD string
    const logDateString = formData.logDate;

    sheet.appendRow([
      logDateString, // Store as String
      formData.accountId.trim(),
      formData.contactSageId.trim(),
      formData.callDetails.trim(),
      timestamp // Timestamp remains a Date object
    ]);

    const successReturn = { status: "success", message: "Data submitted successfully!" };
    Logger.log("Returning from submitData: " + JSON.stringify(successReturn));
    return successReturn;

  } catch (error) {
    Logger.log("Error caught in submitData: " + error + "\nStack:\n" + error.stack);
    const errorReturn = { status: "error", message: "An error occurred: " + error.message };
    Logger.log("Returning from submitData (catch block): " + JSON.stringify(errorReturn));
    return errorReturn;
  }
}


// --- UPDATED searchData (Handles Date object or String from Sheet) ---
function searchData(searchCriteria) {
  let returnValue;
  let headers = HEADER_ROW;
  let sheet;

  try {
    sheet = getSheet();
    try {
       headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    } catch(e) {
       Logger.log("Warning: Could not get headers from sheet, using default. Error: " + e);
       headers = [...HEADER_ROW];
    }

    const data = sheet.getLastRow() > 1 ? sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn()).getValues() : [];

    if (data.length === 0) {
        returnValue = { status: "success", message: "No data found in the sheet to search.", results: [headers] };
        Logger.log("Returning from searchData (no data): " + JSON.stringify(returnValue));
        return returnValue;
    }

    // Get search date string directly from input (YYYY-MM-DD)
    const searchDateStr = searchCriteria.searchDate ? String(searchCriteria.searchDate).trim() : "";
    const searchAccountIdStr = searchCriteria.searchAccountId ? String(searchCriteria.searchAccountId).trim() : "";
    const searchContactSageIdStr = searchCriteria.searchContactSageId ? String(searchCriteria.searchContactSageId).trim().toLowerCase() : "";
    const searchKeyword = searchCriteria.searchKeyword ? String(searchCriteria.searchKeyword).trim() : "";

    if (!searchDateStr && !searchAccountIdStr && !searchContactSageIdStr && !searchKeyword) {
        returnValue = { status: "error", message: "Please enter at least one search criterion.", results: [headers] };
        Logger.log("Returning from searchData (no criteria): " + JSON.stringify(returnValue));
        return returnValue;
    }

    const keywordRegex = searchKeyword ? new RegExp("\\b" + escapeRegExp(searchKeyword) + "\\b", "i") : null;

    // --- Filter Data (Handle Date Object OR String from Sheet) ---
    const results = data.filter(row => {
      let match = true;
      if (row.length < headers.length) {
          Logger.log("Warning: Skipping filter for row with fewer columns than headers: " + JSON.stringify(row));
          return false;
      }

      // --- NEW: Handle Date comparison robustly ---
      let rowDateFormattedStr = ""; // YYYY-MM-DD format from sheet data
      const sheetCellValue = row[0]; // Value from Date column

      if (sheetCellValue instanceof Date) {
           // If it's a Date object, format it to YYYY-MM-DD
           try {
               const year = sheetCellValue.getFullYear();
               const month = String(sheetCellValue.getMonth() + 1).padStart(2, '0'); // +1 because months are 0-indexed
               const day = String(sheetCellValue.getDate()).padStart(2, '0');
               rowDateFormattedStr = `${year}-${month}-${day}`;
           } catch(e) {
               Logger.log("Error formatting date object from sheet: " + e + " | Value: " + sheetCellValue);
               rowDateFormattedStr = String(sheetCellValue); // Fallback to basic string conversion
           }
      } else {
          // Otherwise, assume it's already a string (or number) and use it directly
          rowDateFormattedStr = String(sheetCellValue || "").trim();
      }
      // --- END NEW Date handling ---

      const rowAccountId = String(row[1] || "").trim();
      const rowContactSageId = String(row[2] || "").trim().toLowerCase();
      const rowCallDetails = String(row[3] || "").trim();

      // Compare the consistently formatted sheet date string with the search date string
      if (searchDateStr && rowDateFormattedStr !== searchDateStr) match = false;

      // Rest of the conditions
      if (match && searchAccountIdStr && rowAccountId !== searchAccountIdStr) match = false;
      if (match && searchContactSageIdStr && !rowContactSageId.includes(searchContactSageIdStr)) match = false;
      if (match && keywordRegex && !keywordRegex.test(rowCallDetails)) match = false;

      return match;
    });

    // --- Pre-process results for return (Convert only Timestamp to String) ---
    const processedResults = [headers];
    const timestampIndex = headers.indexOf("Timestamp");

    results.forEach(row => {
        if (row.length < headers.length) {
             Logger.log("Warning: Skipping processing for row with fewer columns than headers: " + JSON.stringify(row));
             return;
        }
        const newRow = [...row];

        // Date column (row[0]) should now be handled correctly as string by the filtering logic,
        // but ensure it's a string before sending back just in case.
        newRow[0] = String(newRow[0] || "");

        if (timestampIndex !== -1 && newRow[timestampIndex] instanceof Date) {
             try { newRow[timestampIndex] = newRow[timestampIndex].toLocaleString(); }
             catch (e) {
                  Logger.log("Warning: Could not format timestamp in row: " + JSON.stringify(row) + " Error: " + e);
                  newRow[timestampIndex] = String(newRow[timestampIndex]);
             }
         } else if (timestampIndex !== -1) {
              newRow[timestampIndex] = String(newRow[timestampIndex] || "");
         }

        for(let i=0; i < newRow.length; i++){
            if (i === 0 || i === timestampIndex) continue; // Already handled/ensured string
            if (typeof newRow[i] === 'object' && newRow[i] !== null) {
                 Logger.log(`Warning: Converting complex object/Date in cell [${processedResults.length}, ${i}] to string.`);
                 try { newRow[i] = newRow[i].toLocaleString ? newRow[i].toLocaleString() : JSON.stringify(newRow[i]); }
                 catch (e) { newRow[i] = JSON.stringify(newRow[i]); }
            } else if (newRow[i] === null || typeof newRow[i] === 'undefined') { newRow[i] = ""; }
            else { newRow[i] = String(newRow[i]); }
        }
        processedResults.push(newRow);
    });

    returnValue = { status: "success", message: `Found ${results.length} matching records.`, results: processedResults };
    // Updated log message slightly for clarity
    Logger.log("Returning from searchData (success/filtered/processed/date-handled): " + JSON.stringify(returnValue));
    return returnValue;

  } catch (error) {
    Logger.log("Error caught in searchData: " + error + "\nStack:\n" + error.stack);
    let headersOnError = [...HEADER_ROW];
    try {
        if (sheet) { headersOnError = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]; }
    } catch(e) { Logger.log("Could not retrieve headers within catch block."); }
    returnValue = { status: "error", message: "Search failed: " + error.message, results: [headersOnError] };
    Logger.log("Returning from searchData (catch block): " + JSON.stringify(returnValue));
    return returnValue;
  }
}


function escapeRegExp(string) {
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}


==========================================================
.html

==========================================================

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      body { font-family: sans-serif; margin: 20px; background-color: #f8f9fa; }
      h1, h2 { color: #444; border-bottom: 1px solid #ccc; padding-bottom: 5px;}
      label { display: block; margin-top: 10px; font-weight: bold; color: #555;}
      input[type="text"], input[type="number"], input[type="date"], textarea {
        width: 95%; /* Responsive width */
        max-width: 400px; /* Max width */
        padding: 8px;
        margin-top: 5px;
        border: 1px solid #ccc;
        border-radius: 4px;
        box-sizing: border-box; /* Include padding in width */
      }
      textarea { height: 80px; resize: vertical; }
      button {
        padding: 10px 15px;
        margin-top: 15px;
        background-color: #4CAF50; /* Green */
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 1em;
        transition: background-color 0.2s;
      }
      button:hover { background-color: #45a049; }
      button:disabled { background-color: #cccccc; cursor: not-allowed; }
      #statusMessage, #searchStatus { margin-top: 15px; padding: 10px; border-radius: 4px; min-height: 1.2em; /* Prevent layout shift */ }
      .success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
      .error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
      #searchResults { margin-top: 20px; }
      table { border-collapse: collapse; width: 100%; margin-top: 10px; }
      th, td { border: 1px solid #ddd; padding: 8px; text-align: left; word-wrap: break-word; /* Wrap long text */}
      th { background-color: #e9ecef; color: #333; }
      tr:nth-child(even) { background-color: #f2f2f2; }
      .form-section {
         background-color: #ffffff;
         padding: 20px;
         border-radius: 8px;
         box-shadow: 0 2px 4px rgba(0,0,0,0.1);
         margin-bottom: 30px;
      }
      .button-group button { margin-right: 10px; }
    </style>
  </head>
  <body>
    <h1>Call Log Entry</h1>

    <div class="form-section">
      <form id="callLogForm">
        <label for="logDate">Date:</label>
        <input type="date" id="logDate" name="logDate" required>
        <label for="accountId">Account ID (Numbers only):</label>
        <input type="text" id="accountId" name="accountId" pattern="\d+" title="Please enter numbers only." required>

        <label for="contactSageId">Contact/Sage ID (Alphanumeric):</label>
        <input type="text" id="contactSageId" name="contactSageId">

        <label for="callDetails">Call Details:</label>
        <textarea id="callDetails" name="callDetails" required></textarea>

        <div class="button-group">
           <button type="button" id="submitButton" onclick="submitForm()">Submit Call Log</button>
        </div>
         <div id="statusMessage"></div>
      </form>
    </div>

    <h2>Search Call Logs</h2>

    <div class="form-section">
      <form id="searchForm">
         <label for="searchDate">Date:</label>
         <input type="date" id="searchDate" name="searchDate">

         <label for="searchAccountId">Account ID:</label>
         <input type="text" id="searchAccountId" name="searchAccountId">

         <label for="searchContactSageId">Contact/Sage ID:</label>
         <input type="text" id="searchContactSageId" name="searchContactSageId">

         <label for="searchKeyword">Call Details Keyword (Whole Word):</label>
         <input type="text" id="searchKeyword" name="searchKeyword">

         <div class="button-group">
            <button type="button" id="searchButton" onclick="searchLogs()">Search</button>
            <button type="button" onclick="clearSearch()" style="background-color: #6c757d;">Clear</button>
         </div>
          <div id="searchStatus"></div>
      </form>

      <div id="searchResults">
        </div>
    </div>


    <script>
      // Set default date to today on load (Keep this functionality)
      document.addEventListener('DOMContentLoaded', (event) => {
        setToday();
      });

      // Keep setToday function to initialize date field, even without the button
      function setToday() {
        const today = new Date();
        const year = today.getFullYear();
        const month = String(today.getMonth() + 1).padStart(2, '0');
        const day = String(today.getDate()).padStart(2, '0');
        document.getElementById('logDate').value = `${year}-${month}-${day}`;
      }

      function submitForm() {
        const form = document.getElementById('callLogForm');
        const submitButton = document.getElementById('submitButton');
        const statusDiv = document.getElementById('statusMessage');
        const accountIdInput = document.getElementById('accountId');

        if (!/^\d+$/.test(accountIdInput.value)) {
           setStatus(statusDiv, 'error', 'Account ID must contain only numbers.');
           accountIdInput.focus(); return;
        }
        if (!form.checkValidity()) {
           setStatus(statusDiv, 'error', 'Please fill out all required fields correctly.');
           form.reportValidity(); return;
        }

        const formData = {
          logDate: document.getElementById('logDate').value,
          accountId: document.getElementById('accountId').value,
          contactSageId: document.getElementById('contactSageId').value,
          callDetails: document.getElementById('callDetails').value
        };

        submitButton.disabled = true; submitButton.textContent = 'Submitting...';
        setStatus(statusDiv, '', '');
        google.script.run.withSuccessHandler(onSubmitSuccess).withFailureHandler(onFailure).submitData(formData);
      }

      function onSubmitSuccess(response) {
        const submitButton = document.getElementById('submitButton');
        const statusDiv = document.getElementById('statusMessage');
        if (!response) {
             console.error("onSubmitSuccess received null/undefined response");
             setStatus(statusDiv, 'error', 'Invalid response received after submit.');
        } else {
            setStatus(statusDiv, response.status, response.message);
            if (response.status === 'success') {
              document.getElementById('callLogForm').reset(); setToday(); // Reset date to today after submit
            }
        }
        submitButton.disabled = false; submitButton.textContent = 'Submit Call Log';
      }

      function searchLogs() {
          const searchButton = document.getElementById('searchButton');
          const statusDiv = document.getElementById('searchStatus');
          const resultsDiv = document.getElementById('searchResults');
          const searchCriteria = {
              searchDate: document.getElementById('searchDate').value,
              searchAccountId: document.getElementById('searchAccountId').value,
              searchContactSageId: document.getElementById('searchContactSageId').value,
              searchKeyword: document.getElementById('searchKeyword').value
          };
          if (!searchCriteria.searchDate && !searchCriteria.searchAccountId && !searchCriteria.searchContactSageId && !searchCriteria.searchKeyword) {
              setStatus(statusDiv, 'error', 'Please enter at least one search criterion.');
              resultsDiv.innerHTML = ''; return;
          }
          searchButton.disabled = true; searchButton.textContent = 'Searching...';
          setStatus(statusDiv, '', ''); resultsDiv.innerHTML = '';
          google.script.run.withSuccessHandler(onSearchSuccess).withFailureHandler(onFailure).searchData(searchCriteria);
      }

       function onSearchSuccess(response) {
           console.log("Raw response received in onSearchSuccess:", response);
           const searchButton = document.getElementById('searchButton');
           const statusDiv = document.getElementById('searchStatus');
           const resultsDiv = document.getElementById('searchResults');

           if (!response) {
               console.error("onSearchSuccess received a null or undefined response.");
               setStatus(statusDiv, 'error', 'Received an invalid response from the server. Please check Apps Script logs.');
               resultsDiv.innerHTML = ''; searchButton.disabled = false; searchButton.textContent = 'Search'; return;
           }

           setStatus(statusDiv, response.status, response.message);
           if (response.status === 'success' && response.results && response.results.length > 1) {
               displayResults(response.results);
           } else {
               if (response.status === 'success' && response.message){
                    resultsDiv.innerHTML = `<p>${response.message}</p>`;
               } else if (response.status !== 'success' && response.message){
                    resultsDiv.innerHTML = `<p>Error: ${response.message}</p>`;
               }
               else { resultsDiv.innerHTML = '<p>No matching records found.</p>'; }
           }
           searchButton.disabled = false; searchButton.textContent = 'Search';
       }

       function displayResults(data) {
           const resultsDiv = document.getElementById('searchResults');
           resultsDiv.innerHTML = '';
           if (!data || data.length <= 1) {
               resultsDiv.innerHTML = '<p>No matching records found.</p>'; return;
           }

           const table = document.createElement('table');
           const thead = table.createTHead();
           const tbody = table.createTBody();
           const headerRow = thead.insertRow();
           data[0].forEach(headerText => {
               const th = document.createElement('th');
               th.textContent = headerText; headerRow.appendChild(th);
           });

           for (let i = 1; i < data.length; i++) {
               const dataRow = tbody.insertRow();
               data[i].forEach((cellData, index) => {
                   const cell = dataRow.insertCell();
                   cell.textContent = cellData !== null && cellData !== undefined ? cellData : "";
               });
           }
           resultsDiv.appendChild(table);
       }

      // clearSearch function remains the same - clears form, results, status
      function clearSearch() {
          document.getElementById('searchForm').reset();
          document.getElementById('searchResults').innerHTML = '';
          document.getElementById('searchStatus').innerHTML = '';
          document.getElementById('searchStatus').className = '';
      }

      function onFailure(error) {
        console.error('Client-side script execution failed: ', error);
        const statusDiv = document.getElementById('statusMessage');
        const searchStatusDiv = document.getElementById('searchStatus');
        const msg = 'Client-side error occurred: ' + (error.message || error) + ' (Check browser console F12 for details)';
         if (document.getElementById('searchButton').disabled) { setStatus(searchStatusDiv, 'error', msg); }
         else if (document.getElementById('submitButton').disabled) { setStatus(statusDiv, 'error', msg); }
         else { setStatus(searchStatusDiv, 'error', msg); }
        document.getElementById('submitButton').disabled = false; document.getElementById('submitButton').textContent = 'Submit Call Log';
        document.getElementById('searchButton').disabled = false; document.getElementById('searchButton').textContent = 'Search';
      }

      function setStatus(element, type, message) {
          element.className = '';
          element.textContent = message || '';
          if (type === 'success') element.classList.add('success');
          else if (type === 'error') element.classList.add('error');
      }
    </script>
  </body>
</html>


==========================================================


NPS Goal Calculator


Code.gs

====================================================

/**
 * Serves the HTML for the web app.
 * This function runs when the web app URL is accessed.
 *
 * @param {Object} e The event parameter for a web app doGet request.
 * @return {HtmlOutput} The HTML output for the web application.
 */
function doGet(e) {
  // Log access for debugging (optional)
  console.log("Web app accessed. Parameters: " + JSON.stringify(e.parameter));
  // Serve the main UI file
  return HtmlService.createHtmlOutputFromFile('CalculatorUI')
      .setTitle('NPS Goal Calculator Web App') // Sets browser tab title
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.DEFAULT); // Standard security setting
}

/**
 * Calculates the Net Promoter Score.
 * (Identical calculation function)
 *
 * @param {number} promoters The number of promoters.
 * @param {number} detractors The number of detractors.
 * @param {number} passives The number of passives (neutrals).
 * @return {number|string} The calculated NPS or an error message.
 */
function calculateCurrentNPS(promoters, detractors, passives) {
  promoters = Number(promoters) || 0;
  detractors = Number(detractors) || 0;
  passives = Number(passives) || 0;
  const total = promoters + detractors + passives;
  if (total === 0) {
    return 'N/A (No responses)';
  }
  const nps = ((promoters - detractors) / total) * 100;
  console.log(`Calculated NPS: ${nps.toFixed(2)} for P:${promoters}, D:${detractors}, N:${passives}`); // Log calculation
  return nps.toFixed(2);
}

/**
 * Calculates the additional promoters needed to reach a target NPS.
 * (Identical calculation function)
 *
 * @param {number} currentPromoters Current number of promoters.
 * @param {number} currentDetractors Current number of detractors.
 * @param {number} currentPassives Current number of passives.
 * @param {number} targetNPS The desired NPS goal.
 * @return {number|string} The number of additional promoters needed or a message.
 */
function calculateNeededPromoters(currentPromoters, currentDetractors, currentPassives, targetNPS) {
  currentPromoters = Number(currentPromoters) || 0;
  currentDetractors = Number(currentDetractors) || 0;
  currentPassives = Number(currentPassives) || 0;
  targetNPS = Number(targetNPS);

  console.log(`Calculating needed for P:${currentPromoters}, D:${currentDetractors}, N:${currentPassives}, Goal:${targetNPS}`); // Log input

  if (isNaN(targetNPS) || targetNPS < -100 || targetNPS > 100) {
      console.error("Invalid Target NPS:", targetNPS);
      return "Invalid Target NPS (must be -100 to 100)";
  }

  const currentNPSResult = calculateCurrentNPS(currentPromoters, currentDetractors, currentPassives);

  if (currentNPSResult === 'N/A (No responses)') {
      if (targetNPS > 0) return 1;
      if (targetNPS <= 0) return 0;
      return 0;
  }

  const currentNPSValue = parseFloat(currentNPSResult);
  if (!isNaN(currentNPSValue) && currentNPSValue >= targetNPS) {
    console.log("Goal already met.");
    return 0;
  }

  if (targetNPS === 100) {
      const result = (currentDetractors > 0) ? "Impossible (Goal 100 requires 0 detractors)" : 0;
      console.log("Goal is 100. Result:", result);
      return result;
  }

  const currentTotal = currentPromoters + currentDetractors + currentPassives;
  const numerator = (targetNPS * currentTotal) + (100 * currentDetractors) - (100 * currentPromoters);
  const denominator = 100 - targetNPS;

  if (denominator <= 0) {
       console.error("Denominator error. Target NPS:", targetNPS);
       return "Calculation Error (Target NPS must be < 100)";
  }

  const neededRaw = numerator / denominator;
  const needed = Math.max(0, Math.ceil(neededRaw));
  console.log(`Calculation: Numerator=${numerator}, Denominator=${denominator}, NeededRaw=${neededRaw}, Needed=${needed}`); // Log calculation details
  return needed;
}

// IMPORTANT: Remove any onOpen() function if it exists, as it's not needed
// for a standalone web app and only applies when the spreadsheet is opened.

=============================================
CalculatorUI.html

==============================================

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.1/mini-default.min.css">
    <style>
      /* Center content and add padding */
      body {
        padding: 20px;
        max-width: 500px; /* Adjusted max-width slightly */
        margin: 20px auto;
        font-family: sans-serif; /* Use a common sans-serif font */
      }
      fieldset {
        margin-bottom: 20px; /* Add more space between fieldsets */
        border: 1px solid #ccc; /* Add subtle border */
        padding: 15px; /* Add padding inside fieldset */
      }
      legend {
        font-size: 1.1em;
        font-weight: bold;
        margin-bottom: 15px; /* Space below legend */
        padding: 0 5px; /* Add slight horizontal padding */
      }
      /* Style for each row containing a label and input */
      .input-row {
        margin-bottom: 15px; /* Space below each label/input pair */
      }
      /* Ensure labels take full width and have space below */
      label {
        display: block; /* Force label onto its own line */
        margin-bottom: 5px; /* Space between label and input */
        font-weight: bold;
      }
      /* Make input fields take full width */
      input[type="number"] {
          width: 100%;
          padding: 8px;
          border: 1px solid #ccc;
          border-radius: 4px;
          box-sizing: border-box; /* Include padding/border in width */
      }
      /* Results area styling */
      #results {
        margin-top: 25px;
        padding: 15px;
        border: 1px solid #e0e0e0;
        border-radius: 4px;
        background-color: #f9f9f9;
      }
       #results h3 {
         margin-top: 0; /* Remove default top margin for heading inside results */
         margin-bottom: 15px;
         border-bottom: 1px solid #eee; /* Add a separator line */
         padding-bottom: 10px;
       }
      .result-value { font-weight: bold; color: #0288D1; font-size: 1.1em; }
      .error { color: #D32F2F; font-weight: bold; margin-top: 10px; }
      /* Loader animation */
      .loader { display: none; border: 4px solid #f3f3f3; border-top: 4px solid #0288D1; border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; vertical-align: middle; }
      @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
      button .loader { margin-left: 8px;}
      button:disabled { cursor: not-allowed; opacity: 0.7; }
      /* Primary button style from mini.css (optional, but nice) */
      button.primary {
        background-color: #0288D1; /* Match result color */
        color: white;
      }
      button.primary:hover {
        background-color: #0277BD;
      }
    </style>
  </head>
  <body>
    <h1>NPS Goal Calculator</h1>
    <p>Calculate current NPS and find out how many more promoters you need to reach a target score.</p>

    <form id="npsForm" onsubmit="event.preventDefault(); calculate();">

      <fieldset>
        <legend>Enter Survey Results</legend>
        <div class="input-row">
          <label for="promoters">Promoters:</label>
          <input type="number" id="promoters" min="0" value="1" required>
        </div>

        <div class="input-row">
          <label for="detractors">Detractors:</label>
          <input type="number" id="detractors" min="0" value="5" required>
        </div>

        <div class="input-row">
          <label for="passives">Passives (Neutrals):</label>
          <input type="number" id="passives" min="0" value="1" required>
        </div>
      </fieldset>

      <fieldset>
         <legend>Set Goal</legend>
         <div class="input-row">
            <label for="goal">NPS Goal (-100 to 100):</label>
            <input type="number" id="goal" min="-100" max="100" value="50" required>
         </div>
       </fieldset>

      <button type="submit" id="calcButton" class="primary">
          Calculate
          <span id="loader" class="loader"></span>
       </button>
    </form>

    <div id="results">
      <h3>Results:</h3>
      <p>Current NPS: <span id="currentNPS" class="result-value">N/A</span></p>
      <p>Additional Promoters Needed for Goal <span id="goalDisplay" class="result-value">50</span>: <span id="neededPromoters" class="result-value">N/A</span></p>
      <p id="errorMessage" class="error"></p>
    </div>

    <script>
      // --- JavaScript logic remains the same ---
      // (Make sure the JS functions from the previous answer are here)
      const form = document.getElementById('npsForm');
      const calcButton = document.getElementById('calcButton');
      const loader = document.getElementById('loader');
      const currentNpsEl = document.getElementById('currentNPS');
      const neededPromotersEl = document.getElementById('neededPromoters');
      const errorMessageEl = document.getElementById('errorMessage');
      const goalDisplayEl = document.getElementById('goalDisplay');
      const goalInput = document.getElementById('goal');

       function showLoader(show) {
        loader.style.display = show ? 'inline-block' : 'none';
        calcButton.disabled = show;
      }

      function clearResults() {
          currentNpsEl.textContent = 'N/A';
          neededPromotersEl.textContent = 'N/A';
          errorMessageEl.textContent = '';
          goalDisplayEl.textContent = goalInput.value || 'N/A'; // Update from input field
      }

       function calculate() {
        if (!form.checkValidity()) {
          form.reportValidity();
          return;
        }
        clearResults();
        showLoader(true);
        const promoters = document.getElementById('promoters').value;
        const detractors = document.getElementById('detractors').value;
        const passives = document.getElementById('passives').value;
        const goal = goalInput.value;
        goalDisplayEl.textContent = goal || 'N/A';

        google.script.run
          .withSuccessHandler(function(npsResult) {
            currentNpsEl.textContent = npsResult;
            if (typeof npsResult === 'string' && (npsResult.startsWith('Error') || npsResult.startsWith('Invalid'))) {
                 showError({ message: npsResult });
                 showLoader(false);
                 return;
            }
            google.script.run
              .withSuccessHandler(function(neededResult) {
                 neededPromotersEl.textContent = neededResult;
                 showLoader(false);
              })
              .withFailureHandler(showError)
              .calculateNeededPromoters(promoters, detractors, passives, goal);
          })
          .withFailureHandler(showError)
          .calculateCurrentNPS(promoters, detractors, passives);
      }

      function showError(error) {
        const message = (error && error.message) ? error.message : String(error);
        errorMessageEl.textContent = "Error: " + message;
        console.error("Server Error:", error);
        showLoader(false);
      }

      goalInput.addEventListener('input', () => {
          goalDisplayEl.textContent = goalInput.value || 'N/A';
      });
    </script>
  </body>
</html>


=====================================================

Task Tool/Others 

Code.gs

// Global constant for the Sheet Name
const SHEET_NAME = "Links/Details"; // Ensure this matches your sheet name exactly
const TASK_COLUMN = 1; // Column A (Task Name)
const LINK_COLUMN = 2; // Column B (Link)
const DETAILS_COLUMN = 3; // Column C (Detail)
const DETAIL2_COLUMN = 4; // Column D (Detail 2)
const DETAIL3_COLUMN = 5; // Column E (Detail 3)

/**
 * Serves the HTML page for the web app.
 * This function runs when someone visits the web app URL.
 * @param {object} e The event parameter (not used here but required by doGet).
 * @return {HtmlOutput} The HTML page to be served.
 */
function doGet(e) {
  return HtmlService.createHtmlOutputFromFile('Index') // Serves the Index.html file
      .setTitle('Task Lookup Tool'); // Sets the browser tab title
}

/**
 * Gets the list of tasks from the spreadsheet's task column.
 * This function is called by the frontend JavaScript.
 * @return {string[]} An array of task names (strings). Returns empty array on error.
 */
function getTasks() {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName(SHEET_NAME);
    if (!sheet) {
      // Log error if sheet is not found and throw an error
      Logger.log(`Error: Sheet "${SHEET_NAME}" not found.`);
      throw new Error(`Sheet "${SHEET_NAME}" not found.`);
    }
    // Get data starting from row 2 (assuming row 1 is header) down to the last row with content
    // Fetches only the first column (Task names)
    const range = sheet.getRange(2, TASK_COLUMN, sheet.getLastRow() - 1, 1);
    const values = range.getValues(); // Gets values as a 2D array [[task1], [task2], ...]

    // Flatten the 2D array into a 1D array and filter out any empty rows/cells
    const tasks = values.map(row => row[0]).filter(task => task && task.toString().trim() !== "");

    Logger.log(`Tasks fetched successfully: ${tasks.length} tasks found.`);
    return tasks;
  } catch (error) {
    // Log any errors encountered during the process
    Logger.log(`Error in getTasks: ${error.message} \nStack: ${error.stack}`);
    return []; // Return empty array to the frontend on error
  }
}

/**
 * Gets the link and all details (Detail, Detail 2, Detail 3) for a specific task.
 * This function is called by the frontend JavaScript when a task is selected.
 * @param {string} selectedTask The name of the task selected in the dropdown.
 * @return {object|null} An object containing {link: '...', details: '...', details2: '...', details3: '...'}
 * or null if task not found or an error occurs.
 */
function getTaskDetails(selectedTask) {
  // Return null immediately if no task name is provided
  if (!selectedTask) {
    Logger.log('getTaskDetails called with no selectedTask.');
    return null;
  }
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName(SHEET_NAME);
     if (!sheet) {
      // Log error if sheet is not found and throw an error
      Logger.log(`Error: Sheet "${SHEET_NAME}" not found.`);
      throw new Error(`Sheet "${SHEET_NAME}" not found.`);
    }
    // Get all data from the sheet to search through
    const data = sheet.getDataRange().getValues(); // [[header1, header2,...], [row1col1, row1col2,...], ...]

    // Find the row matching the selected task (case-insensitive search)
    // Start loop from i = 1 to skip header row (index 0)
    for (let i = 1; i < data.length; i++) {
      const taskInSheet = data[i][TASK_COLUMN - 1]; // Get task name from current row (adjusting for 0-based index)

      // Check if taskInSheet exists and matches selectedTask (case-insensitive)
      if (taskInSheet && taskInSheet.toString().trim().toLowerCase() === selectedTask.trim().toLowerCase()) {
        // Task found, extract link and details, providing empty string as default if cell is empty
        const link = data[i][LINK_COLUMN - 1] || '';
        const details = data[i][DETAILS_COLUMN - 1] || '';
        const details2 = data[i][DETAIL2_COLUMN - 1] || '';
        const details3 = data[i][DETAIL3_COLUMN - 1] || '';

        Logger.log(`Details found for "${selectedTask}"`);
        // Return the found data as an object
        return {
          link: link,
          details: details,
          details2: details2,
          details3: details3
        };
      }
    }
    // If loop completes without finding the task
    Logger.log(`No details found for task: "${selectedTask}"`);
    return null; // Task not found
  } catch (error) {
    // Log any errors encountered during the process
    Logger.log(`Error in getTaskDetails for task "${selectedTask}": ${error.message} \nStack: ${error.stack}`);
    return null; // Return null to the frontend on error
  }
}



====================================================

Index.html

====================================================

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
  <style>
    /* --- Base Styles --- */
    body {
      font-family: 'Roboto', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background-color: #f8f9fa; /* Light grey background */
      color: #333;
      margin: 0;
      padding: 0;
      font-size: 15px;
      line-height: 1.6;
    }

    /* --- Container --- */
    #app-container {
      max-width: 700px;
      margin: 20px auto;
      padding: 25px 30px;
      background-color: #ffffff;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    }

    /* --- Header --- */
    h1 {
      color: #0d6efd; /* Bootstrap primary blue */
      font-size: 1.8em;
      margin-top: 0; /* Remove default margin */
      margin-bottom: 25px;
      text-align: center;
      font-weight: 500;
    }

    /* --- Form Elements --- */
    label {
      display: block;
      margin-bottom: 8px;
      font-weight: 500;
      color: #555;
    }

    select#taskDropdown {
      width: 100%;
      padding: 10px 12px;
      margin-bottom: 25px;
      border: 1px solid #ced4da;
      border-radius: 5px;
      background-color: #fff;
      font-size: 1em;
      box-sizing: border-box;
    }

    select#taskDropdown:focus {
      border-color: #86b7fe;
      outline: 0;
      box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
    }

    /* --- Content Sections --- */
    .content-section {
      margin-top: 20px;
      padding: 15px;
      border: 1px solid #e9ecef;
      border-radius: 6px;
      background-color: #fdfdff;
    }

    .details-title {
      font-weight: 500;
      margin-top: 0;
      margin-bottom: 8px;
      display: block;
      color: #0d6efd;
      font-size: 1.1em;
    }

    #linkArea span#linkDisplay {
       display: block;
       word-wrap: break-word;
       margin-bottom: 10px;
       color: #495057;
    }

    /* Style for the content within detail sections */
     .content-section div {
       font-size: 1em; /* Ensure consistent font size */
       color: #343a40;
    }


    /* --- Button --- */
    #linkButton {
        display: inline-block;
        margin-top: 0;
        padding: 8px 18px;
        background-color: #198754; /* Bootstrap success green */
        color: white;
        border: none;
        border-radius: 5px;
        cursor: pointer;
        text-decoration: none;
        font-size: 0.95em;
        font-weight: 500;
        transition: background-color 0.2s ease;
     }

    #linkButton:hover {
      background-color: #157347; /* Darker green on hover */
    }

    /* --- Messages --- */
    #loadingMessage, #errorMessage {
      margin-top: 15px;
      padding: 10px 15px;
      border-radius: 5px;
      text-align: center;
      font-size: 0.95em;
    }
    #loadingMessage { color: #555; background-color: #eee; }
    #errorMessage { color: #842029; background-color: #f8d7da; border: 1px solid #f5c2c7;}

    /* --- Utility --- */
    .hidden {
      display: none;
    }

  </style>
</head>
<body>
  <div id="app-container">
    <h1>Task Lookup Tool</h1>

    <label for="taskDropdown">Select a task/guide/tool:</label>
    <select id="taskDropdown">
      <option value="" disabled selected>Loading tasks...</option>
      </select>

    <div id="loadingMessage" class="hidden">Loading details... <span style="font-style: italic;">Please wait...</span></div>
    <div id="errorMessage" class="hidden"></div>

    <div id="linkArea" class="content-section hidden">
       <span class="details-title">Link</span>
       <span id="linkDisplay"></span>
       <a id="linkButton" href="#" target="_blank">Open Link</a>
    </div>

    <div id="detailsArea" class="content-section hidden">
        <span class="details-title">Detail</span>
        <div id="detailsDisplay"></div>
    </div>

    <div id="detailsArea2" class="content-section hidden">
        <span class="details-title">Detail 2</span>
        <div id="detailsDisplay2"></div>
    </div>

    <div id="detailsArea3" class="content-section hidden">
        <span class="details-title">Detail 3</span>
        <div id="detailsDisplay3"></div>
    </div>
  </div> <script>
    // --- Initialization ---
    document.addEventListener('DOMContentLoaded', function() {
      google.script.run
        .withSuccessHandler(populateDropdown)
        .withFailureHandler(showError)
        .getTasks();
      document.getElementById('taskDropdown').addEventListener('change', handleTaskSelection);
    });

    // --- Functions ---
    function escapeHtml(unsafe) {
      if (unsafe === null || typeof unsafe === 'undefined') return "";
      return unsafe
           .toString()
           .replace(/&/g, "&")
           .replace(/</g, "<")
           .replace(/>/g, ">")
           .replace(/"/g, """)
           .replace(/'/g, "'");
    }
    function populateDropdown(tasks) {
        const dropdown = document.getElementById('taskDropdown');
        dropdown.innerHTML = '';
        if (!tasks || tasks.length === 0) {
          showError('No tasks found or error loading tasks from Sheet.');
          const defaultOption = document.createElement('option');
          defaultOption.value = ""; defaultOption.textContent = "No tasks available"; defaultOption.disabled = true;
          dropdown.appendChild(defaultOption); return;
        }
        const defaultOption = document.createElement('option');
        defaultOption.value = ""; defaultOption.textContent = "-- Select a Task --"; defaultOption.disabled = true; defaultOption.selected = true;
        dropdown.appendChild(defaultOption);
        tasks.forEach(function(task) {
          const option = document.createElement('option');
          option.value = task; option.textContent = task;
          dropdown.appendChild(option);
        });
        dropdown.disabled = false;
    }
    function handleTaskSelection() {
        const selectedTask = document.getElementById('taskDropdown').value;
        const linkArea = document.getElementById('linkArea');
        const detailsArea = document.getElementById('detailsArea');
        const detailsArea2 = document.getElementById('detailsArea2');
        const detailsArea3 = document.getElementById('detailsArea3');
        const loadingMsg = document.getElementById('loadingMessage');
        const errorMsg = document.getElementById('errorMessage');
        linkArea.classList.add('hidden'); detailsArea.classList.add('hidden');
        detailsArea2.classList.add('hidden'); detailsArea3.classList.add('hidden');
        errorMsg.classList.add('hidden'); loadingMsg.classList.add('hidden');
        document.getElementById('linkDisplay').textContent = '';
        document.getElementById('detailsDisplay').innerHTML = '';
        document.getElementById('detailsDisplay2').innerHTML = '';
        document.getElementById('detailsDisplay3').innerHTML = '';
        document.getElementById('linkButton').href = '#';
        if (selectedTask) {
          loadingMsg.classList.remove('hidden'); errorMsg.classList.add('hidden');
          google.script.run .withSuccessHandler(updateDetails) .withFailureHandler(showError) .getTaskDetails(selectedTask);
        }
    }
    function updateDetails(data) {
        const linkArea = document.getElementById('linkArea');
        const detailsArea = document.getElementById('detailsArea');
        const detailsArea2 = document.getElementById('detailsArea2');
        const detailsArea3 = document.getElementById('detailsArea3');
        const linkDisplay = document.getElementById('linkDisplay');
        const detailsDisplay = document.getElementById('detailsDisplay');
        const detailsDisplay2 = document.getElementById('detailsDisplay2');
        const detailsDisplay3 = document.getElementById('detailsDisplay3');
        const linkButton = document.getElementById('linkButton');
        const loadingMsg = document.getElementById('loadingMessage');
        loadingMsg.classList.add('hidden');
        if (data) {
          let contentFound = false;
          if (data.link) {
             linkDisplay.textContent = data.link; linkButton.href = data.link;
             linkArea.classList.remove('hidden'); contentFound = true;
          } else { linkArea.classList.add('hidden'); }
          if (data.details) {
            detailsDisplay.innerHTML = escapeHtml(data.details).replace(/\n/g, '<br>');
            detailsArea.classList.remove('hidden'); contentFound = true;
          } else { detailsArea.classList.add('hidden'); }
          if (data.details2) {
            detailsDisplay2.innerHTML = escapeHtml(data.details2).replace(/\n/g, '<br>');
            detailsArea2.classList.remove('hidden'); contentFound = true;
          } else { detailsArea2.classList.add('hidden'); }
          if (data.details3) {
            detailsDisplay3.innerHTML = escapeHtml(data.details3).replace(/\n/g, '<br>');
            detailsArea3.classList.remove('hidden'); contentFound = true;
          } else { detailsArea3.classList.add('hidden'); }
          if (!contentFound) { showError('No details or link found for the selected task.'); }
        } else { showError('Details not found or error fetching data.'); }
    }
    function showError(error) {
        const errorMsg = document.getElementById('errorMessage');
        const loadingMsg = document.getElementById('loadingMessage');
        loadingMsg.classList.add('hidden');
        const message = (typeof error === 'object' && error !== null && error.message) ? error.message : error;
        errorMsg.textContent = 'Error: ' + message;
        errorMsg.classList.remove('hidden');
        console.error('Apps Script Error:', error);
        document.getElementById('linkArea').classList.add('hidden');
        document.getElementById('detailsArea').classList.add('hidden');
        document.getElementById('detailsArea2').classList.add('hidden');
        document.getElementById('detailsArea3').classList.add('hidden');
    }
  </script>
</body>
</html>