IMOS Auto Payment Filler

Auto-fill one-time payment forms in IMOS based on URL params

// ==UserScript==
// @name         IMOS Auto Payment Filler
// @namespace    https://imos.churchofjesuschrist.org/
// @version      1.0
// @description  Auto-fill one-time payment forms in IMOS based on URL params
// @match        https://imos.churchofjesuschrist.org/payments/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Utility: wait for element to exist
    function waitForElement(selector, callback, timeout = 15000) {
        const start = Date.now();
        const interval = setInterval(() => {
            const el = document.querySelector(selector);
            if (el) {
                clearInterval(interval);
                callback(el);
            } else if (Date.now() - start > timeout) {
                clearInterval(interval);
                console.warn("Timeout waiting for:", selector);
            }
        }, 300);
    }

    function setAngularValue(el, value) {
        el.value = value;
        el.dispatchEvent(new Event('input', { bubbles: true }));
        el.dispatchEvent(new Event('change', { bubbles: true }));
        el.dispatchEvent(new Event('keyup', { bubbles: true }));
        el.dispatchEvent(new Event('blur', { bubbles: true }));
    }

    // Parse URL params
    // get the hash part
    let hash = window.location.hash; // "#!/one-time-payment/create-new?type=missionary&id=815478&li=EMERGENCY%20FOOD&val=2561&inv=MISSIONARY%20REIMBURSEMENT%2016&tax=250"

    // find the "?" and take everything after it
    let queryString = hash.includes('?') ? hash.split('?')[1] : '';
    let urlParams = new URLSearchParams(queryString);

    // now this works
    let type = urlParams.get("type"); // "missionary"
    let id = urlParams.get("id"); // "815478"
    let inv = urlParams.get("inv"); // "MISSIONARY REIMBURSEMENT 16"
    let tax = urlParams.get("tax"); // "250"


    // Collect multiple li / val pairs
    const lineItems = [];
    urlParams.forEach((val, key) => {
        if (key.startsWith("li")) {
            const index = key.includes("_") ? key.split("_")[1] : "1";
            const amount = urlParams.get("val_" + index) || urlParams.get("val");
            lineItems.push({ li: val, val: amount });
        }
    });
    if (lineItems.length === 0 && urlParams.get("li")) {
        lineItems.push({ li: urlParams.get("li"), val: urlParams.get("val") });
    }

    // Step 1: Select payee type (missionary, reimbursement, etc.)
    waitForElement("select[ng-model='payeeType.service.payment.selectedPayeeType']", (sel) => {
        sel.value = type.toUpperCase();
        sel.dispatchEvent(new Event('change', { bubbles: true }));
        console.log("Selected payee type:", sel.value);

        if (type === "missionary") {
            // Step 2: Select missionary payee by id
            waitForElement("select[ng-model='payeeName.service.payment.selectedPayeeMissionary']", (msel) => {
                const option = msel.querySelector(`option[value="${id}"]`);
                if (option) {
                    msel.value = id;
                    msel.dispatchEvent(new Event('change', { bubbles: true }));
                    console.log("Selected missionary:", option.label);
                }
            });
        }

        // Step 3: Fill invoice number
        if (inv) {
            waitForElement("input[ng-model='invoiceNumber.service.payment.invoiceNumber']", (invInput) => {
                invInput.value = inv;
                invInput.dispatchEvent(new Event('input', { bubbles: true }));
                console.log("Set invoice:", inv);
            });
        }

        // Step 4: Add line items
        waitForElement("input[ng-model='line.distributionDescription']", (descInput) => {
            // Need to fill multiple rows: loop through lineItems
            lineItems.forEach((item, idx) => {
                if (idx > 0) {
                    // Add row for additional items
                    const addBtn = document.querySelector("imos-button[type='action add'] button");
                    if (addBtn) addBtn.click();
                }

                setTimeout(() => {
                    const descs = document.querySelectorAll("input[ng-model='line.distributionDescription']");
                    const amounts = document.querySelectorAll("input[ng-model='line.amountDisplayedToUser']");
                    if (descs[idx] && amounts[idx]) {
                        descs[idx].value = item.li;
                        descs[idx].dispatchEvent(new Event('input', { bubbles: true }));

                        setAngularValue(amounts[idx], item.val);

                        console.log("Added line:", item.li, item.val);
                    }
                }, 500 * (idx+1));
            });

            // Step 5: Tax line if present
            if (tax) {
                setTimeout(() => {
                    const addBtn = document.querySelector("imos-button[type='action add'] button");
                    if (addBtn) addBtn.click();

                    setTimeout(() => {
                        const lastDesc = [...document.querySelectorAll("input[ng-model='line.distributionDescription']")].pop();
                        const lastAmt = [...document.querySelectorAll("input[ng-model='line.amountDisplayedToUser']")].pop();
                        const lastAcct = [...document.querySelectorAll("select[ng-model='line.account']")].pop();

                        if (lastDesc && lastAmt && lastAcct) {
                            setAngularValue(lastDesc, "TAX");
                            setAngularValue(lastAmt, tax);

                            // select "Food and Other Expenses"
                            const opt = [...lastAcct.options].find(o => o.label.includes("Food and Other Expenses"));
                            if (opt) {
                                lastAcct.value = opt.value;
                                lastAcct.dispatchEvent(new Event('change', { bubbles: true }));
                            }

                            console.log("Added TAX line:", tax);
                        }
                    }, 500);
                }, 600 * (lineItems.length+1));
            }
        });
    });
})();