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 = `${event.name}`; if (event.message) { html += ` - ${event.message.replaceAll('\n','
')}
`; } 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 0) { let v = to_visit.pop(); for(let j=0; j { 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 = ['
']; for (let i = dayjs(startOfWorkDay); dayjs.isSameOrBefore(i, endOfWorkDay); i = i.add(30, 'minutes')) { if (i.minute() == 0) { ticker.push(`
${i.format("h:mm")} ${i.format("A")}
`); } else { ticker.push(`
 ${i.format("h:mm")}
`); } } 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 += `
${word}
`; } 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 += `
${today.date()}
`; 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(); }); }