PUP Survey Helper

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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);
})();