Company Enhancement Suite

Your one stop solution to managing your company.

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

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Company Enhancement Suite
// @namespace    LordBusiness.CES
// @version      3.4.1
// @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 [1976582]'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>`);
        }
    }

    /* So why did I decide to rewrite this old, obselete script?
       Good question. When I saw Mathiaas [1918010]'s Stock Order script, I thought to myself,
       "Hey, I've made a script just like this! Wonder what happened to that."
       And thus, I started working on this one. You can start crying now */

    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);
})();