PUP Survey Helper

Adds a button to help fill out PUP faculty evaluation surveys with organic randomization

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        PUP Survey Helper
// @namespace   Violentmonkey Scripts
// @match       https://survey.pup.edu.ph/apps/ofes/survey/*
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_registerMenuCommand
// @version     3.0
// @author      intMeinVoid
// @icon        https://www.pup.edu.ph/about/images/PUPLogo.png
// @description Adds a button to help fill out PUP faculty evaluation surveys with organic randomization
// @license     MIT
// ==/UserScript==

(function () {
  "use strict";

  const CONFIG = {
    DEFAULT_AVERAGE: 2.5,
    PRESETS: [2.0, 2.5, 3.0, 3.5, 4.0, 5.0],
    STORAGE_KEY: "pupSurveyLastUsedRating",
    COMMENT_TEXT: "No further comments. satisfied with the performance.",
  };

  // --- State Management ---
  const state = {
    isMinimized: false,
    lastRating: parseFloat(
      GM_getValue(CONFIG.STORAGE_KEY, CONFIG.DEFAULT_AVERAGE),
    ),
  };

  // --- UI Construction ---
  const container = document.createElement("div");
  container.id = "pup-survey-helper";
  container.style.cssText = `
        position: fixed; top: 20px; right: 20px; z-index: 9999;
        font-family: 'Segoe UI', Arial, sans-serif;
        background: rgba(255, 255, 255, 0.98);
        padding: 0; border-radius: 8px;
        box-shadow: 0 4px 15px rgba(0,0,0,0.15);
        border: 1px solid #e0e0e0;
        overflow: hidden;
        transition: height 0.3s ease;
    `;

  // Header with Minimize Button
  const header = document.createElement("div");
  header.style.cssText = `
        padding: 10px; background: #880000; color: white;
        display: flex; justify-content: space-between; align-items: center;
        font-weight: bold; font-size: 13px; cursor: pointer;
    `;
  header.innerHTML = `<span>🎓 PUP Eval Helper</span> <span id="toggle-icon">▼</span>`;

  const contentArea = document.createElement("div");
  contentArea.style.cssText = `padding: 12px; display: flex; flex-direction: column; gap: 8px;`;

  // Toggle Logic
  header.onclick = () => {
    state.isMinimized = !state.isMinimized;
    contentArea.style.display = state.isMinimized ? "none" : "flex";
    header.querySelector("#toggle-icon").textContent = state.isMinimized
      ? "▲"
      : "▼";
  };

  const createButton = (text, onClick, isPrimary = false, title = "") => {
    const btn = document.createElement("button");
    btn.textContent = text;
    btn.title = title;
    btn.style.cssText = `
            padding: 8px 12px;
            background-color: ${isPrimary ? "#900000" : "#f8f9fa"};
            color: ${isPrimary ? "white" : "#333"};
            border: 1px solid ${isPrimary ? "#900000" : "#ddd"};
            border-radius: 4px; cursor: pointer; font-size: 13px;
            transition: all 0.2s; flex: 1;
        `;
    btn.onmouseenter = () => (btn.style.filter = "brightness(0.95)");
    btn.onmouseleave = () => (btn.style.filter = "brightness(1)");
    btn.onclick = onClick;
    return btn;
  };

  // Preset Grid
  const presetsContainer = document.createElement("div");
  presetsContainer.style.cssText = `display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px;`;

  CONFIG.PRESETS.forEach((preset) => {
    presetsContainer.appendChild(
      createButton(preset, () => setEvaluation(preset)),
    );
  });

  // Action Buttons
  const mainButton = createButton(
    `Apply (${state.lastRating})`,
    () => setEvaluation(),
    true,
  );

  const commentButton = createButton(
    "✍️ Auto Comment",
    () => {
      const textAreas = document.querySelectorAll("textarea");
      if (textAreas.length === 0)
        return showToast("No comment boxes found", true);
      textAreas.forEach((area) => {
        if (!area.value) area.value = CONFIG.COMMENT_TEXT;
      });
      showToast(`Filled ${textAreas.length} comment boxes`);
    },
    false,
    "Fill empty comment boxes",
  );

  contentArea.append(presetsContainer, mainButton, commentButton);
  container.append(header, contentArea);
  document.body.appendChild(container);

  // --- Logic Utilities ---

  // Fisher-Yates Shuffle Algorithm
  const shuffleArray = (array) => {
    for (let i = array.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
  };

  const showToast = (message, isError = false) => {
    const toast = document.createElement("div");
    toast.textContent = message;
    toast.style.cssText = `
            position: fixed; bottom: 20px; right: 20px;
            background: ${isError ? "#dc3545" : "#28a745"};
            color: white; padding: 10px 20px; border-radius: 4px;
            z-index: 10001; font-size: 14px; box-shadow: 0 3px 6px rgba(0,0,0,0.2);
            animation: fadeIn 0.3s ease-in-out;
        `;
    document.body.appendChild(toast);
    setTimeout(() => {
      toast.style.opacity = "0";
      setTimeout(() => toast.remove(), 300);
    }, 3000);
  };

  // --- Main Logic ---
  const setEvaluation = async (targetAvg) => {
    try {
      if (targetAvg === undefined) {
        const input = prompt(
          "Enter target average (1.0 - 5.0):",
          state.lastRating,
        );
        if (input === null) return;
        targetAvg = parseFloat(input);
      }

      if (isNaN(targetAvg) || targetAvg < 1 || targetAvg > 5) {
        throw new Error("Please enter a number between 1 and 5");
      }

      // Save preference
      state.lastRating = targetAvg;
      GM_setValue(CONFIG.STORAGE_KEY, targetAvg);
      mainButton.textContent = `Apply (${targetAvg})`;

      // Identify Questions
      // Heuristic: Input names usually follow 'q1', 'q2' pattern in PUP SIS
      // We count unique names starting with 'q' followed by a number
      const allRadios = Array.from(
        document.querySelectorAll('input[type="radio"]'),
      );
      const questionNames = [
        ...new Set(allRadios.map((r) => r.name).filter((n) => /^q\d+/.test(n))),
      ]; // Filter ensures we only get question inputs

      const totalQuestions = questionNames.length;

      if (totalQuestions === 0)
        throw new Error("No evaluation questions detected.");

      // Calculate Math
      const exactTotal = targetAvg * totalQuestions;
      const roundedTotal = Math.round(exactTotal);
      const lowerValue = Math.floor(targetAvg);
      const higherValue = Math.ceil(targetAvg);
      const numberOfHigher = roundedTotal - lowerValue * totalQuestions;

      // Create Score Distribution Array
      let scoreDistribution = [];
      for (let i = 0; i < numberOfHigher; i++)
        scoreDistribution.push(higherValue);
      for (let i = 0; i < totalQuestions - numberOfHigher; i++)
        scoreDistribution.push(lowerValue);

      // SHUFFLE the scores to make it look organic
      scoreDistribution = shuffleArray(scoreDistribution);

      // Execute
      mainButton.textContent = "Processing...";
      mainButton.disabled = true;

      await new Promise((resolve) =>
        requestAnimationFrame(() => {
          let successCount = 0;

          // Iterate through the identified question names (q1, q2, etc.)
          questionNames.forEach((qName, index) => {
            const scoreToSet = scoreDistribution[index];

            // PUP Selector Logic: ID is usually q{questionNum}{score}
            // Extract the question number from the name (e.g. "q15" -> "15")
            const qNum = qName.replace("q", "");

            // Try ID selector first (Fastest)
            let targetRadio = document.getElementById(`q${qNum}${scoreToSet}`);

            // Fallback: Query by name and value if ID fails
            if (!targetRadio) {
              targetRadio = document.querySelector(
                `input[name="${qName}"][value="${scoreToSet}"]`,
              );
            }

            if (targetRadio) {
              targetRadio.checked = true;
              successCount++;
            }
          });

          showToast(
            `Filled ${successCount}/${totalQuestions} items (Avg: ~${targetAvg})`,
          );
          mainButton.textContent = `Apply (${targetAvg})`;
          mainButton.disabled = false;
          resolve();
        }),
      );
    } catch (error) {
      showToast(error.message, true);
      mainButton.disabled = false;
    }
  };

  // Keyboard Shortcuts
  document.addEventListener("keydown", (e) => {
    if (e.ctrlKey && e.shiftKey && e.code === "KeyE") {
      e.preventDefault();
      setEvaluation();
    }
  });

  // Style Injection
  const style = document.createElement("style");
  style.textContent = `@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }`;
  document.head.appendChild(style);
})();