Company Enhancement Suite

Your one stop solution to managing your company.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Company Enhancement Suite
// @namespace    LordBusiness.CES
// @version      3.4.8
// @description  Your one stop solution to managing your company.
// @author       LordBusiness
// @match        *.torn.com/companies.php
// @run-at       document-end
// @license      MIT
// @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 to stay safe from the jQuerypocalypse. */

  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">
                <a href="/preferences.php#tab=api" target="_blank" title="Fill with your correct API key" class="input-money-symbol">KEY</a><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>
        <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.`;
          // This reload is allowed. IceBlueFire: That's fine (Wed, 12:47 - TCT 05/06/19)
          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 => {
          if(response.error) {
            console.log(response.error.error)
            if(response.error.code == 2) getAPIkey()
          }
          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" title="Toggle Last Action"></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);
})();