metis/src/calendar.js
tomate e420cdd0b4 Move room and calendar data to a specific folder (#52)
On met tout dans /data pour décorréler les sources du calendrier de la logique au maximum

Co-authored-by: Tom Hubrecht <tom.hubrecht@ens.fr>
Reviewed-on: https://git.rz.ens.wtf/Klub-RZ/metis/pulls/52
Co-authored-by: tomate <tom.hubrecht@ens.fr>
Co-committed-by: tomate <tom.hubrecht@ens.fr>
2022-09-12 17:46:54 +02:00

208 lines
5.2 KiB
JavaScript

import CALENDARS from '../data/calendars.json'
import LOCATIONS from '../data/locations.json'
// https://stackoverflow.com/a/35970186
function invertColor(hex) {
if (hex.indexOf('#') === 0) {
hex = hex.slice(1)
}
// convert 3-digit hex to 6-digits.
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
}
if (hex.length !== 6) {
throw new Error('Invalid HEX color.')
}
var r = parseInt(hex.slice(0, 2), 16),
g = parseInt(hex.slice(2, 4), 16),
b = parseInt(hex.slice(4, 6), 16)
// https://stackoverflow.com/a/3943023/112731
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? '#000000' : '#FFFFFF'
}
const parseCalendars = () => {
let calendars = {};
for (const [cloud, cals] of Object.entries(CALENDARS.sources)) {
for (const [id, attrs] of Object.entries(cals)) {
calendars[id] = { cloud: cloud, ...attrs }
}
}
return calendars;
}
const calendars = parseCalendars()
const calendarsByName = Object.fromEntries(
Object.entries(calendars).map(([id, { name }]) => [name, id])
)
export const initialCalendars = Array.from(
Object.entries(calendars).map(([_, { name, initial }]) => [name, initial ?? true])
)
.filter(cal => cal[1])
.map(cal => cal[0])
export const calendarTree = CALENDARS.tree
const dfs = (p, t, l) => {
for (const [c, s] of Object.entries(t)) {
l[c] = p === null ? [] : [p, ...l[p]]
dfs(c, s, l)
}
}
export const ancestors = (() => {
let l = []
dfs(null, calendarTree, l)
return l
})()
export function getSubCalendars(name, tree = calendarTree) {
let ret
for (const [cal, subTree] of Object.entries(tree)) {
if (cal === name) {
ret = subTree
} else {
ret = ret || getSubCalendars(name, subTree)
}
}
return ret
}
const calendarIds = Object.keys(calendars)
function mkCalendarUrl(id, { cloud }, extra = {}) {
return (
`/cal/${cloud}/${id}/?` +
new URLSearchParams({
...extra,
export: true,
expand: true,
accept: 'jcal'
})
)
}
function mkExportUrl(id, { cloud }) {
return `/cal/${cloud}/${id}/?export`
}
function fetchCalendar(id, cal, extra = {}) {
return fetch(mkCalendarUrl(id, cal, extra), { credentials: 'omit' })
.then(resp => resp.json())
.catch(err => console.error(err))
}
class Calendar {
constructor(id, calendar) {
const metadata = calendars[id]
this.name = metadata.name
this.short_name = metadata.short_name
this.color = metadata.color || calendar[1][4][3]
this.default_location = metadata.default_location
this.events = calendar[2]
.filter(item => item[0] === 'vevent')
.map(item => this._parse_vevent(item[1]))
}
_parse_vevent(vevent) {
const event = {}
vevent.forEach(elt => {
event[elt[0]] = elt[3]
})
return event
}
}
function findLocationId(location) {
const correctedLocation = LOCATIONS.nameMap[location] || location
const result = Object.entries(LOCATIONS.rooms).find(([building, rooms]) =>
rooms.includes(correctedLocation)
)
if (result === undefined) return undefined
const [building, _] = result
return `${building}-${correctedLocation}`
}
function fcEventFromjCalEvent(cal) {
return function (evt) {
const start = new Date(evt.dtstart)
const end = new Date(evt.dtend)
const fcEvent = {
title: `${cal.short_name ?? cal.name} : ${evt.summary}`,
start: evt.dtstart,
end: evt.dtend,
color: cal.color,
textColor: invertColor(cal.color),
duration: end - start // in ms
}
fcEvent.calendar = cal.name
fcEvent.short_name = evt.summary
fcEvent.description = evt.description
fcEvent.location = evt.location || cal.default_location
if (fcEvent.location) {
fcEvent.resourceId = findLocationId(fcEvent.location)
}
if (evt.status) {
fcEvent.status = evt.status
fcEvent.classNames = [`st-${evt.status.toLowerCase()}`]
}
if (evt.rrule) {
const { freq, byday, interval } = evt.rrule
fcEvent.rrule = {
freq,
byweekday: byday,
dtstart: evt.dtstart
}
if (interval) {
fcEvent.rrule.interval = interval
}
}
return fcEvent
}
}
function mkEventsFromCalendar(id, cal) {
return fetchCalendar(id, cal).then(calendar => {
if (calendar[0] !== 'vcalendar') return
const cal = new Calendar(id, calendar)
return cal.events.map(fcEventFromjCalEvent(cal))
})
}
export function mkSource(name) {
const calendarId = calendarsByName[name]
if (!calendarId) return null
const calendar = calendars[calendarId]
return {
id: name,
...(calendar?.meta || {}),
success: calendarData => {
if (calendarData[0] !== 'vcalendar') return
const cal = new Calendar(calendarId, calendarData)
return cal.events.map(fcEventFromjCalEvent(cal))
},
failure: error => {
console.error(`Fatal error during event source fetching of '${name}': ${error}`)
},
events: (info, successCallback, failureCallback) => {
const { start, end } = info
fetchCalendar(calendarId, calendar, {
start: start.valueOf() / 1000,
end: end.valueOf() / 1000
}).then(successCallback, failureCallback)
},
export_url: mkExportUrl(calendarId, calendar)
}
}