Company Enhancement Suite

Your one stop solution to managing your company.

目前為 2019-05-26 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Company Enhancement Suite
// @namespace    LordBusiness.CES
// @version      3.4
// @description  Your one stop solution to managing your company.
// @author       LordBusiness
// @match        *.torn.com/companies.php
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
    .last_action_icon {
        cursor: pointer;
        vertical-align: middle;
        display: inline-block;
        background-image: url(/images/v2/sidebar_icons_desktop_2017.png);
        background-repeat: no-repeat;
        background-position-y: -783px;
        width: 34px;
        height: 30px;
    }

    .stock-list li:not(.total) .delivery {
        background-color: white;
    }

    .order.hidden ~ .fill-custom,
    .order.hidden ~ .fill-daily {
        display: none;
    }`);

    const APIkeyName = 'TornApiKey';
    let APIkey = localStorage.getItem(APIkeyName),
        itemsStock = JSON.parse(localStorage.getItem("TornCustomStock") || '{}'),
        keyUpEvent = new KeyboardEvent('keyup');

    /* Credits to Mafia [610357] for the original snippet of code from his excellent library, tornlib.
       Rewritten in Vanilla JS cuz .. who uses jQuery nowadays? */
    const getAPIkey = () => {
        localStorage.removeItem(APIkeyName);
        document.querySelector('div.content-title').insertAdjacentHTML('afterend',
        `<div id='apibox' class="m-top10">
            <div class="title-gray top-round" role="heading" aria-level="5">
            <i class="issue-attention-icon"></i>
            <span id="title">API Key required</span>
            </div>
            <div class="bottom-round cont-gray p10" id="api-input-wrapper">
                <fieldset class="submit-wrap">
                    <p>You are currently using <strong>${GM.info.script.name}</strong> that requires your API key. Please fill in your API key to use it.</p><br/>
                    <div class="cont-quantity left">
                        <div class="input-money-group"><span title="Fill with your correct API key" class="input-money-symbol">KEY</span><input id="api-input" class="quantity price input-money" type="text" value=""></div>
                    </div>
                    <div class="cont-button left" id="apiSignIn" style="margin-left: 10px;">
                        <span class="btn-wrap silver">
                            <span class="btn c-pointer bold" style="padding: 0 15px 0 10px;"><span>SIGN IN</span></span>
                        </span>
                    </div>
                    <div class="clear"></div>
                </fieldset>
            </div>
            <!--div class="clear"></div-->
            <hr class="page-head-delimiter m-top10">
        </div>`);
        const apiWrapper = document.getElementById("api-input-wrapper");
        document.getElementById("apiSignIn").addEventListener('click', () => {
            APIkey = document.getElementById("api-input").value.trim();
            fetch(`https://api.torn.com/user/?selections=profile&key=${APIkey}`)
                .then(response => response.json())
                .then(data => {
                if(data.error) {
                    alert(data.error.error);
                    apiWrapper.querySelector(".input-money-group").classList.add("error");
                } else {
                    localStorage.setItem(APIkeyName, APIkey);
                    apiWrapper.innerHTML = `Hi ${data.name}, you have successfully signed in with your API key. This page will be refreshed in a moment.`;
                    setTimeout(() => location.reload(), 3000);
                }
            });
        });
    };

    // Add the t-red class to parent element
    const makeRed = theElement => theElement.parentElement.classList.add('t-red');

    // Check trains to make sure you don't lose a few tomorrow
    const checkTrains = () => {
        const trains = document.querySelector(".trains"),
              stars = document.querySelector(".company-rating").querySelectorAll("li.active").length;
        if(parseInt(trains.innerText) + stars > 20) {
            makeRed(trains);
        }
    }

    // Check if employee capacity is filled
    const checkEmployees = () => {
        const currentEmployees = document.querySelector(".total-employees"),
              maxEmployees = document.querySelector(".limit-employees");
        if(parseInt(maxEmployees.innerText) - parseInt(currentEmployees.innerText)) {
            makeRed(maxEmployees);
        }
    }

    /* Employee last action is a refined (AKA semi-colonified) version of tos's Faction Last Action script.
       See https://www.torn.com/forums.php#/p=threads&f=67&t=16046522&b=0&a=0&to=18638360
       I'm starting to wonder if my script is just a compilation of everybody else's scripts. */

    const generateTimeColor = timeInterval => {
        if (timeInterval.includes('minute')) return 'ftGreen';
        else if (timeInterval.includes('hour')) return (parseInt(timeInterval.split(' ')[0]) <= 12) ? 't-green' : 'ftDarkGold';
        return 't-red';
    }

    const toggleLastAction = (columnTitle, memberList) => {
        if (columnTitle.innerText === 'Rank') {
            columnTitle.childNodes[0].nodeValue = 'Last Action';
            for (const li of memberList.children) {
                const lastActionDIV = li.querySelector('.last-action');
                const memberID = lastActionDIV.getAttribute('data-member-ID');
                fetch(`https://api.torn.com/user/${memberID}?selections=profile&key=${APIkey}`)
                .then(response => response.json())
                .then(response => {
                    const lastAction = response.last_action.relative;
                    li.querySelector('.rank .employee-rank-drop-list').classList.toggle('hide');
                    lastActionDIV.innerText = lastAction;
                    lastActionDIV.classList.add(generateTimeColor(lastAction));
                    lastActionDIV.classList.toggle('hide');
                });
            }
        }
        else {
            columnTitle.childNodes[0].nodeValue = 'Rank';
            for (const li of memberList.children) {
                li.querySelector('.rank .employee-rank-drop-list').classList.toggle('hide');
                li.querySelector('.last-action').classList.toggle('hide');
            }
        }
    }

    const employeeCallback = node => {
        const columnTitle = node.querySelector('.employee-list-title .rank');
        const memberList = node.querySelector('.employee-list');
        columnTitle.insertAdjacentHTML('beforeend', `<i class="last_action_icon right"></i>`);
        node.querySelector('.last_action_icon').addEventListener('click', () => toggleLastAction(columnTitle, memberList));
        for (const li of memberList.children) {
            const memberID = li.getAttribute('data-user');
            li.querySelector('.rank .employee-rank-drop-list').insertAdjacentHTML('afterend', `<div class="last-action hide" data-member-id="${memberID}"></div>`);
        }
    }

    const giveMeNumber = numberString => parseInt(numberString.replace(/[^0-9]/g, ''));

    const fillInput = (element, value) => {
        if(value <= 0) return;
        element.value = value;
        element.dispatchEvent(keyUpEvent); // Send keyboard event so that Torn correctly registers input.
    }

    const updateItemStocks = (item, itemName) => {
        itemsStock[itemName] = giveMeNumber(item.innerText);
        localStorage.setItem("TornCustomStock", JSON.stringify(itemsStock));
    }

    const stockCallback = node => {
        let itemList = node.querySelectorAll('.stock-list li:not(.total)'),
            items = new Map();

        for(let item of itemList) {
            let itemName = item.querySelector('.name').innerText;
            items.set(itemName, {
                input: item.querySelector(".quantity input[type=text]"),
                stock: giveMeNumber(item.querySelector('.stock').innerText),
                soldDaily: giveMeNumber(item.querySelector('.sold-daily').innerText)
            });

            let itemStock = item.querySelector('.delivery');
            itemStock.innerHTML = itemsStock[itemName] || 0;
            itemStock.contentEditable = 'true';
            itemStock.addEventListener('blur', () => updateItemStocks(itemStock, itemName));
        }

        // Add "Fill daily" button to stock form
        node.querySelector('span.order.btn-wrap.silver').insertAdjacentHTML('afterend',
                                                                            `<span class="fill-custom btn-wrap silver"><span class="btn disable" role="button">FILL CUSTOM</span></span>
                                                                             <span class="fill-daily btn-wrap silver"><span class="btn disable" role="button">FILL DAILY</span></span>`);
        // Click Handler for fill-custom
        node.querySelector('span.fill-custom').addEventListener('click', () => {
            items.forEach((item, itemName, items) => {
                let remainingStock = (itemsStock[itemName] || 0) - item.stock;
                fillInput(item.input, remainingStock);
            });
        });

        // Click Handler for fill-daily
        node.querySelector('span.fill-daily').addEventListener('click', () => {
            items.forEach((item, itemName, items) => {
                fillInput(item.input, item.soldDaily);
            });
        });

        // Hide delivered orders
        let orders = document.querySelectorAll('#stock .order-list li');
        for(let order of orders) {
            if(order.innerText.includes('Delivered')) {
                order.style.display = 'none';
            } else {
                // And add incoming orders to items map
                let orderName = order.querySelector('.name').innerText;
                let orderAmount = giveMeNumber(order.querySelector('.amount').innerText);
                items.get(orderName).stock += orderAmount;
            }
        }

        node.querySelector('.stock-list-title .delivery').innerHTML = `Total<div class="t-delimiter"></div>`;
    }

    const observe = (elementId, callback) => {
        (new MutationObserver(mutations => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.tagName === 'FORM') {
                        callback(node);
                        return;
                    }
                }
            }
        }))
        .observe(document.getElementById(elementId), {
            childList: true
        });
    }

    checkTrains();
    checkEmployees();
    if(APIkey === null) {
        getAPIkey();
    } else {
        observe('employees', employeeCallback);
    }
    observe('stock', stockCallback);
})();