// ==UserScript== // @name IHSS Timesheet Autofill // @namespace ihss-autofill // @version 2.0 // @description Auto-populate IHSS timesheet hours with random distribution // @match https://etimesheets.ihss.ca.gov/* // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // ==/UserScript== (function () { "use strict"; const TARGET_PATH = "/provider-ts-details"; // --- Default preferences --- const DEFAULTS = { location: "Home", liveIn: false, daysPerWeek: 5, workDays: [1, 2, 3, 4, 5], // 0=Sun, 1=Mon, ..., 6=Sat startNoEarlier: "07:00", endNoLater: "18:00", lastHours: 40, }; const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; // --- Persistent preferences --- function loadPrefs() { const saved = GM_getValue("ihss_prefs", null); if (!saved) return { ...DEFAULTS }; try { return { ...DEFAULTS, ...JSON.parse(saved) }; } catch { return { ...DEFAULTS }; } } function savePrefs(prefs) { GM_setValue("ihss_prefs", JSON.stringify(prefs)); } // --- Utility --- function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function waitForElement(selector, timeout = 5000) { return new Promise((resolve, reject) => { const el = document.querySelector(selector); if (el) return resolve(el); const observer = new MutationObserver(() => { const el = document.querySelector(selector); if (el) { observer.disconnect(); resolve(el); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`Timeout waiting for ${selector}`)); }, timeout); }); } const nativeSetter = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, "value" ).set; function setInputValue(el, value) { el.focus(); nativeSetter.call(el, value); el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); el.dispatchEvent(new Event("blur", { bubbles: true })); } // --- Read available dates from the page --- function readPageDates() { const labels = document.querySelectorAll('[id^="hours-label-a11y-"]'); const inputIds = []; document.querySelectorAll('[id^="hours-"]').forEach((el) => { const m = el.id.match(/^hours-(\d+)$/); if (m) inputIds.push(parseInt(m[1])); }); inputIds.sort((a, b) => a - b); const dates = []; labels.forEach((label, i) => { if (i >= inputIds.length) return; const text = label.textContent.trim(); const match = text.match(/Hours for (.+)/); if (!match) return; const parsed = new Date(match[1]); if (isNaN(parsed)) return; dates.push({ iso: parsed.toISOString().split("T")[0], index: inputIds[i], dateObj: parsed, dayOfWeek: parsed.getDay(), // 0=Sun }); }); return dates; } // --- Distribute hours --- function distributeHours(totalHours, availableDates, prefs) { // Filter to preferred work days only const eligible = availableDates.filter((d) => prefs.workDays.includes(d.dayOfWeek) ); if (eligible.length === 0) return []; // Group by ISO week const weeks = {}; for (const d of eligible) { const dt = new Date(d.iso); const jan1 = new Date(dt.getFullYear(), 0, 1); const dayOfYear = Math.floor((dt - jan1) / 86400000); const weekNum = Math.ceil((dayOfYear + 1) / 7); if (!weeks[weekNum]) weeks[weekNum] = []; weeks[weekNum].push(d); } // Pick random days from each week (up to daysPerWeek) const selected = []; for (const wk of Object.keys(weeks).sort((a, b) => a - b)) { const pool = weeks[wk]; let pick = Math.min(prefs.daysPerWeek, pool.length); if (pool.length < prefs.daysPerWeek) { pick = Math.max(1, Math.round((pool.length * prefs.daysPerWeek) / 7)); } const shuffled = [...pool].sort(() => Math.random() - 0.5); selected.push( ...shuffled.slice(0, pick).sort((a, b) => a.index - b.index) ); } if (selected.length === 0) return []; // Spread hours const n = selected.length; const base = Math.floor(totalHours / n); let remainder = totalHours % n; const dayHours = selected.map(() => { const h = base + (remainder > 0 ? 1 : 0); if (remainder > 0) remainder--; return h; }); for (let i = dayHours.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [dayHours[i], dayHours[j]] = [dayHours[j], dayHours[i]]; } // Parse time constraints const [earliestH, earliestM] = prefs.startNoEarlier.split(":").map(Number); const [latestH, latestM] = prefs.endNoLater.split(":").map(Number); const earliestMinutes = earliestH * 60 + earliestM; const latestMinutes = latestH * 60 + latestM; return selected .map((d, i) => { const h = dayHours[i]; if (h === 0) return null; const workMinutes = h * 60; // Latest possible start so we finish by endNoLater const latestStart = latestMinutes - workMinutes; const effectiveEarliest = Math.max(earliestMinutes, 7 * 60); const effectiveLatest = Math.max(effectiveEarliest, latestStart); // Random start in 15-min increments within the allowed window const slots = []; for (let m = effectiveEarliest; m <= effectiveLatest; m += 15) { slots.push(m); } if (slots.length === 0) slots.push(effectiveEarliest); const startMins = slots[Math.floor(Math.random() * slots.length)]; const endMins = startMins + workMinutes; const sh = Math.floor(startMins / 60); const sm = startMins % 60; const eh = Math.floor(endMins / 60); const em = endMins % 60; return { ...d, hours: h, minutes: 0, start: `${String(sh).padStart(2, "0")}:${String(sm).padStart(2, "0")}`, end: `${String(eh).padStart(2, "0")}:${String(em).padStart(2, "0")}`, }; }) .filter(Boolean); } // --- Expand all workweek accordions --- async function expandAllAccordions() { const panels = document.querySelectorAll("mat-expansion-panel"); for (const panel of panels) { if (!panel.classList.contains("mat-expanded")) { const header = panel.querySelector("mat-expansion-panel-header"); if (header) { header.click(); await sleep(600); } } } } // --- Dismiss live-in modal --- async function dismissModal(liveIn) { const dialog = document.querySelector("mat-dialog-container"); if (!dialog) return; const buttons = dialog.querySelectorAll("mat-dialog-actions button"); for (const btn of buttons) { const text = btn.textContent.trim(); if ((!liveIn && text === "No") || (liveIn && text === "Yes")) { btn.click(); await sleep(800); return; } } } // --- Select a mat-option --- async function selectMatOption(text) { await waitForElement("mat-option"); await sleep(200); const options = document.querySelectorAll("mat-option"); for (const opt of options) { if (opt.textContent.trim().includes(text)) { opt.click(); await sleep(300); return; } } throw new Error(`Option "${text}" not found`); } // --- Fill a single day --- async function fillDay(entry, location) { const idx = entry.index; const hoursEl = document.getElementById(`hours-${idx}`); const minutesEl = document.getElementById(`minutes-${idx}`); const startEl = document.getElementById(`starttime-${idx}`); const endEl = document.getElementById(`endtime-${idx}`); const startLocEl = document.getElementById(`start-locationSelect-${idx}`); const endLocEl = document.getElementById(`end-locationSelect-${idx}`); if (!hoursEl) throw new Error(`hours-${idx} not found — is the accordion expanded?`); setInputValue(hoursEl, String(entry.hours).padStart(2, "0")); setInputValue(minutesEl, String(entry.minutes).padStart(2, "0")); setInputValue(startEl, entry.start); setInputValue(endEl, entry.end); startLocEl.click(); await selectMatOption(location); endLocEl.click(); await selectMatOption(location); } // --- Save workweeks --- async function saveWorkweeks(indices) { const wws = new Set( indices.map((idx) => (idx <= 6 ? 0 : idx <= 13 ? 1 : 2)) ); for (const ww of [...wws].sort()) { const btn = document.getElementById(`save-timesheet-button-${ww}`); if (btn && !btn.disabled) { btn.scrollIntoView({ behavior: "smooth", block: "center" }); await sleep(300); btn.click(); await sleep(2000); } } } // --- URL check --- function isOnTimesheetPage() { return window.location.pathname.includes(TARGET_PATH); } // --- Build the floating UI --- function createUI() { if (!isOnTimesheetPage()) return; if (document.getElementById("ihss-autofill-panel")) return; const prefs = loadPrefs(); const panel = document.createElement("div"); panel.id = "ihss-autofill-panel"; panel.innerHTML = `

IHSS Autofill

${DAY_NAMES.map( (name, i) => `
${name}
` ).join("")}
Settings auto-save
`; document.body.appendChild(panel); // --- Draggable --- let isDragging = false, offsetX, offsetY; const handle = panel.querySelector("h3"); handle.style.cursor = "grab"; handle.addEventListener("mousedown", (e) => { isDragging = true; offsetX = e.clientX - panel.getBoundingClientRect().left; offsetY = e.clientY - panel.getBoundingClientRect().top; handle.style.cursor = "grabbing"; }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; panel.style.left = e.clientX - offsetX + "px"; panel.style.top = e.clientY - offsetY + "px"; panel.style.right = "auto"; }); document.addEventListener("mouseup", () => { isDragging = false; handle.style.cursor = "grab"; }); // --- Minimize --- let minimized = false; const bodyEls = panel.querySelectorAll( ":scope > :not(#ihss-minimize):not(style)" ); document.getElementById("ihss-minimize").addEventListener("click", () => { minimized = !minimized; bodyEls.forEach((el) => (el.style.display = minimized ? "none" : "")); panel.style.width = minimized ? "auto" : "340px"; panel.style.padding = minimized ? "8px 16px" : "16px"; document.getElementById("ihss-minimize").textContent = minimized ? "+" : "_"; }); // --- Settings panel toggle --- document .getElementById("ihss-settings-toggle") .addEventListener("click", () => { const sp = document.getElementById("ihss-settings-panel"); sp.style.display = sp.style.display === "none" ? "block" : "none"; }); // --- Work day toggles --- panel.querySelectorAll(".ihss-day-check").forEach((el) => { el.addEventListener("click", () => { el.classList.toggle("active"); updatePrefsFromUI(); }); }); // --- Days per week slider --- document.getElementById("ihss-dpw").addEventListener("input", (e) => { document.getElementById("ihss-dpw-display").textContent = e.target.value; updatePrefsFromUI(); }); // --- Time inputs auto-save --- document .getElementById("ihss-start-time") .addEventListener("change", updatePrefsFromUI); document .getElementById("ihss-end-time") .addEventListener("change", updatePrefsFromUI); function updatePrefsFromUI() { const workDays = []; panel.querySelectorAll(".ihss-day-check.active").forEach((el) => { workDays.push(parseInt(el.dataset.day)); }); const p = { ...loadPrefs(), workDays, daysPerWeek: parseInt(document.getElementById("ihss-dpw").value), startNoEarlier: document.getElementById("ihss-start-time").value, endNoLater: document.getElementById("ihss-end-time").value, }; savePrefs(p); } // --- State --- let currentEntries = []; const status = document.getElementById("ihss-status"); const setStatus = (msg, cls) => { status.innerHTML = `${msg}`; }; // --- Generate --- function generate() { const hours = parseInt(document.getElementById("ihss-hours").value); if (!hours || hours < 1) { setStatus("Enter a number of hours", "ihss-err"); return; } // Save last-used hours const p = loadPrefs(); p.lastHours = hours; savePrefs(p); const dates = readPageDates(); if (dates.length === 0) { setStatus( "No timesheet inputs found — are you on the right page?", "ihss-err" ); return; } currentEntries = distributeHours(hours, dates, loadPrefs()); if (currentEntries.length === 0) { setStatus( "No eligible days — check your work day settings", "ihss-err" ); return; } let html = ""; for (const e of currentEntries) { const d = new Date(e.iso + "T12:00:00"); const dayStr = d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", }); html += ``; } html += "
DayHrsTime
${dayStr}${e.hours}h${e.start}-${e.end}
"; document.getElementById("ihss-preview").innerHTML = html; document.getElementById("ihss-actions").style.display = "flex"; setStatus(`${currentEntries.length} days, ${hours}h total`, "ihss-ok"); } document.getElementById("ihss-gen").addEventListener("click", generate); document.getElementById("ihss-regen").addEventListener("click", generate); document .getElementById("ihss-hours") .addEventListener("keydown", (e) => { if (e.key === "Enter") generate(); }); // --- Fill --- document .getElementById("ihss-fill") .addEventListener("click", async () => { if (currentEntries.length === 0) return; const prefs = loadPrefs(); const btn = document.getElementById("ihss-fill"); btn.disabled = true; btn.textContent = "Filling..."; try { await dismissModal(prefs.liveIn); setStatus("Expanding accordions...", "ihss-info"); await expandAllAccordions(); await sleep(500); for (let i = 0; i < currentEntries.length; i++) { const entry = currentEntries[i]; const d = new Date(entry.iso + "T12:00:00"); const dayStr = d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", }); setStatus( `Filling ${dayStr} (${i + 1}/${currentEntries.length})...`, "ihss-info" ); await fillDay(entry, prefs.location); await sleep(200); } setStatus("Saving workweeks...", "ihss-info"); await saveWorkweeks(currentEntries.map((e) => e.index)); setStatus("Done! Review and submit when ready.", "ihss-ok"); } catch (err) { setStatus(`Error: ${err.message}`, "ihss-err"); console.error("IHSS Autofill error:", err); } finally { btn.disabled = false; btn.textContent = "Fill Form"; } }); } // --- Remove panel when navigating away --- function removeUI() { const panel = document.getElementById("ihss-autofill-panel"); if (panel) panel.remove(); } // --- SPA-aware lifecycle --- let lastPath = window.location.pathname; function checkRoute() { const currentPath = window.location.pathname; if (currentPath !== lastPath) { lastPath = currentPath; if (isOnTimesheetPage()) { setTimeout(createUI, 1000); } else { removeUI(); } } } // Initial check if (isOnTimesheetPage()) { setTimeout(createUI, 1500); } // Watch for SPA navigation const observer = new MutationObserver(checkRoute); observer.observe(document.body, { childList: true, subtree: true }); // Also poll in case MutationObserver misses a route change setInterval(checkRoute, 1000); })();