jbrey / Factorial – Clock me in

// ==UserScript==
// @name         Factorial – Clock me in
// @description  Automatically fills in the shifts in a calendar month
// @version      1.5
// @copyright    2019, jbrey
// @author       jbrey
// @license      MIT
// @namespace    http://openuserjs.org/users/jbrey
// @require      http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// @require      https://gist.github.com/raw/2625891/waitForKeyElements.js
// @match        *://app.factorialhr.com/attendance/clock-in/*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';
  const workingHoursSeed = 8;
  const workingHoursDeltaSeed = 0;

  const timeForLunchDeltaSeed = 40;
  const timeForLunchMin = 0.75;

  const morningStartFromHour = 8.70;
  const morningStartDeltaSeed = 15;

  const morningEndFromHour = 14;
  const morningEndDeltaSeed = 30;

  function getRandomDelta(delta) {
    const n = Math.floor(Math.random() * delta + 1);
    return Math.ceil(n / 5) * 5;
  }

  function getTimeInMins(time) {
    return Number.parseFloat((time / 60).toFixed(2));
  }

  function getTimeString(hour, min) {
    return ('0' + hour).slice(-2) + ':' + ('0' + min).slice(-2);

  }

  function getMins(time) {
    const mins = time - Math.floor(time);
    return Math.floor(Number.parseFloat((mins * 60).toFixed(0)) / 5) * 5;
  }

  function getRandomTimings() {
    const workingHoursDelta = getRandomDelta(workingHoursDeltaSeed);
    const workingHoursDeltaMins = getTimeInMins(workingHoursDelta);
    const workingHours = workingHoursSeed + workingHoursDeltaMins;

    const timeForLunchDelta = getRandomDelta(timeForLunchDeltaSeed);
    const timeForLunchDeltaMins = getTimeInMins(timeForLunchDelta);
    const timeForLunch = timeForLunchMin + timeForLunchDeltaMins;

    const morningStartDelta = getRandomDelta(morningStartDeltaSeed);
    const morningStartDeltaMins = getTimeInMins(morningStartDelta);
    const morningStart = morningStartFromHour + morningStartDeltaMins;

    const morningEndDelta = getRandomDelta(morningEndDeltaSeed);
    const morningEndDeltaMins = getTimeInMins(morningEndDelta);

    const morningWorkingHours = Number.parseFloat(((morningEndFromHour + morningEndDeltaMins) -
      (morningStartFromHour + morningStartDeltaMins)).toFixed(2));

    const eveStart = morningEndFromHour + morningEndDeltaMins + timeForLunch;
    const eveEnd = eveStart + (workingHours - morningWorkingHours);

    const eveWorkingHours = Number.parseFloat((eveEnd - eveStart).toFixed(2));

    const morningStartStr = getTimeString(Math.floor(morningStart), getMins(morningStart));
    const morningEndStr = getTimeString(morningEndFromHour, morningEndDelta);
    const eveStartStr = getTimeString(Math.floor(eveStart), getMins(eveStart));
    const eveEndStr = getTimeString(Math.floor(eveEnd), getMins(eveEnd));
    return [morningStartStr, morningEndStr, eveStartStr, eveEndStr]
  }

  // Calls the setter for an element in a component oriented framework like react
  // and dispatchs the associated event
  function setReactValue(element, value) {
    const {
      set: valueSetter
    } = Object.getOwnPropertyDescriptor(element, 'value') || {}
    const prototype = Object.getPrototypeOf(element)
    const {
      set: prototypeValueSetter
    } = Object.getOwnPropertyDescriptor(prototype, 'value') || {}

    if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
      prototypeValueSetter.call(element, value)
    }
    else if (valueSetter) {
      valueSetter.call(element, value)
    }
    // dispatch the event to notify react
    element.dispatchEvent(new Event('change', {
      bubbles: true
    }));
  }

  // Iterates through the list of days to add the shifts
  function addShifts() {
    const days = Array.from(document.querySelectorAll('[class^="tr__"]'));

    // filter out bank holidays, vacations and weekends
    const workDays = days.filter(day => {
      return (/tbody/i.test(day.parentNode.tagName) &&
        !/disabled/i.test(day.className) &&
        !/holiday|vacaciones|permisos retribuidos/i.test(day.innerText));
    });

    // add shifts where necessary
    workDays.forEach(workDay => {
      // get button to add a shift
      const addShiftButton = workDay.querySelector('button');
      // check how many shifts already exist
      const inputFields = workDay.querySelectorAll('input');
      // if there are less than 2 shifts, add one
      if ((addShiftButton) && (inputFields.length < 4)) {
        workDay.querySelector('button').click();
      }
    });

    // add values to the shifts
    workDays.forEach(workDay => {
      // get inputs for a day
      const inputFields = workDay.querySelectorAll('input');
      const dailySchedule = getRandomTimings();

      // fill in shifts
      let index = 0;
      inputFields.forEach(inputField => {
        // enabled?
        if ((!inputField.disabled) && (!inputField.value)) {
          setReactValue(inputField, dailySchedule[index]);
          // TODO: find out why the value must be set twice for react to pick it up... !!!
          setReactValue(inputField, dailySchedule[index]);
        }
        // find the shift
        index = (index >= 3) ? 0 : index + 1;
      });
    });

    workDays.forEach(workDay => {
      // get all buttons for a workday
      const buttons = workDay.querySelectorAll('button');
      // cycle through them
      buttons.forEach(button => {
        // is this the submit button and is it enabled?
        if ((button.innerText) &&
          (/submit|guardar/i.test(button.innerText)) &&
          (!button.disabled)) {
          button.click();
        }
      });
    });
  }

  // Injects into the page a button that calls the logic of this script
  function setButton(button) {
    if ((button[0].innerText) &&
      (/clock in|entrada/i.test(button[0].innerText))) {
      const container = button[0].parentNode.parentNode;
      const newContainer = button[0].parentNode.cloneNode();
      const newButton = button[0].cloneNode();
      newButton.insertAdjacentHTML(
        'afterbegin',
        `<div class="_2U_uz_2-jO"><div class="box___nBFPS width_full___GXPW7 height_s32___1gzeu padding_x_s8___1Pgn7 padding_y_s4___1nJEZ border_radius_abs016___27iFP background_primary1000___YyQFE border_color_lighter000___JTTqK border_width_s2___wC6c4 border_style_solid___1kBV3"><div class="box___nBFPS padding_x_s2___BwA3m flex_direction_row___1rGE2 align_items_center___MjjdX"><div class="box___nBFPS padding_x_s4___2sUGX overflow_x_hidden___vhjK1"><span class="text___2TOkD size_200___2HuYx weight_semibold___2R9kP color_grey000___2seTn"><span class="_29A1e">Clock me in!</span>`
      );
      newButton.setAttribute('id', 'clockmein');
      newButton.addEventListener("click", addShifts, false);

      newContainer.appendChild(newButton);
      container.appendChild(newContainer);
    }
  }

  waitForKeyElements("button:contains('Entrada'), button:contains('Clock in')", setButton, false);
})();