Toffu

Autofills Woffu schedule

// ==UserScript==
// @name         Toffu
// @namespace    https://greasyfork.org/en/scripts/419870-toffu
// @version      0.9
// @description  Autofills Woffu schedule
// @author       DonNadie
// @match        https://*.woffu.com/*
// @grant        none
// @license MIT
// ==/UserScript==

(async function() {
    'use strict';

    let userToken;
    let departmentId;
    let scheduleId;
    let calendarId;
    let userId;

    const api = async (route, params, method) =>
    {
        const BASE_URL = "https://" + location.hostname + '/api/';

        if (typeof params === "undefined" || params == null) {
            params = {};
        }
        if (typeof method === "undefined" || method == null) {
            method = "get";
        }

        let request = {
            method: method,
        };

        let paramList;

        // json body or already a FormData
        if (params instanceof FormData || params instanceof URLSearchParams) {
            paramList = params;
        } else if (typeof params === "string") {
            request.headers = {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            };
            paramList = params;
        } else {
            if (method === "post") {
                paramList = new FormData();
            } else {
                paramList = new URLSearchParams();
            }

            for (let k in params) {
                if (!params.hasOwnProperty(k)) {continue;}
                paramList.append(k, params[k]);
            }
        }

        if (method === "post" || method === "put") {
            request.body = paramList;
        } else {
            route = route + "?" + paramList;
        }

        if (typeof request.headers === "undefined") {
            request.headers = {};
        }
        if (userToken) {
            request.headers['Authorization'] = "Bearer " + userToken;
        }

        return new Promise((resolve, reject) => {
            fetch(BASE_URL + route, request)
              .then(response => response.json().then(resolve))
            .catch(error => reject(error));
        });
    };

    const getUserToken = async () => {
        const response = await api('users/token');

        if (response) {
            return response.Token;
        }
        return null;
    };

    const parseUserToken = (token) => {
        token = token.split(".")[1];
        return JSON.parse(atob(token));
    };

    const getPresence = async (userId, start, end) => {
        return (await api('users/' + userId + '/diaries/presence', {
            fromDate: start,
            toDate: end,
            pageIndex: 0,
            pageSize: 31,
        }, "get")).Diaries;
    };

    const getDateRange = () => {
        let response = {};
        const inputs = document.querySelectorAll('.react-datepicker__input-container input');
        const format = understandDateFormat();
        [
            {
                val: inputs[0].value,
                field: "start",
            },
            {
                val: inputs[1].value,
                field: "end",
            }
        ].forEach(date => {
            const parts = date.val.split(format.separator);
            let parsedDate = {};
            parts.forEach((part, i) => {
                parsedDate[format.map[i]] = part;
            });

            response[date.field] = parsedDate.year + "-" + parsedDate.month + "-" + parsedDate.day;
        });

        return response;
    };

    const understandDateFormat = () => {
        const separator = new Date().toLocaleDateString().replaceAll(/\d/g, '').substr(0, 1);
        let map;

        if (getLang() === 'en') {
            map = {
                0: "month",
                1: "day",
                2: "year"
            };
        } else {
            map = {
                0: "day",
                1: "month",
                2: "year"
            };
        }

        return {
            separator,
            map
        };
    }

    const getLang = () => {
        // can't use document.lang as those retards take ages to properly set it
        return getAllH2Contents().includes('Mi Presencia') ? 'es' : 'en';
    }

    const getDayTemplate = (day) => {
        const date = day.Date.split('T')[0];

        const li = document.createElement('li');
        li.dataset.removable = true;
        li.style.marginLeft = "5px";
        li.innerHTML = `
            <div class="form-check">
                <input class="form-check-input" type="checkbox" value="` + day.DiaryId + `" data-date="` + date + `" id="autodate-` + date + `" checked>
                <label class="form-check-label" for="autodate-` + date + `" style="display: inline-block;">
                    ` + date + `<span class="text-danger"> ` + day.DiffFormatted.Values[0] + `h</span>
                </label>
            </div>
        `;

        return li;
    }

    const calculateSlotTime = (start, end) => {
        const startDate = new Date();
        startDate.setHours(parseInt(start));
        startDate.setMinutes(parseInt(start.split(':')[1]));

        const endDate = new Date();
        endDate.setHours(parseInt(end));
        endDate.setMinutes(parseInt(end.split(':')[1]));

        return parseInt(Math.abs(endDate - startDate) / 36e5);
    }

    const createSlot = (start, end, order) => {
        return {
            "Motive":null,
            "In":{
                "Time": start,
                "new":true,
                "SignStatus":1,
                "SignType":3,
                "SignId":0,
                "AgreementEventId":null,
                "RequestId":null
            },
            "Out":{
                "Time": end,
                "new":true,
                "SignStatus":1,
                "SignType":3,
                "SignId":0,
                "AgreementEventId":null,
                "RequestId":null
            },
            "totalSlot": calculateSlotTime(start, end),
            "order": order
        };
    }

    const getTimeWindows = () => {
        const totalSlots = 2;
        const slots = [];

        for (let i = 1; i <= totalSlots; i++) {
            slots.push(createSlot(document.querySelector('[name="af-window-' + i + '-start"]').value, document.querySelector('[name="af-window-' + i + '-end"]').value, i));
        }

        return slots;
    };

    const submitAutofill = async (selectedDays, submitButton) => {
        if (selectedDays.length < 1) {
            return;
        }

        submitButton.setAttribute('disabled', 'disabled');

        const slots = getTimeWindows();

        for (const selectedDay of selectedDays) {
            const dairyId = selectedDay.value;
            const date = selectedDay.dataset.date;
            const params = {
                "DiaryId": dairyId,
                "UserId": userId,
                "Date": date + "T00:00:00.000",
                "DepartmentId": departmentId,
                "JobTitleId":null,
                "CalendarId": calendarId,
                "ScheduleId": scheduleId,
                "AgreementId":null,
                "TrueStartTime":null,
                "TrueEndTime":null,
                "TrueBreaksHours":1,
                "Accepted":false,
                "Comments":null,
                "Slots": slots
            };

            // one by one to prevent getting banned
            try {
                await api("diaries/" + dairyId + "/workday/slots/self", JSON.stringify(params), 'put');
            } catch (e) {console.log(e)}
        }

        alert("done, reload to actually check if it worked");
        submitButton.removeAttribute('disabled');

    };

    const getContainerTemplate = () => {
        const div = document.createElement('div');
        div.classList.add('dropdown');
        div.style.display = 'inline-block';

        div.innerHTML = `
    <style>
    .dropdown {
        left: 20%;
        top: 10px;
        position: absolute;
        z-index: 99999;
     }
     .text-danger {
         color: red;
     }
     .dropdown-menu {
        background-color: white;
        padding: 5px;
        border: 2px solid
     }
     .dropdown-menu:not(.show) {
         display: none;
     }
    </style>
            <ul class="dropdown-menu" style="overflow: auto; text-align: left">
                <li style="margin-left: 5px">
                    <strong>Entrada - Salida</strong>
                    <div>
                        <input name="af-window-1-start" value="09:00" type="time" class="form-control" style="padding-right:0px">
                        <input name="af-window-1-end" value="14:00" type="time" class="form-control" style="margin-left: 0px">
                    </div>
                    <div>
                        <input name="af-window-2-start" value="16:00" type="time" class="form-control" style="padding-right:0px">
                        <input name="af-window-2-end" value="19:00" type="time" class="form-control" style="margin-left: 0px">
                    </div>
                    <button class="btn btn-primary" type="button" style="width:100%">Autofill</button>
                    <hr>
                </li>
            </ul>
        `;
        const submitButton = div.querySelector('button');
        submitButton.addEventListener('click', () => {
            submitAutofill(div.querySelectorAll('input:checked'), submitButton);
        });
        const ul = div.querySelector('ul');

        const button = document.createElement('button');
        button.classList.add('btn', 'btn-secondary', 'dropdown-toggle')
        button.type = "button";
        button.innerHTML = 'AutoFill <i class="fa fa-chevron-down"></i>';
        button.addEventListener('click', () => {
            ul.classList.toggle('show');
        });

        div.appendChild(button);
        div.appendChild(ul);

        return div;
    }

    const setGlobalData = (day) => {
        departmentId = day.DepartmentId;
        scheduleId = day.ScheduleId;
        calendarId = day.CalendarId;
        userId = day.UserId;
    };

    const removeOldEntries = () => {
        document.querySelectorAll('li[data-removable="true"]').forEach(el => {
            el.remove();
        });
    }

    const showUnfilledDays = async (ul) => {
        removeOldEntries();
        const user = parseUserToken(userToken);
        const dateRange = getDateRange();
        const days = await getPresence(user.UserId, dateRange.start, dateRange.end);
        const pendingDays = [];

        setGlobalData(days[0]);

        for (const day of days) {
            // we only want negative days
            if (parseInt(day.DiffFormatted.Values[0]) < 0 && !day.TrueStartTime && day.IsUserEditable) {
                pendingDays.push(day);
            }
        }

        for (const day of pendingDays) {
            const tpl = getDayTemplate(day);

            ul.appendChild(tpl);
        }
    };

    const getAllH2Contents = () => {
        let str = '';
        for (const el of document.querySelectorAll('h2')) {
            str += ' ' + el.innerText;
        }
        return str;
    }

    const onLoaded = async () => {
        if (document.querySelectorAll('.react-datepicker__input-container input').length < 2) {
            setTimeout(onLoaded, 1000 * 1)
            return;
        }

        userToken = await getUserToken();

        // not logged in
        if (!userToken) {
            return;
        }

        const container = getContainerTemplate();

        document.body.prepend(container);

        container.querySelector('button').addEventListener('click', () => {
            const ul = container.querySelector('ul');
            showUnfilledDays(ul);
        });

    };

    onLoaded();
})();