Skip to content

Instantly share code, notes, and snippets.

@juandjara
Last active October 31, 2023 15:20
Show Gist options
  • Save juandjara/e4424c8be0186fee949ace0d43f442e4 to your computer and use it in GitHub Desktop.
Save juandjara/e4424c8be0186fee949ace0d43f442e4 to your computer and use it in GitHub Desktop.
Script para GreaseMonkey / TamperMonkey para rellenar las horas en kenjo automaticamente
// ==UserScript==
// @name Kenjo Attendance Fill Month
// @namespace attendance.kenjo.sconde.net
// @version 1.1
// @description Fill Kenjo Attendance month with templates
// @author Sergio Conde, Juan Domínguez Jara
// @match https://app.kenjo.io/*
// @match https://app.orgos.io/*
// @grant GM.getValue
// @grant GM.setValue
// @homepageURL https://github.com/skgsergio/greasemonkey-scripts
// @supportURL https://github.com/skgsergio/greasemonkey-scripts/issues
// @updateURL https://gist.githubusercontent.com/juandjara/e4424c8be0186fee949ace0d43f442e4/raw/f7e1352b3e484420d0e704c88bdc02a99fdc9551/kenjo-fill-month.js
// ==/UserScript==
'use strict';
/*
Don't touch this DEFAULT_* values, won't persist across updates.
Load Kenjo for the first time with the script and then open this script Storage preferences and edit there.
*/
/* 0 = Sunday, 1 = Monday, ..., 6 = Saturday */
const DEFAULT_SCHEDULE = {
1: [{ start: '9:00', hours: '8:00', pause: '00:30' }],
2: [{ start: '9:00', hours: '8:00', pause: '00:30' }],
3: [{ start: '9:00', hours: '8:00', pause: '00:30' }],
4: [{ start: '9:00', hours: '8:00', pause: '00:30' }],
5: [{ start: '9:00', hours: '8:00', pause: '00:30' }]
};
const DEFAULT_ENTROPY_MINUTES = 15;
/**
* Here be dragons
**/
/* API Endpoints */
const API_URL = 'https://api.kenjo.io';
const AUTH_COOKIE_URL = `${API_URL}/auth/cookie`;
const ME_URL = `${API_URL}/user-account-db/user-accounts/me`;
const TIMEOFF_URL = `${API_URL}/controller/time-off-user-history/`;
const CALENDAR_URL = `${API_URL}/calendar-db/find`;
const TEMPLATES_URL = `${API_URL}/calendar-template-db/templates`;
const ATTENDANCE_URL = `${API_URL}/user-attendance-db`;
function USERWORK_URL(userId) {
return `${API_URL}/user-work-db/${userId}/calendar`;
}
/* Fetch function */
async function fetchUrl(auth, url, method = 'GET', body = null) {
const headers = { 'Content-Type': 'application/json' }
if (auth) {
headers.Authorization = auth;
}
try {
const response = await fetch(url, { method, credentials: 'include', headers, body })
if (!response.ok) {
throw Error(`HTTP Code: ${response.status}`);
}
if (method !== 'DELETE') {
return await response.json();
}
} catch (err) {
throw new Error(`Failed performing request, reload the site and try again.\n\n${method} ${url}\n${err}`);
}
}
/* AUTH */
async function getAuth() {
const data = await fetchUrl(null, AUTH_COOKIE_URL);
return `${data.token_type} ${data.access_token}`;
}
/* GET */
function getUser(auth) {
return fetchUrl(auth, ME_URL);
}
function getUserCalendar(auth, userId) {
return fetchUrl(auth, USERWORK_URL(userId));
}
function getCalendarTemplates(auth) {
return fetchUrl(auth, TEMPLATES_URL);
}
/* POST */
function getCalendar(auth, calendarId) {
return fetchUrl(
auth,
CALENDAR_URL,
'POST',
JSON.stringify({
_id: calendarId
})
);
}
function getUserTimeOff(auth, userId, fromDate, toDate) {
return fetchUrl(
auth,
TIMEOFF_URL,
'POST',
JSON.stringify({
_from: { $gte: fromDate },
_to: { $lte: toDate },
userId: userId
})
);
}
function addEntry(auth, userId, date, startTime, endTime, breakTime) {
return fetchUrl(
auth,
ATTENDANCE_URL,
'POST',
JSON.stringify({
ownerId: userId,
date: date,
startTime: startTime,
endTime: endTime,
breakTime: breakTime,
_approved: false,
_changesTracking: [],
_deleted: false,
_userId: userId
})
);
}
function getEntries(auth, userId, startDate, endDate) {
return fetchUrl(
auth,
`${ATTENDANCE_URL}/find`,
'POST',
JSON.stringify({
_deleted: false,
_userId: userId,
date: {
'$gte': startDate,
'$lte': endDate
}
})
)
}
function deleteEntry(auth, entryId) {
return fetchUrl(
auth,
`${ATTENDANCE_URL}/${entryId}`,
'DELETE'
)
}
/* HELPERS */
function startOfMonth(date) {
return new Date(Date.UTC(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0));
}
function endOfMonth(date) {
return new Date(Date.UTC(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59, 999));
}
function hhmmToMinutes(str) {
return str.split(':').reduce((acc, curr) => (acc*60) + +curr);
}
/* MAIN */
var SCHEDULE = {};
var ENTROPY_MINUTES = 0;
const months_ES = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']
const months_EN = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
function findCurrentMonth() {
const monthLabel = document.querySelector('kenjo-input-month-picker > div > div > div')
console.log('current month: ', monthLabel.innerText)
const currentMonthLabel = monthLabel.innerText.split(' ')[0]
const currentYear = monthLabel.innerText.split(' ')[1]
const indexES = months_ES.indexOf(currentMonthLabel)
const indexEN = months_EN.indexOf(currentMonthLabel)
if (indexES !== -1) {
return new Date(currentYear, indexES, 12)
}
if (indexEN !== -1) {
return new Date(currentYear, indexEN, 12)
}
return null
}
async function fillMonth(log) {
try {
log("Getting current month");
const baseDate = findCurrentMonth();
const monthStart = startOfMonth(baseDate);
const monthEnd = endOfMonth(baseDate);
/* Get user info */
log("Getting user info...");
const auth = await getAuth();
const user = await getUser(auth);
log("Getting user time off...");
const timeOff = await getUserTimeOff(auth, user.ownerId, monthStart.toISOString(), monthEnd.toISOString());
/* Get calendar info */
log("Getting user calendar...");
const userCalendar = await getUserCalendar(auth, user.ownerId);
const calendars = await getCalendar(auth, userCalendar.calendarId);
const templates = await getCalendarTemplates(auth);
const template = templates.filter(tpl => tpl.templateKey == calendars[0]._calendarTemplateKey)[0];
/* Parse non working days */
log("Processing non working days...");
const nonWorkingDays = [];
console.log(timeOff.records)
timeOff.records.forEach((t) => {
nonWorkingDays.push({
reason: t.request._policyName,
start: new Date(Date.parse(t.request.from)),
end: new Date(Date.parse(t.request.to))
});
});
template.holidays.forEach((h) => {
const start = new Date(Date.parse(`${h.holidayDate}T00:00:00.000Z`));
const end = new Date(Date.parse(`${h.holidayDate}T23:59:59.999Z`));
if (start >= monthStart && start <= monthEnd) {
nonWorkingDays.push({
reason: h.holidayKey,
start: start,
end: end
});
}
});
calendars[0]._customHolidays.forEach((h) => {
const holidayDate = h.holidayDate.split("T")[0];
const start = new Date(Date.parse(`${holidayDate}T00:00:00.000Z`));
const end = new Date(Date.parse(`${holidayDate}T23:59:59.999Z`));
if (start >= monthStart && start <= monthEnd) {
nonWorkingDays.push({
reason: h.holidayName,
start: start,
end: end
});
}
});
/* Delete previous entries */
const confirmResult = window.confirm('Do you want to delete previous entries for this month?')
if (confirmResult) {
log(`Deleting previous entries...`)
const previousEntries = await getEntries(auth, user._id, monthStart.toISOString(), monthEnd.toISOString())
log(`Found ${previousEntries.length} previous entries...`)
console.log('Entries to delete: \n', previousEntries)
for (const p of previousEntries) {
log(`Deleting entry ${previousEntries.indexOf(p) + 1} of ${previousEntries.length}...`)
await deleteEntry(auth, p._id)
}
}
/* Generate month sheet */
log("Generating attendance sheet...");
const entries = [];
const skippedDays = [];
for (let day = monthStart; day <= monthEnd; day.setDate(day.getDate() + 1)) {
/* Check if the day has an schedule */
const weekday = day.getDay()
if (!(weekday in SCHEDULE) || SCHEDULE[weekday].length == 0) {
continue;
}
/* Check if the day should be skipped (holiday or time off) */
const skipReasons = nonWorkingDays.filter((nwd) => day >= nwd.start && day <= nwd.end);
if (skipReasons.length > 0) {
skippedDays.push({ day: new Date(day.getTime()), reasons: skipReasons.map(sr => sr.reason) });
continue;
}
/* Produce an entry (or many) for this day */
for (const sch of SCHEDULE[weekday]) {
const start = hhmmToMinutes(sch.start) + Math.ceil(Math.random() * ENTROPY_MINUTES);
const pause = hhmmToMinutes(sch.pause);
const end = start + pause + hhmmToMinutes(sch.hours);
entries.push({
date: day.toISOString(),
start: start,
end: end,
pause: pause
});
}
}
/* Store sheet */
for (const ts of entries) {
const idx = entries.indexOf(ts)
log(`Saving day ${idx+1} of ${entries.length}...`);
const entryResult = await addEntry(auth, user.ownerId, ts.date, ts.start, ts.end, ts.pause)
console.log(`Saved entry ${idx+1} of ${entries.length}\n`, entryResult);
}
/* Show info to the user */
log("Done");
let skippedTxt = "";
skippedDays.forEach((s) => { skippedTxt += `\n${s.day.toISOString().split("T")[0]}: ${s.reasons.join(', ')}` });
alert(`Created ${entries.length} entries.\n\nSkipped days:${skippedTxt}`);
/* Reload page to reflect changes */
location.assign(`${location.origin}/cloud/people/${user.ownerId}/attendance`);
} catch(err) {
alert(`Kenjo Attendance Fill Month error:\n${err}`);
}
}
async function createUI() {
/* Make schedule and entropy configurable */
SCHEDULE = await GM.getValue('SCHEDULE');
if (!SCHEDULE) {
SCHEDULE = DEFAULT_SCHEDULE;
GM.setValue('SCHEDULE', SCHEDULE);
}
ENTROPY_MINUTES = await GM.getValue('ENTROPY_MINUTES');
if (!ENTROPY_MINUTES) {
ENTROPY_MINUTES = DEFAULT_ENTROPY_MINUTES;
GM.setValue('ENTROPY_MINUTES', ENTROPY_MINUTES);
}
/* Add button */
const extDiv = document.createElement('div');
Object.assign(extDiv.style, {
position: 'fixed',
top: '58px',
right: '8px'
})
const monthBtn = document.createElement('button');
Object.assign(monthBtn.style, {
padding: '4px 8px',
fontSize: '14px',
cursor: 'pointer'
});
monthBtn.type = 'button';
monthBtn.innerText = 'Attendance: Fill Month';
function log(text) {
monthBtn.innerText = text
}
monthBtn.addEventListener('click', () => {
monthBtn.disabled = 'disabled';
fillMonth(log);
})
extDiv.append(monthBtn);
document.body.insertBefore(extDiv, document.body.firstChild);
}
createUI();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment