web-app-demos/apps/calendar/static/js/script.js
2025-03-20 03:10:43 -06:00

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 || '&nbsp;';
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>&nbsp;</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();
});
}