Shell script scrapes @name/@description from userscripts and builds a table in README.md between marker comments. Pre-commit hook runs it automatically so the table stays in sync. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
633 lines
22 KiB
JavaScript
633 lines
22 KiB
JavaScript
// ==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
|
|
// @downloadURL https://git.jeirslab.xyz/jeirmeister/tampermonkey-scripts/raw/branch/master/scripts/ihss-autofill.user.js
|
|
// @updateURL https://git.jeirslab.xyz/jeirmeister/tampermonkey-scripts/raw/branch/master/scripts/ihss-autofill.user.js
|
|
// ==/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 = `
|
|
<style>
|
|
#ihss-autofill-panel {
|
|
position: fixed; top: 80px; right: 20px; z-index: 99999;
|
|
background: #1a1a2e; color: #eee; border-radius: 12px;
|
|
padding: 16px; width: 340px; font-family: -apple-system, sans-serif;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.5); border: 1px solid #0f3460;
|
|
}
|
|
#ihss-autofill-panel h3 {
|
|
margin: 0 0 12px 0; color: #e94560; font-size: 15px;
|
|
user-select: none;
|
|
}
|
|
#ihss-autofill-panel label {
|
|
font-size: 12px; color: #8892b0; display: block; margin-bottom: 3px;
|
|
}
|
|
#ihss-autofill-panel input[type=number],
|
|
#ihss-autofill-panel input[type=time] {
|
|
padding: 5px 7px; border: 2px solid #0f3460;
|
|
border-radius: 6px; background: #0a0a23; color: #eee; font-size: 14px;
|
|
}
|
|
#ihss-autofill-panel input[type=number] { width: 70px; }
|
|
#ihss-autofill-panel input[type=time] { width: 110px; }
|
|
#ihss-autofill-panel input:focus { outline: none; border-color: #e94560; }
|
|
#ihss-autofill-panel button {
|
|
padding: 7px 14px; border: none; border-radius: 6px; font-size: 13px;
|
|
cursor: pointer; font-weight: 600; transition: all 0.15s;
|
|
}
|
|
.ihss-btn-gen { background: #0f3460; color: #a8b2d1; }
|
|
.ihss-btn-gen:hover { background: #1a4a7a; }
|
|
.ihss-btn-fill { background: #e94560; color: white; }
|
|
.ihss-btn-fill:hover { background: #c73e54; }
|
|
.ihss-btn-save { background: #2d6a4f; color: white; }
|
|
.ihss-btn-save:hover { background: #40916c; }
|
|
.ihss-btn-settings { background: none; color: #8892b0; font-size: 16px; padding: 4px 8px; }
|
|
.ihss-btn-settings:hover { color: #eee; }
|
|
#ihss-preview { max-height: 220px; overflow-y: auto; margin-top: 10px; font-size: 12px; }
|
|
#ihss-preview table { width: 100%; border-collapse: collapse; }
|
|
#ihss-preview th { color: #8892b0; text-align: left; padding: 3px 6px; border-bottom: 1px solid #0f3460; }
|
|
#ihss-preview td { padding: 3px 6px; border-bottom: 1px solid #0a0a23; }
|
|
#ihss-status { margin-top: 8px; font-size: 12px; min-height: 18px; }
|
|
.ihss-ok { color: #4ade80; }
|
|
.ihss-err { color: #f87171; }
|
|
.ihss-info { color: #60a5fa; }
|
|
.ihss-row { display: flex; gap: 8px; align-items: end; margin-bottom: 10px; }
|
|
.ihss-btn-row { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
#ihss-minimize {
|
|
position: absolute; top: 8px; right: 12px; background: none;
|
|
border: none; color: #8892b0; cursor: pointer; font-size: 18px; padding: 0;
|
|
}
|
|
#ihss-minimize:hover { color: #eee; }
|
|
#ihss-settings-panel {
|
|
display: none; margin-top: 10px; padding-top: 10px;
|
|
border-top: 1px solid #0f3460;
|
|
}
|
|
.ihss-day-checks { display: flex; gap: 4px; margin: 6px 0 10px 0; }
|
|
.ihss-day-check {
|
|
width: 36px; height: 28px; border-radius: 4px; border: 2px solid #0f3460;
|
|
background: #0a0a23; color: #8892b0; font-size: 11px; font-weight: 600;
|
|
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
transition: all 0.15s; user-select: none;
|
|
}
|
|
.ihss-day-check.active { background: #0f3460; color: #eee; border-color: #e94560; }
|
|
.ihss-time-row { display: flex; gap: 10px; margin-bottom: 8px; }
|
|
.ihss-time-row > div { flex: 1; }
|
|
</style>
|
|
<button id="ihss-minimize" title="Minimize">_</button>
|
|
<h3>IHSS Autofill</h3>
|
|
<div class="ihss-row">
|
|
<div>
|
|
<label for="ihss-hours">Total hours</label>
|
|
<input type="number" id="ihss-hours" min="1" max="200" value="${prefs.lastHours}">
|
|
</div>
|
|
<button class="ihss-btn-gen" id="ihss-gen">Generate</button>
|
|
<button class="ihss-btn-settings" id="ihss-settings-toggle" title="Settings">⚙</button>
|
|
</div>
|
|
<div id="ihss-settings-panel">
|
|
<label>Work days</label>
|
|
<div class="ihss-day-checks">
|
|
${DAY_NAMES.map(
|
|
(name, i) =>
|
|
`<div class="ihss-day-check ${prefs.workDays.includes(i) ? "active" : ""}" data-day="${i}">${name}</div>`
|
|
).join("")}
|
|
</div>
|
|
<div class="ihss-time-row">
|
|
<div>
|
|
<label for="ihss-start-time">Start no earlier than</label>
|
|
<input type="time" id="ihss-start-time" value="${prefs.startNoEarlier}">
|
|
</div>
|
|
<div>
|
|
<label for="ihss-end-time">End no later than</label>
|
|
<input type="time" id="ihss-end-time" value="${prefs.endNoLater}">
|
|
</div>
|
|
</div>
|
|
<label>Days per week: <strong id="ihss-dpw-display">${prefs.daysPerWeek}</strong></label>
|
|
<input type="range" id="ihss-dpw" min="1" max="7" value="${prefs.daysPerWeek}"
|
|
style="width:100%; margin-bottom:8px; accent-color:#e94560;">
|
|
<div style="font-size:11px; color:#4ade80; margin-top:4px;">Settings auto-save</div>
|
|
</div>
|
|
<div id="ihss-preview"></div>
|
|
<div class="ihss-btn-row" id="ihss-actions" style="display:none;">
|
|
<button class="ihss-btn-gen" id="ihss-regen">Regenerate</button>
|
|
<button class="ihss-btn-fill" id="ihss-fill">Fill Form</button>
|
|
</div>
|
|
<div id="ihss-status"></div>
|
|
`;
|
|
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 = `<span class="${cls}">${msg}</span>`;
|
|
};
|
|
|
|
// --- 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 = "<table><tr><th>Day</th><th>Hrs</th><th>Time</th></tr>";
|
|
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 += `<tr><td>${dayStr}</td><td>${e.hours}h</td><td>${e.start}-${e.end}</td></tr>`;
|
|
}
|
|
html += "</table>";
|
|
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);
|
|
})();
|