Teambition

Beautify the Teambition.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name               Teambition
// @name:zh-CN         Teambition
// @description        Beautify the Teambition.
// @description:zh-CN  美化Teambition。
// @namespace          https://github.com/HaleShaw
// @version            1.0.3
// @author             HaleShaw
// @copyright          2022+, HaleShaw (https://github.com/HaleShaw)
// @license            AGPL-3.0-or-later
// @homepage           https://github.com/HaleShaw/TM-Teambition
// @supportURL         https://github.com/HaleShaw/TM-Teambition/issues
// @contributionURL    https://www.jianwudao.com/
// @icon               data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAA21BMVEUAAAAyr/8bofcSm/dOuv4OnPUWnvZKufwRm/Ugo/cLmPRFtvtVvv0wq/krqfgGlfRPvP1As/sboPcLmPVVvv01rvolpvcFlfRKuv0Rm/ZTvv46sfsGlvROuv1EtfsMl/RUvv4/tf0xrPo7sfs2rvtLuv1AtPwQm/b///9Gt/3j9P9RvP4mpvkdovcaoPf6/f8LmPXD5/1QvP10x/xbvftWu/twxPpHtPksqfkrqfnm9f7T7f664/2S0/16y/2r3fxlwPrp9v7d8f6g2v6z4PyZ1vyCzPw9sPpQtvl1UhomAAAAInRSTlMACJQ/Ghr+/Pz+/vz39/f37Ozs7NfX19eurpWVlZKSkj8/xzBnxQAAAMZJREFUGNMtz0cWgkAQBNAeQMCcc86gAwoCEgWz9z+RPfP4iw61K0BEnPTr9f5EJMAV5u3SGZXa8wL/h+o+pw4xIYIsm6ZhmDhkVSAgtgzX8zLnd00sx2iJIBRpfH9EVuqF2osWBehRP9ZQmKXRhdIe1ALf/bwt7XBdff0gqEF1xVxOGDBV6G4YDBR+dGG8Zm6ng82PMSybO4SBy3ZzCWRW1nXdSZ5HXOUZ1pEGyjanDCRA0rRhH5HdmErAkcWoU6l0RgtW/w98aR8lvTImwwAAAABJRU5ErkJggg==
// @match              https://www.teambition.com/project/*
// @match              https://www.teambition.com/organization/*
// @match              https://apps.teambition.com/work-time-client/*
// @compatible	       Chrome
// @grant              GM_addStyle
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_registerMenuCommand
// ==/UserScript==

// ==OpenUserJS==
// @author             HaleShaw
// @collaborator       HaleShaw
// ==/OpenUserJS==

(function () {
  'use strict';

  let data;

  const mainStyle = `
  /* 任务面板 */

  /* 看板模式下,将任务卡片的任务ID左移,鼠标悬浮时使“…”按钮不覆盖任务ID */
  .task-card .task-content-set .task-card-footer>* {
      margin-right: 25px !important;
  }

  /* 隐藏工作流顶部右侧的“更多”按钮 */
  div.kanban-single-lane-body-wrapper>div.kanban-single-list>div.kanban-single-list-contents>div>div>div:last-child {
    display: none !important;
  }

  /* -------------------------------------------------------------------------- */
  /* 工作流配置页面 */

  /* 将整个页面周围边距清除 */
  .taskflow-config-contents {
      bottom: 0px !important;
      left: 0px !important;
      right: 0px !important;
      top: 60px !important;
  }

  /* 减小行高 */
  .taskflow-config-table-block-item {
      height: 50px !important;
  }

  /* 减小列宽 */
  .taskflow-config-table-status-column {
      width: 110px !important;
  }

  .status-label-with-menu {
      padding: 0 !important;
      width: 110px !important;
  }
  /* -------------------------------------------------------------------------- */
  /* 迭代面板 */
  .sprint-panel-component-title-wrapper {
      padding: 4px 16px !important;
  }
  .sprint-panel-component-sprint-content>.sprint-name {
      padding: 3px 0 !important;
  }
  .sprint-panel-item .sprint-control-option {
      height: 28px !important;
  }

  .sprint-panel-item .sprint-subtitle {
      margin-bottom: 0px !important;
  }
  `;

  const loadAllStyle = `
  .loadAll {
    background-color: #f2fbff!important;
    border-color: transparent!important;
    color: #1b9aee!important;
    font-size: 14px;
    height: 28px;
    line-height: 26px;
    min-width: 52px;
    padding: 0 7px;
    margin-right: 15px;
    border-radius: 4px;
    border-width: 1px;
    box-shadow: none;
    transition: background-color .3s ease,color .3s ease,border-color .3s ease;
  }

  .loadAll:hover{
    color: #ffffff !important;
    background-color: #1b9aee !important;
    border-color: #1b9aee !important;
  }
  `;

  const worktimeStyle = `
    /* 头部标题 */
    #root header h4 {
        display: none !important;
    }
    /* 头部日期选择器 */
    ._18ivZlWaPJH2YVLnpZrb0N>header .LZNYW1Yb6acagP8pDYBcB .vkkNP00hyMhbgKHQS-CCT {
        margin-right: 8px !important;
    }
    ._18ivZlWaPJH2YVLnpZrb0N>header .LZNYW1Yb6acagP8pDYBcB ._2gIxRD0Aw3Wz4V58lFor-k {
        margin-left: 8px !important;
    }

    #root > div > div > header > div:first-child > h4 {
      padding: 0 5px !important;
    }

    #root > div > div > header > div:last-child {
      flex-grow: 0 !important;
      flex-basis: 240px !important;
    }

    /* 表头第一列 */
    ._1a3bcSp1CtO4ejX9ugVzzO._2Q3J8umLBh0SslZCOuVtuw {
      width: 180px !important;
    }

    /* 表体第一列 */
    ._1a3bcSp1CtO4ejX9ugVzzO {
      padding: 0 0px 0 28px !important;
    }

    .rt-table > .rt-tbody > .rt-tr-group > .rt-tr > .rt-td:first-child,
    .rt-table > .rt-thead > .rt-tr > .rt-th:first-child,
    .rt-table > .rt-tfoot > .rt-tr > .rt-td:first-child {
      flex: 180 0 auto !important;
      width: 180px !important;
      max-width: 180px !important;
    }

    .rt-table > .rt-tbody > .rt-tr-group > .rt-tr > .rt-td:not(:first-child),
    .rt-table > .rt-thead > .rt-tr > .rt-th:not(:first-child),
    .rt-table > .rt-tfoot > .rt-tr > .rt-td:not(:first-child) {
      flex: 124 0 auto !important;
      width: 124px !important;
      max-width: 124px !important;
    }

    /* 隐藏悬浮边框 */
    ._1SmlEXYUElETMoqOAF1ym0:hover {
      border: none !important;
    }
    ._3AzO2C7wToS7pqSbOxyEXm {
      border: none !important;
    }

    /* 直接展示实际和计划工时 */
    ._3sRT72VQQvRBhWYMmH0hD2 {
      display: none !important;
    }
    .JqBbgp5bIj-_fWcuSdn8X {
      display: block !important;
    }

    /* footer总计工时 */
    .Z8fZCPB9x3aQBvMMYEzWI {
      padding: 0px 0px !important;
    }

    button.cusBtn {
      outline: none;
      padding: 2px 10px;
      border: none;
      border-radius: 4px;
      color: #595959;
      line-height: 26px;
      background-color: #e5e5e5;
      font-size: 14px;
      cursor: pointer;
      margin-left: 5px;
    }
    button.cusBtn:hover {
      color: #1b9aee;
    }

    button.cusBtn.selected {
      background-color: #888888;
      color: #ffffff;
    }

    select.cusSelect {
      font-size: 14px;
      border: 1px solid #e5e5e5;
      border-radius: 4px;
      color: #262626;
      transition: border 218ms;
      padding: 0px 8px;
      margin-left: 10px;
      height: 30px;
    }

    /* 计划时间过大 */
    span.time.plan.greater {
      color: #ff2222;
    }

    /* 计划时间过小 */
    span.time.plan.less {
      color: #f29900;
    }
    `;

  const settingHTML = `
<div id="settingContent">
  <div id="settingTitle">
    <span class="icon"></span><span class="title">Teambition - 工时应用设置</span>
  </div>
  <ol id="settingBody">
    <li>
      <span class="setting title">工时应用URL</span>
      <div class="setting comment">
        <a>a.在左侧边栏上,点击“更多”按钮 -> “全部应用” -> “工时”</a><span class="errMsg url"></span>
        <a>b.页面加载完成后,按F12进入开发者模式,找到第一个iframe的地址,即是工时应用的地址</a>
        <a class="code">document.querySelectorAll('iframe')[0].src</a>
        <button type="button" id="getUrl">一键获取</button>
      </div>
      <input type="text" class="setting text url" placeholder="https://appshell.teambition.com/api/v1/organization/..."></input>
    </li>
    <li>
      <span class="setting title">分组过滤按钮</span>
      <div class="setting comment">
        <a>配置每个按钮过滤部分人员。请使用标准JSON格式,使用英文双引号。</a><span class="errMsg filter"></span>
      </div>
      <textarea
        type="text"
        class="setting text filter"
        placeholder="{
    'button name':['username','username'],
    'button name':['username','username']
}"></textarea>
    </li>
    <li>
      <span class="setting title">移除人员</span>
      <div class="setting comment">
        <a>可将不必要的人员从表格中移除。请使用标准JSON格式,使用英文双引号。</a><span class="errMsg remove"></span>
      </div>
      <input type="text" class="setting text remove" placeholder="['username','username']"></input>
    </li>
  </ol>
  <div id="settingFooter">
    <button type="button" id="settingCancel" class="setting button">取消</button>
    <button type="button" id="settingSave" class="setting button">保存</button>
  </div>
</div>
`;

  const settingStyle = `
#settingPanel {
  display: none;
  justify-content: center;
  align-items: center;
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 200000000;
  overflow: auto;
  font-family: arial, sans-serif;
  min-height: 100%;
  font-size: 16px;
  transition: 0.5s;
  opacity: 1;
  user-select: none;
  -moz-user-select: none;
  padding-bottom: 80px;
  box-sizing: border-box;
}

#settingContent {
  display: flex;
  flex-wrap: wrap;
  padding: 20px;
  background-color: #f7f7f7;
  border-radius: 4px;
  position: absolute;
  width: 50%;
  transition: 0.5s;
}

#settingTitle {
  width: 100%;
  margin: 15px 0;
}

#settingTitle > span.icon {
  width: 20px;
  height: 20px;
  display: inline-block;
  background-image: url("https://tcs-ga.teambition.net/thumbnail/312ca08e98b657c56f74c8f5eae0b66dd8d2/w/20/h/20");
  background-position: center center;
  background-repeat: no-repeat;
  background-size: 100%;
  border-radius: 4px;
}

#settingTitle > span.title {
  font-size: 18px;
  vertical-align: top;
  font-weight: 500;
}

#settingBody {
  padding: 20px 20px 20px 40px;
  background-color: #fff;
  border: 1px solid #fff;
  border-radius: 5px;
  width: 100%;
  list-style: decimal;
}

span.setting.title {
  font-size: 1.125rem;
  font-weight: 500;
  line-height: 2rem;
  display: block;
}

span.setting.title:not(:first-child) {
  margin-top: 8px;
}

.setting.comment > a {
  color: #666;
  display: inline-block;
  line-height: 24px;
  text-decoration: none;
  user-select: text;
}

.setting.comment > a.code {
  background: #fafafa;
  line-height: 150%;
  padding-right: 10px;
  padding-left: 10px;
  border-left: 2px solid #6ce26c;
  display: block !important;
}

.setting.text {
  width: 100%;
  margin-top: 5px;
  line-height: 24px;
}

.setting.text.url {
  width: calc(100% - 90px) !important;
}

textarea.setting.text {
  min-height: 126px;
  max-height: 200px;
}

#getUrl {
  position: absolute;
  right: 40px;
  padding: 4px 8px;
  margin-top: 5px;
  cursor: pointer;
  outline-style: none;
  border-radius: 3px;
  color: #fff;
  border: 1px solid #1b9aee;
  background-color: #1b9aee;
}

#settingFooter {
  text-align: center;
  margin: auto;
  margin-top: 15px;
  border-radius: 4px;
}

.setting.button {
  padding: 5px 10px;
  cursor: pointer;
  outline-style: none;
  border-radius: 3px;
}

#settingCancel {
  border: 1px solid #e7e7e7;
  background-color: #e7e7e7;
}

#settingCancel:hover {
  border: 1px solid #979797;
  background-color: #979797;
}

#settingSave {
  margin-left: 20px;
  color: #fff;
  border: 1px solid #1b9aee;
  background-color: #1b9aee;
}

#settingSave:hover,
#getUrl:hover {
  border: 1px solid #0171c2;
  background-color: #0171c2;
}

span.errMsg {
  color: red;
  margin-left: 10px;
}
  `;

  let members = [];

  const planTimeMaximum = 10;
  const planTimeMinimum = 7;

  // The number of times the worktime page was loaded.
  const loadCount = 5;

  main();

  function main() {
    GM_addStyle(mainStyle);
    logInfo(GM_info.script.name, GM_info.script.version);
    data = GM_getValue('worktime');
    GM_registerMenuCommand("设置", () => {
      new Setting();
    });

    if (window.location.href.startsWith('https://apps.teambition.com/work-time-client')) {
      beautifyWorktime();
    } else {
      setTimeout(() => {
        hideAddingStatus();
        listenDom();
        loadAll();
        addSideButton();
      }, 4000);
    }
  }

  function beautifyWorktime() {
    GM_addStyle(worktimeStyle);

    let timeout = 0;
    let table;
    let interval = setInterval(() => {
      timeout += 1;
      table = document.querySelector('.ReactTable > .rt-table');

      // Stop the circulator when the element is found or after 10 seconds(100*100 ms).
      if (timeout == 100 || table) {
        clearInterval(interval);
        loadAllWorktime();
        choosePlanTime();
        addFilterButton();
      }
    }, 100);
  }

  /**
   * Listen the parent DOM. While the children is changing, and the top board is TaskBoard then hide the panel.
   */
  function listenDom() {
    let parentObj = document.querySelector('div#teambition-web-content>div.project-app-view>div.project-app-inner');
    if (!parentObj) {
      return;
    }
    let innerListener = new MutationObserver(() => {
      setTimeout(() => { hideAddingStatus() }, 4000);
    });

    let listener = new MutationObserver((mutationRecords) => {
      top:
      for (let i = 0; i < mutationRecords.length; i++) {
        let addedNodes = mutationRecords[i].addedNodes;
        let removedNodes = mutationRecords[i].removedNodes;
        for (let j = 0; j < addedNodes.length; j++) {
          if (addedNodes[j].className == 'board-view') {
            // When you enter the task panel, start the task listener.
            setTimeout(() => {
              let innerTaskBoard = document.querySelector('div.board-view > div.board-flex-view > div.board-right-view');
              if (!innerTaskBoard) {
                return;
              }
              hideAddingStatus();
              innerListener.observe(innerTaskBoard, {
                childList: true
              });
            }, 4000);
            break top;
          }
        }
        for (let j = 0; j < removedNodes.length; j++) {
          if (removedNodes[j].className == 'board-view') {
            // When you leave the task panel, stop the task listener.
            innerListener.disconnect();
            break top;
          }
        }
      }
    });
    listener.observe(parentObj, {
      childList: true
    });

    listenTaskBoard();
  }

  /**
   * While the content of the task board changing then hide the adding button.
   */
  function listenTaskBoard() {
    let taskListener = new MutationObserver(() => {
      setTimeout(() => { hideAddingStatus() }, 3000);
    });

    let taskBoard = document.querySelector('div.board-view > div.board-flex-view > div.board-right-view');
    if (taskBoard) {
      hideAddingStatus();
      taskListener.observe(taskBoard, {
        childList: true
      });
    }
  }

  function hideAddingStatus() {
    let status = document.querySelector('div.kanban-single-lane-body-wrapper>div.kanban-single-list:last-child');
    if (!status) {
      return;
    }
    let statusText = status.querySelector('div>div>button>span:last-child');
    if (!statusText) {
      return;
    }
    if ('添加状态' == statusText.textContent) {
      status.style.display = 'none';
    }
  }

  /**
   * Add the button of loading all the content.
   */
  function loadAll() {
    GM_addStyle(loadAllStyle);
    let parent = $('nav.project-navigation.sub-nav.sub-navigator > div.row-flex.nav-container > div.nav-footer');
    if (!parent || parent.length == 0) {
      return;
    }
    let loadAllButton = $('<button type="button" class="loadAll" title="表格视图下,加载所有数据">LoadAll</button>');
    loadAllButton.click(function (e) {
      let test = getLoadButton();
      load(test);
    });
    loadAllButton.insertBefore(parent.children().eq(0));
  }

  /**
   * Load content.
   * @param {Object} element The button of loading.
   */
  function load(element) {
    if (element) {
      element.click();
      setTimeout(() => {
        let test = getLoadButton();
        load(test);
        // load(getLoadButton());
      }, 1000);
    }
  }

  /**
   * Get the button of loading.
   * @returns The button of loading.
   */
  function getLoadButton() {
    let button = null;
    $('div').each((index, element) => {
      if (element.textContent.startsWith('加载更多')) {
        button = element;
        return;
      }
    })
    return button;
  }

  /**
   * Add the side button which used for opening the worktime page.
   */
  function addSideButton() {
    let data = GM_getValue('worktime');
    if (!data || !data?.url || data.url.trim() == '') {
      return;
    }

    let parent = $('#nav_bar-apps > div.nav_bar_detail-apps > ul').last();
    let button = $(`
      <li class="nav_bar_detail-app">
        <span class="next-badge nav_bar_detail-badge">
          <span class="nav_bar_detail-app-icon" style="display: inline-block">
            <span
              style="
                width: 100%;
                height: 100%;
                display: inline-block;
                background-image: url('https://tcs-ga.teambition.net/thumbnail/312ca08e98b657c56f74c8f5eae0b66dd8d2/w/20/h/20');
                background-position: center center;
                background-repeat: no-repeat;
                background-size: 100%;
                border-radius: 4px;
              "
            ></span>
          </span>
          <span></span>
        </span>
        <span class="nav_bar_detail-app-text">工时</span>
      </li>
    `);
    button.click(() => {
      window.open(data?.url, "_blank");
    });
    if (parent.length < 1) {
      return;
    }
    parent.append(button);
  }

  /**
   * Load all of the worktime list.
   */
  function loadAllWorktime() {
    let table = document.querySelector('.ReactTable > .rt-table');
    for (var i = 0; i < loadCount; i++) {
      (function (t) {
        setTimeout(function () {
          table.scroll(0, 10000);
        }, 1000 * t);
      })(i)
    }
    setTimeout(() => {
      table.scroll(0, 0);
    }, loadCount * 1000);

    setTimeout(() => {
      simplifyHeader();
      removeWorktime();
      cleanWord();
      AppendMemberNumber();
      addMemberSelect();
      markAbnormalTime();
    }, (loadCount + 1) * 1000);
  }

  /**
   * Simplify header.
   */
  function simplifyHeader() {
    let spanList = document.querySelectorAll('._38MgU8RZnSct2M6Qs3ZR2i');
    for (let i = 0; i < spanList.length; i++) {
      const text = spanList[i].textContent;
      spanList[i].textContent = text.replace('工时', "");
    }
  }

  /**
   * Add a button for clean the unuseful word.
   */
  function cleanWord() {
    let spans = document.querySelectorAll('._3RNDj8L2GanC2QT53PysSS > span');
    for (let i = 0; i < spans.length; i++) {
      spans[i].innerHTML = spans[i].innerHTML.replace('合计 :&nbsp;&nbsp;', '');
    }
    let total = document.querySelector('.rt-tfoot > .rt-tr > .rt-td > div > div');
    if (total) {
      total.innerHTML = total.innerHTML.replace('总计 :&nbsp;&nbsp;', '');
    }

    let values = document.querySelectorAll('.rt-td span.JqBbgp5bIj-_fWcuSdn8X');
    for (let i = 0; i < values.length; i++) {
      const element = values[i];
      let text = values[i].innerText;
      if (text.endsWith(' 小时')) {
        values[i].innerText = text.replace(' 小时', '');
      }
    }

    document.querySelectorAll('span._3sRT72VQQvRBhWYMmH0hD2').forEach((item) => {
      item.remove();
    });
  }

  /**
   * Append the number of members at the 'All' button.
   */
  function AppendMemberNumber() {
    let button = document.querySelector('.team.cusBtn.all');
    let trList = document.querySelectorAll('.rt-table > .rt-tbody > .rt-tr-group');
    if (!button || !trList) {
      return;
    }
    button.textContent += `(${trList.length})`;

    // Collect member list.
    for (let i = 0; i < trList.length; i++) {
      const nameElement = trList[i].querySelector('.rt-tr > .rt-td:first-child > div > div > div');
      const username = nameElement.textContent;
      members.push(username);
    }
    members.sort((a, b) => { return a.localeCompare(b) });
  }

  /**
   * The Plan Time is selected by default.
   */
  function choosePlanTime() {
    let times = document.querySelectorAll('#root header h4 + div > div');
    if (times.length == 2) {
      times[1].click();
    }
  }

  /**
   * Add the buttons for filtering worktime list.
   */
  function addFilterButton() {
    let parent = document.querySelector('#root header > div');
    if (!parent) {
      return;
    }
    let teamMember = data?.filter;
    if (!teamMember) {
      return;
    }
    const teams = Object.keys(teamMember);
    for (let i = 0; i < teams.length; i++) {
      let button = document.createElement('button');
      button.className = 'team cusBtn';
      button.textContent = `${teams[i]}(${teamMember[teams[i]].length})`;
      button.title = teamMember[teams[i]].join(',');
      button.onclick = function () {
        changeButtonStyle(button.textContent);
        let trList = document.querySelectorAll('.rt-table > .rt-tbody > .rt-tr-group');
        for (let j = 0; j < trList.length; j++) {
          const nameElement = trList[j].querySelector('.rt-tr > .rt-td:first-child > div > div > div');
          const username = nameElement.textContent;
          if (teamMember[teams[i]].indexOf(username) != -1) {
            trList[j].style.display = 'flex';
          } else {
            trList[j].style.display = 'none';
          }
        }
        changeSelectStatus();
      };
      parent.append(button);
    }
    parent.append(createAllButton());
  }

  /**
   * Create the button for displaying all the worktime list.
   */
  function createAllButton() {
    let button = document.createElement('button');
    button.className = 'team cusBtn all';
    button.textContent = 'All';
    button.onclick = function () {
      changeButtonStyle(button.textContent);
      let trList = document.querySelectorAll('.rt-table > .rt-tbody > .rt-tr-group');
      for (let j = 0; j < trList.length; j++) {
        trList[j].style.display = 'flex';
      }
      changeSelectStatus();
    };
    return button;
  }

  /**
   * Change the style of the button.
   * @param {String} button The name of the button.
   */
  function changeButtonStyle(button) {
    let buttons = document.querySelectorAll('button.team.cusBtn');
    for (let i = 0; i < buttons.length; i++) {
      if (buttons[i].textContent == button) {
        addClass(buttons[i], 'selected');
      } else {
        removeClass(buttons[i], 'selected');
      }
    }
  }

  /**
   * Set the first element be selected.
   */
  function changeSelectStatus() {
    let select = document.querySelector('select.cusSelect');
    if (select) {
      select.selectedIndex = 0;
    }
  }

  /**
   * Remove the worktime item in the table.
   */
  function removeWorktime() {
    let removeMembers = data?.remove;
    if (!removeMembers) {
      return;
    }
    let trList = document.querySelectorAll('.rt-table > .rt-tbody > .rt-tr-group');
    for (let i = 0; i < trList.length; i++) {
      const nameElement = trList[i].querySelector('.rt-tr > .rt-td:first-child > div > div > div');
      const username = nameElement.textContent;
      if (removeMembers.indexOf(username) != -1) {
        trList[i].remove();
      }
    }
  }

  /**
   * Add the select element for searching member.
   */
  function addMemberSelect() {
    let parent = document.querySelector('#root header > div');
    if (!parent || members.length == 0) {
      return;
    }
    let select = document.createElement('select');
    select.className = 'cusSelect';

    let hiddenOption = document.createElement('option');
    hiddenOption.style.display = 'none';
    select.append(hiddenOption);

    for (let i = 0; i < members.length; i++) {
      let option = document.createElement('option');
      option.value = members[i];
      option.textContent = members[i];
      select.append(option);
    }
    select.onchange = function () {
      let selectedMember = select.options[select.selectedIndex].value;
      let trList = document.querySelectorAll('.rt-table > .rt-tbody > .rt-tr-group');
      for (let j = 0; j < trList.length; j++) {
        const nameElement = trList[j].querySelector('.rt-tr > .rt-td:first-child > div > div > div');
        const username = nameElement.textContent;
        if (username == selectedMember) {
          trList[j].style.display = 'flex';
        } else {
          trList[j].style.display = 'none';
        }
      }

      // Remove the selected style for all the fileter button.
      let buttons = document.querySelectorAll('button.team.cusBtn');
      for (let i = 0; i < buttons.length; i++) {
        removeClass(buttons[i], 'selected');
      }
    }
    parent.append(select);
  }

  /**
   * Color the abnormal times.
   */
  function markAbnormalTime() {
    let spanList = document.querySelectorAll('.rt-tbody .rt-tr .rt-td:not(:first-child) span.JqBbgp5bIj-_fWcuSdn8X');
    for (let i = 0; i < spanList.length; i++) {
      let timeArr = spanList[i].textContent.split('/');
      const actualTime = new Number(timeArr[0].trim()).toFixed(0);
      const planTime = new Number(timeArr[1].trim()).toFixed(0);
      let innerHTML;
      if (planTime >= planTimeMaximum) {
        innerHTML = `<span>${actualTime}</span><span>&nbsp;/&nbsp;</span><span class="time plan greater">${planTime}</span>`;
      }
      else if (planTime <= planTimeMinimum) {
        innerHTML = `<span>${actualTime}</span><span>&nbsp;/&nbsp;</span><span class="time plan less">${planTime}</span>`;
      }
      else {
        innerHTML = `<span>${actualTime}</span><span>&nbsp;/&nbsp;</span><span class="time plan">${planTime}</span>`;
      }
      spanList[i].innerHTML = innerHTML;
    }
  }
  class Setting {
    constructor() {
      this.init()
    }
    init() {
      let self = this;
      setTimeout(() => {
        if (!document.getElementById('settingPanel')) {
          self.addElement();
          self.addListener();
          GM_addStyle(settingStyle);
        }
        self.loadConfiguration();
        self.show();
      }, 300);
    }

    addElement() {
      let settingPanel = document.createElement('div');
      settingPanel.setAttribute('id', 'settingPanel');
      settingPanel.innerHTML = settingHTML;
      document.body.append(settingPanel);
    }

    addListener() {
      let self = this;
      let panel = document.getElementById('settingPanel');

      document.getElementById('getUrl').onclick = function () {
        let iframes = document.querySelectorAll('iframe');
        if (iframes.length > 0) {
          const url = iframes[0].src;
          if (url.startsWith('https://appshell.teambition.com/api/v1/organization/')) {
            document.querySelector('input.setting.text.url').value = url;
            document.querySelector('span.errMsg.url').textContent = '';
          } else {
            document.querySelector('span.errMsg.url').textContent = '未获取到,请先打开工时应用。';
          }
        } else {
          document.querySelector('span.errMsg.url').textContent = '未检测到iframe,请先打开工时应用。';
        }
      }

      document.getElementById('settingCancel').onclick = function () {
        panel.style.display = 'none';
      }

      document.getElementById('settingSave').onclick = function () {
        if (self.saveConfiguration()) {
          panel.style.display = 'none';
          location.reload();
        }
      }

      document.onkeyup = function (e) {
        var theEvent = e || window.event;
        var code = theEvent.keyCode || theEvent.which || theEvent.charCode;
        // Escape
        if (code == 27 && panel) {
          panel.style.display = 'none';
        }
      };
    }

    loadConfiguration() {
      let data = GM_getValue('worktime');
      if (!data) {
        return;
      }
      const urlValue = data?.url;
      if (urlValue && urlValue.trim() != '') {
        document.querySelector('.setting.text.url').value = urlValue;
      }
      const filterValue = data?.filter;
      if (filterValue) {
        document.querySelector('.setting.text.filter').value = JSON.stringify(filterValue);
      }
      const removeValue = data?.remove;
      if (removeValue) {
        document.querySelector('.setting.text.remove').value = JSON.stringify(removeValue);
      }
      document.querySelector('span.errMsg.url').textContent = '';
      document.querySelector('span.errMsg.filter').textContent = '';
      document.querySelector('span.errMsg.remove').textContent = '';
    }

    saveConfiguration() {
      let urlEle = document.querySelector('.setting.text.url');
      let filterEle = document.querySelector('.setting.text.filter');
      let filterValue;
      try {
        filterValue = JSON.parse(filterEle.value);
      } catch (error) {
        console.error(error);
        document.querySelector('span.errMsg.filter').textContent = 'JSON格式不正确,请修改后重试!';
        return false;
      }
      document.querySelector('span.errMsg.filter').textContent = '';

      let removeEle = document.querySelector('.setting.text.remove');
      let removeValue;
      try {
        removeValue = JSON.parse(removeEle.value);
      } catch (error) {
        console.error(error);
        document.querySelector('span.errMsg.remove').textContent = '数组格式不正确,请修改后重试!';
        return false;
      }
      document.querySelector('span.errMsg.remove').textContent = '';
      const data = {
        'url': urlEle.value,
        'filter': filterValue,
        'remove': removeValue
      }
      GM_setValue('worktime', data);
      return true;
    }

    show() {
      document.getElementById('settingPanel').style.display = 'flex';
    }
  }

  /**
   * Add class name for the element.
   * @param {Object} element The DOM elment object.
   * @param {String} value the class name of the element.
   */
  function addClass(element, value) {
    if (!element.className) {
      element.className = value;
    } else if (element.className.indexOf(value) == -1) {
      let newClassName = element.className;
      newClassName += " ";
      newClassName += value;
      element.className = newClassName;
    }
  }

  /**
   * Remove class name for the element.
   * @param {Object} element The DOM elment object.
   * @param {String} value the class name of the element.
   */
  function removeClass(element, value) {
    if (element.className) {
      let newClassName = element.className;
      newClassName = newClassName.replace(value, '');
      newClassName = newClassName.trim();
      element.className = newClassName;
    }
  }

  /**
   * Log the title and version at the front of the console.
   * @param {String} title title.
   * @param {String} version script version.
   */
  function logInfo(title, version) {
    console.clear();
    const titleStyle = 'color:white;background-color:#606060';
    const versionStyle = 'color:white;background-color:#1475b2';
    const logTitle = ' ' + title + ' ';
    const logVersion = ' ' + version + ' ';
    console.log('%c' + logTitle + '%c' + logVersion, titleStyle, versionStyle);
  }
})();