|
// ==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); |
|
})(); |
gagi balagbag