Toffu

Autofills Woffu schedule

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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