530 lines
No EOL
16 KiB
JavaScript
530 lines
No EOL
16 KiB
JavaScript
dayjs.extend(window.dayjs_plugin_customParseFormat)
|
|
dayjs.extend(window.dayjs_plugin_minMax)
|
|
dayjs.extend(window.dayjs_plugin_isBetween)
|
|
dayjs.extend(window.dayjs_plugin_duration)
|
|
dayjs.isSameOrBefore = (e,t) => {return e.isSame(t) || e.isBefore(t)}
|
|
dayjs.isSameOrAfter = (e,t) => {return e.isSame(t) || e.isAfter(t)}
|
|
|
|
const el = (id) => document.getElementById(id);
|
|
|
|
let selected_cal_type = Cookie.get('caltype') ?? "daily";
|
|
el("caltype").value = selected_cal_type;
|
|
|
|
const date = Cookie.get('date');
|
|
let selected_date = date ? dayjs(date) : dayjs().startOf('day');
|
|
el('selected_date').value = selected_date.format('YYYY-MM-DD');
|
|
|
|
let selected_event = null;
|
|
let update_timeout = null;
|
|
let lastPicker = null;
|
|
let week_starts = [];
|
|
let weeks = [];
|
|
|
|
let startOfWorkDay, endOfWorkDay, startOfDay, endOfDay, startOfMonth, endOfMonth, startOfCalMonth, endOfCalMonth, lastEvent;
|
|
|
|
const event_to_object = (datum) => {
|
|
return {
|
|
id: datum[0],
|
|
name: datum[1],
|
|
message: datum[2],
|
|
timefrom: datum[3],
|
|
timeto: datum[4],
|
|
timetbd: datum[5],
|
|
datefrom: datum[6],
|
|
dateto: datum[7],
|
|
datetbd: datum[8],
|
|
deleted: datum[9]
|
|
}
|
|
}
|
|
|
|
const init_times = (today) => {
|
|
startOfWorkDay = today.hour(8).minute(0).second(0);
|
|
endOfWorkDay = today.hour(17).minute(0).second(0);
|
|
lastEvent = today.hour(16).minute(30).second(0);
|
|
startOfDay = today.startOf('day');
|
|
endOfDay = today.endOf('day');
|
|
startOfMonth = today.startOf('month');
|
|
endOfMonth = today.endOf('month');
|
|
startOfCalMonth = dayjs(startOfMonth).startOf('week');
|
|
endOfCalMonth = dayjs(endOfMonth).endOf('week');
|
|
}
|
|
|
|
const create_daily_event = (event) => {
|
|
let node = document.createElement("DIV");
|
|
node.classList.add("event");
|
|
if (event.timetbd && !event.datetbd) {
|
|
node.classList.add("timetbd");
|
|
}
|
|
|
|
let border = document.createElement("DIV");
|
|
border.classList.add("border-color");
|
|
border.style.backgroundColor = event.colour;
|
|
node.appendChild(border);
|
|
|
|
let content = document.createElement("DIV");
|
|
content.classList.add("p-1", "flex-grow-1");
|
|
let html = `<span class='title'>${event.name}</span>`;
|
|
if (event.message) {
|
|
html += `<span> - ${event.message.replaceAll('\n','<br>')}</span>`;
|
|
}
|
|
content.innerHTML = html;
|
|
node.appendChild(content);
|
|
|
|
node.addEventListener("click", () => { eventedit(event); } );
|
|
|
|
return node;
|
|
}
|
|
|
|
const create_monthly_event = (left, top, right, event, week) => {
|
|
let node = document.createElement("DIV");
|
|
node.className = "monthevent text-truncate";
|
|
if (event.datetbd) {
|
|
node.className += " datetbd";
|
|
}
|
|
node.title = event.message ? `${event.name} - ${event.message}` : event.name || ' ';
|
|
node.innerHTML = node.title;
|
|
|
|
node.style.left = left + "%";
|
|
node.style.top = top + "rem";
|
|
node.style.right = right + "%";
|
|
node.style.backgroundColor = event.colour;
|
|
|
|
node.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
eventedit(event);
|
|
} );
|
|
|
|
week.append(node);
|
|
}
|
|
|
|
const collide = (first, second, domain) => {
|
|
return ( first[domain + "fromeffective"].isBetween(second[domain + "fromeffective"], second[domain + "toeffective"], null, '[]') ||
|
|
second[domain + "fromeffective"].isBetween( first[domain + "fromeffective"], first[domain + "toeffective"], null, '[]'));
|
|
}
|
|
|
|
const conflict = (events, domain) => {
|
|
let conflict_list = [];
|
|
for (let i = 0; i < events.length; i++) {
|
|
conflict_list[i] = [];
|
|
for (let j = 0; j < events.length; j++) {
|
|
if ((i != j) && collide(events[i], events[j], domain)) {
|
|
conflict_list[i].push(j);
|
|
}
|
|
}
|
|
}
|
|
return conflict_list;
|
|
}
|
|
|
|
const connected_components = (adj) => {
|
|
let visited = new Array(adj.length)
|
|
for(let i=0; i<adj.length; ++i) {
|
|
visited[i] = false;
|
|
}
|
|
let components = [];
|
|
for(let i=0; i<adj.length; ++i) {
|
|
if(visited[i]) {
|
|
continue;
|
|
}
|
|
let to_visit = [i];
|
|
let cc = [i];
|
|
visited[i] = true;
|
|
while(to_visit.length > 0) {
|
|
let v = to_visit.pop();
|
|
for(let j=0; j<adj[v].length; ++j) {
|
|
let u = adj[v][j]
|
|
if(visited[u]) {
|
|
continue;
|
|
}
|
|
visited[u] = true;
|
|
to_visit.push(u);
|
|
cc.push(u);
|
|
}
|
|
}
|
|
components.push(cc);
|
|
}
|
|
return components;
|
|
}
|
|
|
|
const event_fits = (events, index, stream, domain) => {
|
|
for (let i = 0; i < stream.length; i++) {
|
|
if (collide(events[stream[i]], events[index], domain)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const fit_events = (components, events, domain) => {
|
|
components.forEach((component) => {
|
|
let events_per_stream = [ [] ];
|
|
component.forEach((index) => {
|
|
let i = 0;
|
|
while (true) {
|
|
if (event_fits(events, index, events_per_stream[i], domain)) {
|
|
events_per_stream[i].push(index)
|
|
events[index].unitoffset = i;
|
|
break;
|
|
}
|
|
i++;
|
|
if (i == events_per_stream.length) {
|
|
events_per_stream.push([]);
|
|
}
|
|
}
|
|
});
|
|
component.forEach((index) => {
|
|
events[index].unitwidth = events_per_stream.length;
|
|
});
|
|
});
|
|
}
|
|
|
|
//Helpers for daily calendar
|
|
const setup_ticker = () => {
|
|
let ticker = ['<div id="now"></div>'];
|
|
|
|
for (let i = dayjs(startOfWorkDay); dayjs.isSameOrBefore(i, endOfWorkDay); i = i.add(30, 'minutes')) {
|
|
if (i.minute() == 0) {
|
|
ticker.push(`<div><span>${i.format("h:mm")}</span> ${i.format("A")}</div>`);
|
|
}
|
|
else {
|
|
ticker.push(`<div><span> </span>${i.format("h:mm")}</div>`);
|
|
}
|
|
}
|
|
|
|
el('timings').innerHTML = ticker.join('');
|
|
}
|
|
|
|
const setup_daily = (dailyEvents, allDayEvents) => {
|
|
el('main').replaceChildren(el('t-daily').content.cloneNode(true));
|
|
el('main').classList.remove("flex-column");
|
|
el('main').classList.add("flex-row");
|
|
setup_ticker();
|
|
el('events').addEventListener("dblclick", eventadd);
|
|
|
|
let conflict_list = conflict(dailyEvents, "time");
|
|
let components = connected_components(conflict_list);
|
|
|
|
fit_events(components, dailyEvents, "time");
|
|
|
|
dailyEvents.forEach(event => {
|
|
let left = (99/event.unitwidth) * (event.unitoffset) + 1;
|
|
let top = (event.timefromeffective - startOfWorkDay) / (endOfWorkDay - startOfWorkDay) * 100;
|
|
let width = 99/event.unitwidth - 1;
|
|
let height = (event.timetoeffective - event.timefromeffective) / (endOfWorkDay - startOfWorkDay) * 100 - 0.2;
|
|
let node = create_daily_event(event);
|
|
node.classList.add("position-absolute");
|
|
node.style.width = width + "%";
|
|
node.style.height = height + "%";
|
|
node.style.top = top + "%";
|
|
node.style.left = left + "%";
|
|
el("events").append(node);
|
|
});
|
|
|
|
allDayEvents.forEach(event => {
|
|
let node = create_daily_event(event);
|
|
el("allday").append(node);
|
|
});
|
|
|
|
}
|
|
|
|
//Helpers for monthly calendar
|
|
const setup_day_names = () => {
|
|
let day_names = '';
|
|
|
|
for (let word of ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]) {
|
|
day_names += `<div class='col text-truncate text-center'>${word}</div>`;
|
|
}
|
|
|
|
el('daynames').innerHTML = day_names;
|
|
}
|
|
|
|
const setup_day_cells = () => {
|
|
let day_cells = '';
|
|
week_starts = [];
|
|
weeks = [];
|
|
let today = dayjs(startOfCalMonth);
|
|
let node = null;
|
|
|
|
for (; dayjs.isSameOrBefore(today, endOfCalMonth); today = today.add(1, 'days')) {
|
|
if (today.day() == 0) {
|
|
node = document.createElement("DIV");
|
|
node.className = "row";
|
|
node.eventcounter = 0;
|
|
node.expanded = false;
|
|
node.expandable = false;
|
|
node.addEventListener("click", (e) => {
|
|
let target = e.currentTarget;
|
|
if (! target.expandable) { return; }
|
|
target.expanded = !target.expanded;
|
|
let newheight = target.expanded ? Math.max((target.eventcounter + 1) * 2.5 + 1, 11) : 11;
|
|
target.style.height = newheight + "rem";
|
|
if (target.expanded) {
|
|
target.classList.remove("expandable");
|
|
target.classList.add("contractable");
|
|
}
|
|
else {
|
|
target.classList.add("expandable");
|
|
target.classList.remove("contractable");
|
|
}
|
|
} );
|
|
weeks.push(node);
|
|
week_starts.push(dayjs(today));
|
|
}
|
|
let current = (today.isBefore(startOfMonth) || today.isAfter(endOfMonth)) ? "notcurrent" : "";
|
|
let thisday = today.isSame(dayjs(), 'day') ? "today" : "";
|
|
let weekend = (today.day() == 0 || today.day() == 6) ? "weekend" : "";
|
|
day_cells += `<div class='col text-truncate cell ${current} ${thisday} ${weekend}'><h5>${today.date()}</h5></div>`;
|
|
if (today.day() == 6) {
|
|
node.innerHTML = day_cells;
|
|
el('monthlycalendar').append(node);
|
|
day_cells = "";
|
|
}
|
|
}
|
|
week_starts.push(dayjs(today));
|
|
}
|
|
|
|
const breakup = (events) => {
|
|
let brokenupevents = [];
|
|
for (let event of events) {
|
|
for (let weekstart of week_starts) {
|
|
if (weekstart.isBetween(event.datefromeffective, event.datetoeffective, null, '[]')) {
|
|
if (!event.broken) {
|
|
let mod = { ...event };
|
|
mod.datetoeffective = dayjs(weekstart).subtract(1, 'days');
|
|
if (dayjs.isSameOrAfter(mod.datetoeffective, mod.datefromeffective)) {
|
|
brokenupevents.push(mod);
|
|
}
|
|
event.broken = true;
|
|
}
|
|
|
|
let mod = { ...event };
|
|
mod.datefromeffective = dayjs(weekstart);
|
|
mod.datetoeffective = dayjs.min(mod.datetoeffective, dayjs(weekstart).add(6, "days"));
|
|
brokenupevents.push(mod);
|
|
}
|
|
}
|
|
if (!event.broken) {
|
|
brokenupevents.push(event);
|
|
}
|
|
}
|
|
return brokenupevents;
|
|
}
|
|
|
|
const setup_monthly = (events) => {
|
|
el('main').replaceChildren(el('t-monthly').content.cloneNode(true));
|
|
el('main').classList.remove("flex-row");
|
|
el('main').classList.add("flex-column");
|
|
setup_day_names();
|
|
setup_day_cells();
|
|
|
|
let brokenupevents = breakup(events);
|
|
let conflict_list = conflict(brokenupevents, "date");
|
|
let components = connected_components(conflict_list);
|
|
|
|
fit_events(components, brokenupevents, "date");
|
|
|
|
for (let event of brokenupevents) {
|
|
for (let i = 0; i < week_starts.length - 1; i++) {
|
|
if (event.datefromeffective.isBetween(week_starts[i], week_starts[i+1], null, '[)')) {
|
|
let leftdaysdiff = dayjs.duration(event.datefromeffective.diff(week_starts[i])).asDays();
|
|
let left = leftdaysdiff / 7 * 100 + 0.5;
|
|
let top = 2.5*(event.unitoffset+1);
|
|
let rightdaysdiff = dayjs.duration(week_starts[i+1].diff(event.datetoeffective)).asDays() - 1;
|
|
let right = rightdaysdiff / 7 * 100 + 0.5;
|
|
let numevents = Math.max(weeks[i].eventcounter, event.unitoffset+1);
|
|
if (numevents > 3) {
|
|
weeks[i].eventcounter = numevents;
|
|
weeks[i].expandable = true;
|
|
weeks[i].classList.add("expandable");
|
|
}
|
|
create_monthly_event(left, top, right, event, weeks[i]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const update_indicator = () => {
|
|
let curtime = (dayjs() - startOfWorkDay) / (endOfWorkDay - startOfWorkDay) * 100;
|
|
el('now').style.top = curtime + "%";
|
|
el('now').style.display = curtime >= 0 && curtime < 100 ? 'block' : 'none';
|
|
}
|
|
|
|
const day_onload = (request) => {
|
|
let data = null;
|
|
if (request.status >= 200 && request.status < 400) {
|
|
data = JSON.parse(request.responseText);
|
|
}
|
|
//else { }
|
|
let dailyEvents = [];
|
|
let allDayEvents = [];
|
|
for (let datum of data) {
|
|
let event = event_to_object(datum);
|
|
const tf = dayjs(event.timefrom, "H:mm:ss");
|
|
event.datefrom = dayjs(event.datefrom);
|
|
event.timefrom = dayjs(event.datefrom).hour(tf.hour()).minute(tf.minute());
|
|
const tt = dayjs(event.timeto, "H:mm:ss");
|
|
event.dateto = dayjs(event.dateto);
|
|
event.timeto = dayjs(event.dateto).hour(tt.hour()).minute(tt.minute());
|
|
event.colour ??= event.deleted ? "#ff0000" : "#32acea";
|
|
|
|
const startsBefore = event.datefrom.isBefore(startOfDay) || dayjs.isSameOrBefore(event.timefrom, startOfWorkDay);
|
|
const endsAfter = event.dateto.isAfter(endOfDay) || dayjs.isSameOrAfter(event.timeto, endOfWorkDay) || event.datetbd;
|
|
|
|
event.timefromeffective = startsBefore ? startOfWorkDay : dayjs.min(lastEvent, event.timefrom);
|
|
|
|
if (endsAfter || dayjs(event.timefromeffective).add(30, 'minutes').isAfter(endOfWorkDay)) {
|
|
event.timetoeffective = endOfWorkDay;
|
|
}
|
|
else if (event.timetbd) {
|
|
if (event.dateto.isBefore(dayjs(), 'day')) {
|
|
event.timetoeffective = endOfWorkDay;
|
|
}
|
|
else {
|
|
event.timetoeffective = dayjs.min(dayjs().add(1, 'hour'), endOfWorkDay);
|
|
}
|
|
}
|
|
else {
|
|
event.timetoeffective = dayjs.max(dayjs(event.timefromeffective).add(30, 'minutes'), event.timeto);
|
|
}
|
|
|
|
if (startsBefore && endsAfter) {
|
|
allDayEvents.push(event);
|
|
}
|
|
else {
|
|
dailyEvents.push(event);
|
|
}
|
|
}
|
|
setup_daily(dailyEvents, allDayEvents);
|
|
update_indicator();
|
|
}
|
|
|
|
const month_onload = (request) => {
|
|
let data = null;
|
|
if (request.status >= 200 && request.status < 400) {
|
|
data = JSON.parse(request.responseText);
|
|
}
|
|
//else { }
|
|
let converted_data = [];
|
|
for (let datum of data) {
|
|
let event = event_to_object(datum);
|
|
converted_data.push(event);
|
|
let tf = dayjs(event.timefrom, "H:mm:ss");
|
|
event.datefrom = dayjs(event.datefrom);
|
|
event.timefrom = dayjs(event.datefrom).hour(tf.hour()).minute(tf.minute());
|
|
let tt = dayjs(event.timeto, "H:mm:ss");
|
|
event.dateto = dayjs(event.dateto);
|
|
event.timeto = dayjs(event.dateto).hour(tt.hour()).minute(tt.minute());
|
|
event.datefromeffective = event.datefrom;
|
|
event.datetoeffective = event.dateto;
|
|
if (event.datefrom.isBefore(startOfCalMonth)) {
|
|
event.datefromeffective = startOfCalMonth;
|
|
}
|
|
if (event.dateto.isAfter(endOfCalMonth)) {
|
|
event.datetoeffective = endOfCalMonth;
|
|
}
|
|
if (event.datetbd) {
|
|
event.datetoeffective = endOfDay;
|
|
}
|
|
event.colour = event.deleted ? "#ff0000" : "#32acea";
|
|
}
|
|
setup_monthly(converted_data);
|
|
}
|
|
|
|
const adjustField = (field) => {
|
|
el(`${field}to`).prop( "readOnly", el(`${field}tbd`).prop("checked") );
|
|
}
|
|
|
|
const eventadd = () => {
|
|
selectedEvent = null;
|
|
let timenow = dayjs().format("HH:mm");
|
|
el('name').value = "";
|
|
el('message').value = "";
|
|
el('timefrom').value = timenow;
|
|
el('datefrom').value = selected_date.format('YYYY-MM-DD');
|
|
el('timeto').value = timenow;
|
|
el('dateto').value = selected_date.format('YYYY-MM-DD');
|
|
el('timetbd').checked = false;
|
|
el('datetbd').checked = false;
|
|
//adjustField('time');
|
|
//adjustField('date');
|
|
if (el('eventdelete')) {
|
|
el('eventdelete').style.display = 'none';
|
|
}
|
|
const myModal = new bootstrap.Modal(el('eventModal'))
|
|
myModal.show();
|
|
}
|
|
|
|
const eventedit = (event) => {
|
|
selectedEvent = event.id;
|
|
el('name').value = event.name;
|
|
el('message').value = event.message;
|
|
el('timefrom').value = event.timefrom.format("HH:mm");
|
|
el('datefrom').value = event.datefrom.format('YYYY-MM-DD');
|
|
el('timeto').value = event.timeto.format("HH:mm");
|
|
el('dateto').value = event.dateto.format('YYYY-MM-DD');
|
|
el('timetbd').checked = event.timetbd;
|
|
el('datetbd').checked = event.datetbd;
|
|
|
|
if (el('eventdelete')) {
|
|
el('eventdelete').style.display = 'block';
|
|
}
|
|
const myModal = new bootstrap.Modal(el('eventModal'))
|
|
myModal.show();
|
|
}
|
|
|
|
const init_event_listeners = () => {
|
|
el('selected_date').addEventListener('input', (e) => {
|
|
selected_date = dayjs(e.target.value);
|
|
init_times(selected_date);
|
|
Cookie.set('date', selected_date.format('YYYY-MM-DD'), selected_date.endOf('day').toDate());
|
|
lastPicker = null;
|
|
clearTimeout(update_timeout);
|
|
update();
|
|
});
|
|
|
|
const showPicker = (e) => {
|
|
if (e.currentTarget == lastPicker) {
|
|
lastPicker = null;
|
|
return;
|
|
}
|
|
lastPicker = e.currentTarget;
|
|
e.currentTarget.showPicker();
|
|
}
|
|
let pickers = ['selected_date', 'timefrom', 'datefrom', 'timeto', 'dateto'];
|
|
for (let picker of pickers) {
|
|
el(picker).addEventListener('click', showPicker);
|
|
el(picker).addEventListener('blur', (e) => {lastPicker = null;})
|
|
}
|
|
|
|
el('caltype').addEventListener("change", (e) => {
|
|
selected_cal_type = el('caltype').value;
|
|
Cookie.set('caltype', selected_cal_type);
|
|
clearTimeout(update_timeout);
|
|
update();
|
|
});
|
|
|
|
el('eventModal').addEventListener('shown.bs.modal', () => {
|
|
el('name').focus();
|
|
})
|
|
}
|
|
|
|
const init_user_listeners = () => {
|
|
el('checkout').addEventListener("click", eventadd);
|
|
|
|
const eventForm = el('eventform');
|
|
|
|
eventForm.addEventListener("submit", (e) => {
|
|
e.preventDefault();
|
|
eventForm.action = selectedEvent ? `/calendar/event/${selectedEvent}/edit` : '/calendar/event/add';
|
|
eventForm.submit();
|
|
});
|
|
|
|
el('eventdelete').addEventListener("click", (e) => {
|
|
eventForm.action = `/calendar/event/${selectedEvent}/delete`;
|
|
eventForm.submit();
|
|
});
|
|
el('eventsubmit').addEventListener("click", (e) => {
|
|
eventForm.action = selectedEvent ? `/calendar/event/${selectedEvent}/edit` : '/calendar/event/add';
|
|
eventForm.submit();
|
|
});
|
|
} |