Last active
October 31, 2023 15:20
-
-
Save juandjara/e4424c8be0186fee949ace0d43f442e4 to your computer and use it in GitHub Desktop.
Script para GreaseMonkey / TamperMonkey para rellenar las horas en kenjo automaticamente
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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