[MTurk Worker] Dashboard Enhancer

Brings many enhancements to the MTurk Worker Dashboard.

Stan na 12-12-2017. Zobacz najnowsza wersja.

// ==UserScript==
// @name         [MTurk Worker] Dashboard Enhancer
// @namespace    http://kadauchi.com/
// @version      2.0.0
// @description  Brings many enhancements to the MTurk Worker Dashboard.
// @author       Kadauchi
// @icon         http://i.imgur.com/oGRQwPN.png
// @include      https://worker.mturk.com/dashboard*
// ==/UserScript==

const dashboard = new Object();

(function () {
    Object.assign(String.prototype, {
        number () {
            return Number(this.replace(/[^0-9.]/g, ``));
        }
    });

    Object.assign(Number.prototype, {
        number () {
            return this;
        }
    });

    const rows = document.getElementById(`dashboard-hits-overview`).getElementsByClassName(`row`);

    dashboard.hits_overview = {
        approved: rows[0].getElementsByClassName(`text-xs-right`)[0].textContent.number(),
        pending: rows[2].getElementsByClassName(`text-xs-right`)[0].textContent.number(),
        rejected: rows[3].getElementsByClassName(`text-xs-right`)[0].textContent.number(),
    };
    dashboard.daily_hit_statistics_overview = {
        total: {
            submitted: 0,
            approved: 0,
            rejected: 0,
            pending: 0,
            earnings: 0,
        }
    };
    dashboard.earnings_to_date = {
        bonuses: document.getElementById(`dashboard-earnings-to-date`).getElementsByClassName(`text-xs-right`)[2].textContent.number()
    };

    for (const row of document.getElementsByClassName(`daily_hit_statuses`)) {
        const col = row.children;
        const date = col[0].children[0].href.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)[0];

        dashboard.daily_hit_statistics_overview[date] = new Object();

        [`submitted`, `approved`, `rejected`, `pending`, `earnings`].forEach((currentValue, index) => {
            const value = col[index + 1].textContent.number();

            dashboard.daily_hit_statistics_overview[date][currentValue] = value;
            dashboard.daily_hit_statistics_overview.total[currentValue] += value;
        });
    }
})();

(function allApprovedRate() {
    const overview = dashboard.hits_overview;

    const row = document.createElement(`div`);
    row.className = `row m-b-sm`;

    const col1 = document.createElement(`div`);
    col1.className = `col-xs-7`;
    row.appendChild(col1);

    const strong = document.createElement(`strong`);
    strong.textContent = `All Approved Rate`;
    col1.appendChild(strong);

    const col2 = document.createElement(`div`);
    col2.className = `col-xs-5 text-xs-right`;
    col2.textContent = `${((overview.approved + overview.pending) / (overview.approved + overview.pending + overview.rejected) * 100).toFixed(4)}%`;
    row.appendChild(col2);

    const additional = document.getElementById(`HitsOverviewAdditionalInfo`);
    additional.parentNode.insertBefore(row, additional);
})();

(function allRejectedRate() {
    const overview = dashboard.hits_overview;

    const row = document.createElement(`div`);
    row.className = `row m-b-sm`;

    const col1 = document.createElement(`div`);
    col1.className = `col-xs-7`;
    row.appendChild(col1);

    const strong = document.createElement(`strong`);
    strong.textContent = `All Rejected Rate`;
    col1.appendChild(strong);

    const col2 = document.createElement(`div`);
    col2.textContent = `${(overview.approved / (overview.approved + overview.rejected + overview.pending) * 100).toFixed(4)}%`;
    col2.className = `col-xs-5 text-xs-right`;
    row.appendChild(col2);

    const additional = document.getElementById(`HitsOverviewAdditionalInfo`);
    additional.parentNode.insertBefore(row, additional);
})();

(function fourDigitPercents() {
    const overview = dashboard.hits_overview;

    for (const row of document.getElementById(`dashboard-hits-overview`).getElementsByClassName(`row`)) {
        if (row.textContent.indexOf(`Approval Rate`) !== -1) {
            row.getElementsByClassName(`text-xs-right`)[0].textContent = `${(overview.approved / (overview.approved + overview.rejected) * 100).toFixed(4)}%`;
        }
        if (row.textContent.indexOf(`Rejection Rate`) !== -1) {
            row.getElementsByClassName(`text-xs-right`)[0].textContent = `${(overview.rejected / (overview.approved + overview.rejected) * 100).toFixed(4)}%`;
        }
    }
})();

(function rejectionsBelow99() {
    const overview = dashboard.hits_overview;

    const row = document.createElement(`div`);
    row.className = `row m-b-sm`;

    const col1 = document.createElement(`div`);
    col1.className = `col-xs-7`;
    row.appendChild(col1);

    const strong = document.createElement(`strong`);
    strong.textContent = `Rejections ≤ 99%`;
    col1.appendChild(strong);

    const col2 = document.createElement(`div`);
    col2.textContent = Math.round((overview.rejected - (0.01 * (overview.approved + overview.rejected + overview.pending))) / -0.99).toLocaleString();
    col2.className = `col-xs-5 text-xs-right`;
    row.appendChild(col2);

    const additional = document.getElementById(`HitsOverviewAdditionalInfo`);
    additional.insertBefore(row, additional.lastChild);
})();

(function rejectionsBelow95() {
    const overview = dashboard.hits_overview;

    const row = document.createElement(`div`);
    row.className = `row m-b-sm`;

    const col1 = document.createElement(`div`);
    col1.className = `col-xs-7`;
    row.appendChild(col1);

    const strong = document.createElement(`strong`);
    strong.textContent = `Rejections ≤ 95%`;
    col1.appendChild(strong);

    const col2 = document.createElement(`div`);
    col2.textContent = Math.round((overview.rejected - (0.05 * (overview.approved + overview.rejected + overview.pending))) / -0.95).toLocaleString();
    col2.className = `col-xs-5 text-xs-right`;
    row.appendChild(col2);

    const additional = document.getElementById(`HitsOverviewAdditionalInfo`);
    additional.insertBefore(row, additional.lastChild);
})();

(function totalLast45Days() {
    const table = document.querySelector(`.mturk-table.hits-statuses`);

    const row = table.insertRow(1);
    row.className = `daily_hit_statuses`;

    const date = row.insertCell(0);
    date.textContent = `Total`;
    date.className = `hidden-xs-down col-sm-2 col-md-2`;

    const submitted = row.insertCell(1);
    submitted.textContent = dashboard.daily_hit_statistics_overview.total.submitted;
    submitted.className = `text-xs-right col-xs-3 col-sm-2 col-md-2`;

    const approved = row.insertCell(2);
    approved.textContent = dashboard.daily_hit_statistics_overview.total.approved;
    approved.className = `text-xs-right col-xs-3 col-sm-2 col-md-2`;

    const rejected = row.insertCell(3);
    rejected.textContent = dashboard.daily_hit_statistics_overview.total.rejected;
    rejected.className = `text-xs-right col-xs-3 col-sm-2 col-md-2`;

    const pending = row.insertCell(4);
    pending.textContent = dashboard.daily_hit_statistics_overview.total.pending;
    pending.className = `text-xs-right col-xs-3 col-sm-2 col-md-2`;

    const earnings = row.insertCell(5);
    earnings.textContent = `$${dashboard.daily_hit_statistics_overview.total.earnings.toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`;
    earnings.className = `text-xs-right col-xs-3 col-sm-2 col-md-2`;
})();

(function hitStatusChanges() {
    const oldDashboard = localStorage.dashboard ? JSON.parse(localStorage.dashboard) : null;
    localStorage.dashboard = JSON.stringify(dashboard);

    for (const row of document.getElementsByClassName(`daily_hit_statuses`)) {
        const col = row.children;
        const date = col[0].children[0] ? col[0].children[0].href.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)[0] : `total`;

        [`submitted`, `approved`, `rejected`, `pending`, `earnings`].forEach((currentValue, index) => {
            const value = col[index + 1].textContent.number();
            const oldValue = oldDashboard ? oldDashboard.daily_hit_statistics_overview[date][currentValue] : 0;

            if (value !== oldValue) {
                const change = value - oldValue;
                const changeString = change.toFixed(2).toString().replace(`.00`, ``);

                if (Math.round(change * 100) !== 0) {
                    const span = document.createElement(`span`);
                    span.textContent = change > 0 ? `+${changeString}` : changeString;
                    span.style.float = `left`;
                    span.style.fontSize = `70%`;
                    col[index + 1].appendChild(span);
                }
            }
        });
    }
})();

(function todaysActivity() {
    const container = document.createElement(`div`);
    container.className = `row m-b-xl`;

    const col = document.createElement(`div`);
    col.className = `col-xs-12`;
    container.appendChild(col);

    const h2 = document.createElement(`h2`);
    h2.className = `m-b-md`;
    h2.textContent = `Today's Activity`;
    col.appendChild(h2);

    const row = document.createElement(`div`);
    row.className = `row`;
    col.appendChild(row);

    const col2 = document.createElement(`div`);
    col2.className = `col-xs-12`;
    row.appendChild(col2);

    const border = document.createElement(`div`);
    border.className = `border-gray-lightest p-a-sm`;
    col2.appendChild(border);

    const earningsRow = document.createElement(`div`);
    earningsRow.className = `row m-b-sm`;
    border.appendChild(earningsRow);

    const earningsText = document.createElement(`div`);
    earningsText.className = `col-xs-7 col-sm-6 col-lg-7`;
    earningsRow.appendChild(earningsText);

    const earningsStrong = document.createElement(`strong`);
    earningsStrong.textContent = `Projected Earnings`;
    earningsText.appendChild(earningsStrong);

    const earningsValue = document.createElement(`div`);
    earningsValue.className = `col-xs-5 col-sm-6 col-lg-5 text-xs-right`;
    earningsValue.textContent = localStorage.todaysearnings || `$0.00`;
    earningsRow.appendChild(earningsValue);

    const bonusesRow = document.createElement(`div`);
    bonusesRow.className = `row m-b-sm`;
    border.appendChild(bonusesRow);

    const bonusesText = document.createElement(`div`);
    bonusesText.className = `col-xs-7 col-sm-6 col-lg-7`;
    bonusesRow.appendChild(bonusesText);

    const bonusesStrong = document.createElement(`strong`);
    bonusesStrong.textContent = `Bonuses`;
    bonusesText.appendChild(bonusesStrong);

    const bonusesValue = document.createElement(`div`);
    bonusesValue.className = `col-xs-5 col-sm-6 col-lg-5 text-xs-right`;
    bonusesValue.textContent = localStorage.todaysbonuses || `$0.00`;
    bonusesRow.appendChild(bonusesValue);

    const collapse = document.createElement(`div`);
    collapse.id = `TodaysActivityAdditionalInfo`;
    collapse.className = `collapse`;
    border.appendChild(collapse);

    const hr = document.createElement(`hr`);
    hr.className = `m-b-sm m-t-0`;
    collapse.appendChild(hr);

    const hr2 = document.createElement(`hr`);
    hr2.className = `m-b-sm m-t-0`;
    border.appendChild(hr2);

    const control = document.createElement(`a`);
    control.className = `collapse-more-less`;
    control.href = `#TodaysActivityAdditionalInfo`;
    control.setAttribute(`aria-controls`, `TodaysActivityAdditionalInfo`);
    control.setAttribute(`aria-expanded`, `false`);
    control.setAttribute(`data-toggle`, `collapse`);
    border.appendChild(control);

    const more = document.createElement(`span`);
    more.className = `more`;
    control.appendChild(more);

    const plus = document.createElement(`i`);
    plus.className = `fa fa-plus-circle`;
    more.appendChild(plus);

    const moreText = document.createTextNode(`\nMore\n`);
    more.appendChild(moreText);

    const less = document.createElement(`span`);
    less.className = `less`;
    control.appendChild(less);

    const minus = document.createElement(`i`);
    minus.className = `fa fa-minus-circle`;
    less.appendChild(minus);

    const lessText = document.createTextNode(`\nLess\n`);
    less.appendChild(lessText);

    const side = document.querySelector(`.col-md-push-8`);
    side.insertBefore(container, side.firstChild);

    const today = document.querySelector(`a[href^="/status_details/"]`);

    if (today.textContent === `Today`) {
        const date = today.href.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)[0];

        if (date === localStorage.WMTD_date) {
            if (!localStorage.WMTD_bonusStart) {
                localStorage.WMTD_bonusStart = dashboard.earnings_to_date.bonuses;
            }
        }
        else {
            if (dashboard.daily_hit_statistics_overview[date].approved === 0) {
                localStorage.WMTD_bonusStart = dashboard.earnings_to_date.bonuses - dashboard.daily_hit_statistics_overview[date].earnings;
            }
            else {
                localStorage.WMTD_bonusStart = dashboard.earnings_to_date.bonuses;
            }
        }

        bonusesValue.textContent = `$${(dashboard.earnings_to_date.bonuses - localStorage.WMTD_bonusStart).toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`;

        const hitLog = date === localStorage.WMTD_date ? localStorage.WMTD_hitLog ? JSON.parse(localStorage.WMTD_hitLog) : {} : {};

        async function get(page, rescan) {
            try {
                earningsValue.textContent = `Calculating Page ${page}`;

                const fetchURL = new URL(`https://worker.mturk.com/status_details/${date}`);
                fetchURL.searchParams.append(`page_number`, page);
                fetchURL.searchParams.append(`format`, `json`);

                const response = await fetch(fetchURL, {
                    credentials: `include`
                });

                if (response.status === 429) {
                    return setTimeout(get, 2000, url, rescan);
                }

                const json = await response.json();

                for (const hit of json.results) {
                    hitLog[hit.hit_id] = hit;
                }

                const logLength = Object.keys(hitLog).length;
                const expectedLength = page.number() * 20 - 20 + json.num_results;

                if (!rescan && logLength !== expectedLength) {
                    return get(1, true);
                }
                else {
                    localStorage.WMTD_hitLog = JSON.stringify(hitLog);
                }

                localStorage.WMTD_lastPage = page;

                if (json.results.length === 20) {
                    return get(++ page, rescan);
                }
                else if (logLength !== json.total_num_results) {
                    return get(1, true);
                }
                else {
                    let projectedEarnings = 0;
                    const reqLog = {};

                    for (const key in hitLog) {
                        const hit = hitLog[key];

                        if (hit.status !== `Rejected`) {
                            projectedEarnings += hit.reward.amount_in_dollars;
                        }
                        if (!reqLog[hit.requester_id]){
                            reqLog[hit.requester_id] = {
                                requester_id: hit.requester_id,
                                requester_name: hit.requester_name,
                                reward: hit.reward.amount_in_dollars,
                                submitted: 1,
                            };
                        }
                        else {
                            reqLog[hit.requester_id].submitted += 1;
                            reqLog[hit.requester_id].reward += hit.reward.amount_in_dollars;
                        }
                    }

                    const sort = Object.keys(reqLog).sort((a, b) => reqLog[a].reward - reqLog[b].reward);

                    const fragment = document.createDocumentFragment();

                    for (let i = sort.length - 1; i > -1; i--) {
                        const key = sort[i];
                        const requester_name = reqLog[key].requester_name;
                        const reward = `$${reqLog[key].reward.toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`;
                        const submitted = reqLog[key].submitted;

                        const reqRow = document.createElement(`div`);
                        reqRow.className = `row m-b-sm`;
                        fragment.appendChild(reqRow);

                        const requester = document.createElement(`div`);
                        requester.className = `col-xs-6`;
                        reqRow.appendChild(requester);

                        const requesterStrong = document.createElement(`strong`);
                        requesterStrong.textContent = requester_name;
                        requester.appendChild(requesterStrong);

                        const submitValue = document.createElement(`div`);
                        submitValue.className = `col-xs-3 text-xs-right`;
                        submitValue.textContent = submitted;
                        reqRow.appendChild(submitValue);

                        const rewardValue = document.createElement(`div`);
                        rewardValue.className = `col-xs-3 text-xs-right`;
                        rewardValue.textContent = reward;
                        reqRow.appendChild(rewardValue);
                    }

                    collapse.appendChild(fragment);

                    earningsValue.textContent = `$${projectedEarnings.toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`;
                }
            }
            catch (error) {
                earningsValue.textContent = error;
            }
        }

        get(date === localStorage.WMTD_date ? localStorage.WMTD_lastPage ? localStorage.WMTD_lastPage.number() : 1 : 1, false);
        localStorage.WMTD_date = date;
    }
    else {
        earningsValue.textContent = `N/A`;
        bonusesValue.textContent = `N/A`;
    }
})();