var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // node_modules/dayjs/dayjs.min.js var require_dayjs_min = __commonJS({ "node_modules/dayjs/dayjs.min.js"(exports, module) { !(function(t, e) { "object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs = e(); })(exports, (function() { "use strict"; var t = 1e3, e = 6e4, n = 36e5, r = "millisecond", i = "second", s = "minute", u = "hour", a = "day", o = "week", c = "month", f = "quarter", h = "year", d = "date", l = "Invalid Date", $ = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/, y = /\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g, M = { name: "en", weekdays: "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), months: "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), ordinal: function(t2) { var e2 = ["th", "st", "nd", "rd"], n2 = t2 % 100; return "[" + t2 + (e2[(n2 - 20) % 10] || e2[n2] || e2[0]) + "]"; } }, m = function(t2, e2, n2) { var r2 = String(t2); return !r2 || r2.length >= e2 ? t2 : "" + Array(e2 + 1 - r2.length).join(n2) + t2; }, v = { s: m, z: function(t2) { var e2 = -t2.utcOffset(), n2 = Math.abs(e2), r2 = Math.floor(n2 / 60), i2 = n2 % 60; return (e2 <= 0 ? "+" : "-") + m(r2, 2, "0") + ":" + m(i2, 2, "0"); }, m: function t2(e2, n2) { if (e2.date() < n2.date()) return -t2(n2, e2); var r2 = 12 * (n2.year() - e2.year()) + (n2.month() - e2.month()), i2 = e2.clone().add(r2, c), s2 = n2 - i2 < 0, u2 = e2.clone().add(r2 + (s2 ? -1 : 1), c); return +(-(r2 + (n2 - i2) / (s2 ? i2 - u2 : u2 - i2)) || 0); }, a: function(t2) { return t2 < 0 ? Math.ceil(t2) || 0 : Math.floor(t2); }, p: function(t2) { return { M: c, y: h, w: o, d: a, D: d, h: u, m: s, s: i, ms: r, Q: f }[t2] || String(t2 || "").toLowerCase().replace(/s$/, ""); }, u: function(t2) { return void 0 === t2; } }, g = "en", D = {}; D[g] = M; var p = "$isDayjsObject", S = function(t2) { return t2 instanceof _ || !(!t2 || !t2[p]); }, w = function t2(e2, n2, r2) { var i2; if (!e2) return g; if ("string" == typeof e2) { var s2 = e2.toLowerCase(); D[s2] && (i2 = s2), n2 && (D[s2] = n2, i2 = s2); var u2 = e2.split("-"); if (!i2 && u2.length > 1) return t2(u2[0]); } else { var a2 = e2.name; D[a2] = e2, i2 = a2; } return !r2 && i2 && (g = i2), i2 || !r2 && g; }, O = function(t2, e2) { if (S(t2)) return t2.clone(); var n2 = "object" == typeof e2 ? e2 : {}; return n2.date = t2, n2.args = arguments, new _(n2); }, b = v; b.l = w, b.i = S, b.w = function(t2, e2) { return O(t2, { locale: e2.$L, utc: e2.$u, x: e2.$x, $offset: e2.$offset }); }; var _ = (function() { function M2(t2) { this.$L = w(t2.locale, null, true), this.parse(t2), this.$x = this.$x || t2.x || {}, this[p] = true; } var m2 = M2.prototype; return m2.parse = function(t2) { this.$d = (function(t3) { var e2 = t3.date, n2 = t3.utc; if (null === e2) return /* @__PURE__ */ new Date(NaN); if (b.u(e2)) return /* @__PURE__ */ new Date(); if (e2 instanceof Date) return new Date(e2); if ("string" == typeof e2 && !/Z$/i.test(e2)) { var r2 = e2.match($); if (r2) { var i2 = r2[2] - 1 || 0, s2 = (r2[7] || "0").substring(0, 3); return n2 ? new Date(Date.UTC(r2[1], i2, r2[3] || 1, r2[4] || 0, r2[5] || 0, r2[6] || 0, s2)) : new Date(r2[1], i2, r2[3] || 1, r2[4] || 0, r2[5] || 0, r2[6] || 0, s2); } } return new Date(e2); })(t2), this.init(); }, m2.init = function() { var t2 = this.$d; this.$y = t2.getFullYear(), this.$M = t2.getMonth(), this.$D = t2.getDate(), this.$W = t2.getDay(), this.$H = t2.getHours(), this.$m = t2.getMinutes(), this.$s = t2.getSeconds(), this.$ms = t2.getMilliseconds(); }, m2.$utils = function() { return b; }, m2.isValid = function() { return !(this.$d.toString() === l); }, m2.isSame = function(t2, e2) { var n2 = O(t2); return this.startOf(e2) <= n2 && n2 <= this.endOf(e2); }, m2.isAfter = function(t2, e2) { return O(t2) < this.startOf(e2); }, m2.isBefore = function(t2, e2) { return this.endOf(e2) < O(t2); }, m2.$g = function(t2, e2, n2) { return b.u(t2) ? this[e2] : this.set(n2, t2); }, m2.unix = function() { return Math.floor(this.valueOf() / 1e3); }, m2.valueOf = function() { return this.$d.getTime(); }, m2.startOf = function(t2, e2) { var n2 = this, r2 = !!b.u(e2) || e2, f2 = b.p(t2), l2 = function(t3, e3) { var i2 = b.w(n2.$u ? Date.UTC(n2.$y, e3, t3) : new Date(n2.$y, e3, t3), n2); return r2 ? i2 : i2.endOf(a); }, $2 = function(t3, e3) { return b.w(n2.toDate()[t3].apply(n2.toDate("s"), (r2 ? [0, 0, 0, 0] : [23, 59, 59, 999]).slice(e3)), n2); }, y2 = this.$W, M3 = this.$M, m3 = this.$D, v2 = "set" + (this.$u ? "UTC" : ""); switch (f2) { case h: return r2 ? l2(1, 0) : l2(31, 11); case c: return r2 ? l2(1, M3) : l2(0, M3 + 1); case o: var g2 = this.$locale().weekStart || 0, D2 = (y2 < g2 ? y2 + 7 : y2) - g2; return l2(r2 ? m3 - D2 : m3 + (6 - D2), M3); case a: case d: return $2(v2 + "Hours", 0); case u: return $2(v2 + "Minutes", 1); case s: return $2(v2 + "Seconds", 2); case i: return $2(v2 + "Milliseconds", 3); default: return this.clone(); } }, m2.endOf = function(t2) { return this.startOf(t2, false); }, m2.$set = function(t2, e2) { var n2, o2 = b.p(t2), f2 = "set" + (this.$u ? "UTC" : ""), l2 = (n2 = {}, n2[a] = f2 + "Date", n2[d] = f2 + "Date", n2[c] = f2 + "Month", n2[h] = f2 + "FullYear", n2[u] = f2 + "Hours", n2[s] = f2 + "Minutes", n2[i] = f2 + "Seconds", n2[r] = f2 + "Milliseconds", n2)[o2], $2 = o2 === a ? this.$D + (e2 - this.$W) : e2; if (o2 === c || o2 === h) { var y2 = this.clone().set(d, 1); y2.$d[l2]($2), y2.init(), this.$d = y2.set(d, Math.min(this.$D, y2.daysInMonth())).$d; } else l2 && this.$d[l2]($2); return this.init(), this; }, m2.set = function(t2, e2) { return this.clone().$set(t2, e2); }, m2.get = function(t2) { return this[b.p(t2)](); }, m2.add = function(r2, f2) { var d2, l2 = this; r2 = Number(r2); var $2 = b.p(f2), y2 = function(t2) { var e2 = O(l2); return b.w(e2.date(e2.date() + Math.round(t2 * r2)), l2); }; if ($2 === c) return this.set(c, this.$M + r2); if ($2 === h) return this.set(h, this.$y + r2); if ($2 === a) return y2(1); if ($2 === o) return y2(7); var M3 = (d2 = {}, d2[s] = e, d2[u] = n, d2[i] = t, d2)[$2] || 1, m3 = this.$d.getTime() + r2 * M3; return b.w(m3, this); }, m2.subtract = function(t2, e2) { return this.add(-1 * t2, e2); }, m2.format = function(t2) { var e2 = this, n2 = this.$locale(); if (!this.isValid()) return n2.invalidDate || l; var r2 = t2 || "YYYY-MM-DDTHH:mm:ssZ", i2 = b.z(this), s2 = this.$H, u2 = this.$m, a2 = this.$M, o2 = n2.weekdays, c2 = n2.months, f2 = n2.meridiem, h2 = function(t3, n3, i3, s3) { return t3 && (t3[n3] || t3(e2, r2)) || i3[n3].slice(0, s3); }, d2 = function(t3) { return b.s(s2 % 12 || 12, t3, "0"); }, $2 = f2 || function(t3, e3, n3) { var r3 = t3 < 12 ? "AM" : "PM"; return n3 ? r3.toLowerCase() : r3; }; return r2.replace(y, (function(t3, r3) { return r3 || (function(t4) { switch (t4) { case "YY": return String(e2.$y).slice(-2); case "YYYY": return b.s(e2.$y, 4, "0"); case "M": return a2 + 1; case "MM": return b.s(a2 + 1, 2, "0"); case "MMM": return h2(n2.monthsShort, a2, c2, 3); case "MMMM": return h2(c2, a2); case "D": return e2.$D; case "DD": return b.s(e2.$D, 2, "0"); case "d": return String(e2.$W); case "dd": return h2(n2.weekdaysMin, e2.$W, o2, 2); case "ddd": return h2(n2.weekdaysShort, e2.$W, o2, 3); case "dddd": return o2[e2.$W]; case "H": return String(s2); case "HH": return b.s(s2, 2, "0"); case "h": return d2(1); case "hh": return d2(2); case "a": return $2(s2, u2, true); case "A": return $2(s2, u2, false); case "m": return String(u2); case "mm": return b.s(u2, 2, "0"); case "s": return String(e2.$s); case "ss": return b.s(e2.$s, 2, "0"); case "SSS": return b.s(e2.$ms, 3, "0"); case "Z": return i2; } return null; })(t3) || i2.replace(":", ""); })); }, m2.utcOffset = function() { return 15 * -Math.round(this.$d.getTimezoneOffset() / 15); }, m2.diff = function(r2, d2, l2) { var $2, y2 = this, M3 = b.p(d2), m3 = O(r2), v2 = (m3.utcOffset() - this.utcOffset()) * e, g2 = this - m3, D2 = function() { return b.m(y2, m3); }; switch (M3) { case h: $2 = D2() / 12; break; case c: $2 = D2(); break; case f: $2 = D2() / 3; break; case o: $2 = (g2 - v2) / 6048e5; break; case a: $2 = (g2 - v2) / 864e5; break; case u: $2 = g2 / n; break; case s: $2 = g2 / e; break; case i: $2 = g2 / t; break; default: $2 = g2; } return l2 ? $2 : b.a($2); }, m2.daysInMonth = function() { return this.endOf(c).$D; }, m2.$locale = function() { return D[this.$L]; }, m2.locale = function(t2, e2) { if (!t2) return this.$L; var n2 = this.clone(), r2 = w(t2, e2, true); return r2 && (n2.$L = r2), n2; }, m2.clone = function() { return b.w(this.$d, this); }, m2.toDate = function() { return new Date(this.valueOf()); }, m2.toJSON = function() { return this.isValid() ? this.toISOString() : null; }, m2.toISOString = function() { return this.$d.toISOString(); }, m2.toString = function() { return this.$d.toUTCString(); }, M2; })(), k = _.prototype; return O.prototype = k, [["$ms", r], ["$s", i], ["$m", s], ["$H", u], ["$W", a], ["$M", c], ["$y", h], ["$D", d]].forEach((function(t2) { k[t2[1]] = function(e2) { return this.$g(e2, t2[0], t2[1]); }; })), O.extend = function(t2, e2) { return t2.$i || (t2(e2, _, O), t2.$i = true), O; }, O.locale = w, O.isDayjs = S, O.unix = function(t2) { return O(1e3 * t2); }, O.en = D[g], O.Ls = D, O.p = {}, O; })); } }); // node_modules/dayjs/plugin/utc.js var require_utc = __commonJS({ "node_modules/dayjs/plugin/utc.js"(exports, module) { !(function(t, i) { "object" == typeof exports && "undefined" != typeof module ? module.exports = i() : "function" == typeof define && define.amd ? define(i) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs_plugin_utc = i(); })(exports, (function() { "use strict"; var t = "minute", i = /[+-]\d\d(?::?\d\d)?/g, e = /([+-]|\d\d)/g; return function(s, f, n) { var u = f.prototype; n.utc = function(t2) { var i2 = { date: t2, utc: true, args: arguments }; return new f(i2); }, u.utc = function(i2) { var e2 = n(this.toDate(), { locale: this.$L, utc: true }); return i2 ? e2.add(this.utcOffset(), t) : e2; }, u.local = function() { return n(this.toDate(), { locale: this.$L, utc: false }); }; var r = u.parse; u.parse = function(t2) { t2.utc && (this.$u = true), this.$utils().u(t2.$offset) || (this.$offset = t2.$offset), r.call(this, t2); }; var o = u.init; u.init = function() { if (this.$u) { var t2 = this.$d; this.$y = t2.getUTCFullYear(), this.$M = t2.getUTCMonth(), this.$D = t2.getUTCDate(), this.$W = t2.getUTCDay(), this.$H = t2.getUTCHours(), this.$m = t2.getUTCMinutes(), this.$s = t2.getUTCSeconds(), this.$ms = t2.getUTCMilliseconds(); } else o.call(this); }; var a = u.utcOffset; u.utcOffset = function(s2, f2) { var n2 = this.$utils().u; if (n2(s2)) return this.$u ? 0 : n2(this.$offset) ? a.call(this) : this.$offset; if ("string" == typeof s2 && (s2 = (function(t2) { void 0 === t2 && (t2 = ""); var s3 = t2.match(i); if (!s3) return null; var f3 = ("" + s3[0]).match(e) || ["-", 0, 0], n3 = f3[0], u3 = 60 * +f3[1] + +f3[2]; return 0 === u3 ? 0 : "+" === n3 ? u3 : -u3; })(s2), null === s2)) return this; var u2 = Math.abs(s2) <= 16 ? 60 * s2 : s2; if (0 === u2) return this.utc(f2); var r2 = this.clone(); if (f2) return r2.$offset = u2, r2.$u = false, r2; var o2 = this.$u ? this.toDate().getTimezoneOffset() : -1 * this.utcOffset(); return (r2 = this.local().add(u2 + o2, t)).$offset = u2, r2.$x.$localOffset = o2, r2; }; var h = u.format; u.format = function(t2) { var i2 = t2 || (this.$u ? "YYYY-MM-DDTHH:mm:ss[Z]" : ""); return h.call(this, i2); }, u.valueOf = function() { var t2 = this.$utils().u(this.$offset) ? 0 : this.$offset + (this.$x.$localOffset || this.$d.getTimezoneOffset()); return this.$d.valueOf() - 6e4 * t2; }, u.isUTC = function() { return !!this.$u; }, u.toISOString = function() { return this.toDate().toISOString(); }, u.toString = function() { return this.toDate().toUTCString(); }; var l = u.toDate; u.toDate = function(t2) { return "s" === t2 && this.$offset ? n(this.format("YYYY-MM-DD HH:mm:ss:SSS")).toDate() : l.call(this); }; var c = u.diff; u.diff = function(t2, i2, e2) { if (t2 && this.$u === t2.$u) return c.call(this, t2, i2, e2); var s2 = this.local(), f2 = n(t2).local(); return c.call(s2, f2, i2, e2); }; }; })); } }); // node_modules/dayjs/plugin/timezone.js var require_timezone = __commonJS({ "node_modules/dayjs/plugin/timezone.js"(exports, module) { !(function(t, e) { "object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs_plugin_timezone = e(); })(exports, (function() { "use strict"; var t = { year: 0, month: 1, day: 2, hour: 3, minute: 4, second: 5 }, e = {}; return function(n, i, o) { var r, a = function(t2, n2, i2) { void 0 === i2 && (i2 = {}); var o2 = new Date(t2), r2 = (function(t3, n3) { void 0 === n3 && (n3 = {}); var i3 = n3.timeZoneName || "short", o3 = t3 + "|" + i3, r3 = e[o3]; return r3 || (r3 = new Intl.DateTimeFormat("en-US", { hour12: false, timeZone: t3, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", timeZoneName: i3 }), e[o3] = r3), r3; })(n2, i2); return r2.formatToParts(o2); }, u = function(e2, n2) { for (var i2 = a(e2, n2), r2 = [], u2 = 0; u2 < i2.length; u2 += 1) { var f2 = i2[u2], s2 = f2.type, m = f2.value, c = t[s2]; c >= 0 && (r2[c] = parseInt(m, 10)); } var d = r2[3], l = 24 === d ? 0 : d, h = r2[0] + "-" + r2[1] + "-" + r2[2] + " " + l + ":" + r2[4] + ":" + r2[5] + ":000", v = +e2; return (o.utc(h).valueOf() - (v -= v % 1e3)) / 6e4; }, f = i.prototype; f.tz = function(t2, e2) { void 0 === t2 && (t2 = r); var n2, i2 = this.utcOffset(), a2 = this.toDate(), u2 = a2.toLocaleString("en-US", { timeZone: t2 }), f2 = Math.round((a2 - new Date(u2)) / 1e3 / 60), s2 = 15 * -Math.round(a2.getTimezoneOffset() / 15) - f2; if (!Number(s2)) n2 = this.utcOffset(0, e2); else if (n2 = o(u2, { locale: this.$L }).$set("millisecond", this.$ms).utcOffset(s2, true), e2) { var m = n2.utcOffset(); n2 = n2.add(i2 - m, "minute"); } return n2.$x.$timezone = t2, n2; }, f.offsetName = function(t2) { var e2 = this.$x.$timezone || o.tz.guess(), n2 = a(this.valueOf(), e2, { timeZoneName: t2 }).find((function(t3) { return "timezonename" === t3.type.toLowerCase(); })); return n2 && n2.value; }; var s = f.startOf; f.startOf = function(t2, e2) { if (!this.$x || !this.$x.$timezone) return s.call(this, t2, e2); var n2 = o(this.format("YYYY-MM-DD HH:mm:ss:SSS"), { locale: this.$L }); return s.call(n2, t2, e2).tz(this.$x.$timezone, true); }, o.tz = function(t2, e2, n2) { var i2 = n2 && e2, a2 = n2 || e2 || r, f2 = u(+o(), a2); if ("string" != typeof t2) return o(t2).tz(a2); var s2 = (function(t3, e3, n3) { var i3 = t3 - 60 * e3 * 1e3, o2 = u(i3, n3); if (e3 === o2) return [i3, e3]; var r2 = u(i3 -= 60 * (o2 - e3) * 1e3, n3); return o2 === r2 ? [i3, o2] : [t3 - 60 * Math.min(o2, r2) * 1e3, Math.max(o2, r2)]; })(o.utc(t2, i2).valueOf(), f2, a2), m = s2[0], c = s2[1], d = o(m).utcOffset(c); return d.$x.$timezone = a2, d; }, o.tz.guess = function() { return Intl.DateTimeFormat().resolvedOptions().timeZone; }, o.tz.setDefault = function(t2) { r = t2; }; }; })); } }); // node_modules/dayjs/plugin/isoWeek.js var require_isoWeek = __commonJS({ "node_modules/dayjs/plugin/isoWeek.js"(exports, module) { !(function(e, t) { "object" == typeof exports && "undefined" != typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define(t) : (e = "undefined" != typeof globalThis ? globalThis : e || self).dayjs_plugin_isoWeek = t(); })(exports, (function() { "use strict"; var e = "day"; return function(t, i, s) { var a = function(t2) { return t2.add(4 - t2.isoWeekday(), e); }, d = i.prototype; d.isoWeekYear = function() { return a(this).year(); }, d.isoWeek = function(t2) { if (!this.$utils().u(t2)) return this.add(7 * (t2 - this.isoWeek()), e); var i2, d2, n2, o, r = a(this), u = (i2 = this.isoWeekYear(), d2 = this.$u, n2 = (d2 ? s.utc : s)().year(i2).startOf("year"), o = 4 - n2.isoWeekday(), n2.isoWeekday() > 4 && (o += 7), n2.add(o, e)); return r.diff(u, "week") + 1; }, d.isoWeekday = function(e2) { return this.$utils().u(e2) ? this.day() || 7 : this.day(this.day() % 7 ? e2 : e2 - 7); }; var n = d.startOf; d.startOf = function(e2, t2) { var i2 = this.$utils(), s2 = !!i2.u(t2) || t2; return "isoweek" === i2.p(e2) ? s2 ? this.date(this.date() - (this.isoWeekday() - 1)).startOf("day") : this.date(this.date() - 1 - (this.isoWeekday() - 1) + 7).endOf("day") : n.bind(this)(e2, t2); }; }; })); } }); // node_modules/@novadi/core/dist/token.js var tokenCounter = 0; function Token(description) { const id = ++tokenCounter; const sym = /* @__PURE__ */ Symbol(description ? `Token(${description})` : `Token#${id}`); const token2 = { symbol: sym, description, toString() { return description ? `Token<${description}>` : `Token<#${id}>`; } }; return token2; } // node_modules/@novadi/core/dist/errors.js var ContainerError = class extends Error { constructor(message) { super(message); this.name = "ContainerError"; } }; var BindingNotFoundError = class extends ContainerError { constructor(tokenDescription, path = []) { const pathStr = path.length > 0 ? ` Dependency path: ${path.join(" -> ")}` : ""; super(`Token "${tokenDescription}" is not bound or registered in the container.${pathStr}`); this.name = "BindingNotFoundError"; } }; var CircularDependencyError = class extends ContainerError { constructor(path) { super(`Circular dependency detected: ${path.join(" -> ")}`); this.name = "CircularDependencyError"; } }; // node_modules/@novadi/core/dist/autowire.js var paramNameCache = /* @__PURE__ */ new WeakMap(); function extractParameterNames(constructor) { const cached = paramNameCache.get(constructor); if (cached) { return cached; } const fnStr = constructor.toString(); const match = fnStr.match(/constructor\s*\(([^)]*)\)/) || fnStr.match(/^[^(]*\(([^)]*)\)/); if (!match || !match[1]) { return []; } const params = match[1].split(",").map((param) => param.trim()).filter((param) => param.length > 0).map((param) => { let name = param.split(/[:=]/)[0].trim(); name = name.replace(/^((public|private|protected|readonly)\s+)+/, ""); if (name.includes("{") || name.includes("[")) { return null; } return name; }).filter((name) => name !== null); paramNameCache.set(constructor, params); return params; } function resolveByMap(constructor, container, options) { if (!options.map) { throw new Error("AutoWire map strategy requires options.map to be defined"); } const paramNames = extractParameterNames(constructor); const resolvedDeps = []; for (const paramName of paramNames) { const resolver = options.map[paramName]; if (resolver === void 0) { if (options.strict) { throw new Error(`Cannot resolve parameter "${paramName}" on ${constructor.name}. Not found in autowire map. Add it to the map: .autoWire({ map: { ${paramName}: ... } })`); } else { resolvedDeps.push(void 0); } continue; } if (typeof resolver === "function") { resolvedDeps.push(resolver(container)); } else { resolvedDeps.push(container.resolve(resolver)); } } return resolvedDeps; } function resolveByMapResolvers(_constructor, container, options) { if (!options.mapResolvers || options.mapResolvers.length === 0) { return []; } const resolvedDeps = []; for (let i = 0; i < options.mapResolvers.length; i++) { const resolver = options.mapResolvers[i]; if (resolver === void 0) { resolvedDeps.push(void 0); } else if (typeof resolver === "function") { resolvedDeps.push(resolver(container)); } else { resolvedDeps.push(container.resolve(resolver)); } } return resolvedDeps; } function autowire(constructor, container, options) { const opts = { by: "paramName", strict: false, ...options }; if (opts.mapResolvers && opts.mapResolvers.length > 0) { return resolveByMapResolvers(constructor, container, opts); } if (opts.map && Object.keys(opts.map).length > 0) { return resolveByMap(constructor, container, opts); } return []; } // node_modules/@novadi/core/dist/builder.js var RegistrationBuilder = class { constructor(pending, registrations) { this.registrations = registrations; this.configs = []; this.defaultLifetime = "singleton"; this.pending = pending; } /** * Bind this registration to a token or interface type * * @overload * @param {Token} token - Explicit token for binding * * @overload * @param {string} typeName - Interface type name (auto-generated by transformer) */ as(tokenOrTypeName) { if (tokenOrTypeName && typeof tokenOrTypeName === "object" && "symbol" in tokenOrTypeName) { const config = { token: tokenOrTypeName, type: this.pending.type, value: this.pending.value, factory: this.pending.factory, constructor: this.pending.constructor, lifetime: this.defaultLifetime }; this.configs.push(config); this.registrations.push(config); return this; } else { const config = { token: null, // Will be set during build() type: this.pending.type, value: this.pending.value, factory: this.pending.factory, constructor: this.pending.constructor, lifetime: this.defaultLifetime, interfaceType: tokenOrTypeName }; this.configs.push(config); this.registrations.push(config); return this; } } /** * Register as default implementation for an interface * Combines as() + asDefault() */ asDefaultInterface(typeName) { this.as("TInterface", typeName); return this.asDefault(); } /** * Register as a keyed interface implementation * Combines as() + keyed() */ asKeyedInterface(key, typeName) { this.as("TInterface", typeName); return this.keyed(key); } /** * Register as multiple implemented interfaces */ asImplementedInterfaces(tokens) { if (tokens.length === 0) { return this; } if (this.configs.length > 0) { for (const config of this.configs) { config.lifetime = "singleton"; config.additionalTokens = config.additionalTokens || []; config.additionalTokens.push(...tokens); } return this; } const firstConfig = { token: tokens[0], type: this.pending.type, value: this.pending.value, factory: this.pending.factory, constructor: this.pending.constructor, lifetime: "singleton" }; this.configs.push(firstConfig); this.registrations.push(firstConfig); for (let i = 1; i < tokens.length; i++) { firstConfig.additionalTokens = firstConfig.additionalTokens || []; firstConfig.additionalTokens.push(tokens[i]); } return this; } /** * Set singleton lifetime (one instance for entire container) */ singleInstance() { for (const config of this.configs) { config.lifetime = "singleton"; } return this; } /** * Set per-request lifetime (one instance per resolve call tree) */ instancePerRequest() { for (const config of this.configs) { config.lifetime = "per-request"; } return this; } /** * Set transient lifetime (new instance every time) * Alias for default behavior */ instancePerDependency() { for (const config of this.configs) { config.lifetime = "transient"; } return this; } /** * Name this registration for named resolution */ named(name) { for (const config of this.configs) { config.name = name; } return this; } /** * Key this registration for keyed resolution */ keyed(key) { for (const config of this.configs) { config.key = key; } return this; } /** * Mark this as default registration * Default registrations don't override existing ones */ asDefault() { for (const config of this.configs) { config.isDefault = true; } return this; } /** * Only register if token not already registered */ ifNotRegistered() { for (const config of this.configs) { config.ifNotRegistered = true; } return this; } /** * Specify parameter values for constructor (primitives and constants) * Use this for non-DI parameters like strings, numbers, config values */ withParameters(parameters) { for (const config of this.configs) { config.parameterValues = parameters; } return this; } /** * Enable automatic dependency injection (autowiring) * Supports three strategies: paramName (default), map, and class * * @example * ```ts * // Strategy 1: paramName (default, requires non-minified code in dev) * builder.registerType(EventBus).as().autoWire() * * // Strategy 2: map (minify-safe, explicit) * builder.registerType(EventBus).as().autoWire({ * map: { * logger: (c) => c.resolveType() * } * }) * * // Strategy 3: class (requires build-time codegen) * builder.registerType(EventBus).as().autoWire({ by: 'class' }) * ``` */ autoWire(options) { for (const config of this.configs) { config.autowireOptions = options || { by: "paramName", strict: false }; } return this; } }; var Builder = class { constructor(baseContainer) { this.baseContainer = baseContainer; this.registrations = []; } /** * Register a class constructor */ registerType(constructor) { const pending = { type: "type", value: null, constructor }; return new RegistrationBuilder(pending, this.registrations); } /** * Register a pre-created instance */ registerInstance(instance) { const pending = { type: "instance", value: instance, constructor: void 0 }; return new RegistrationBuilder(pending, this.registrations); } /** * Register a factory function */ register(factory) { const pending = { type: "factory", value: null, factory, constructor: void 0 }; return new RegistrationBuilder(pending, this.registrations); } /** * Register a module (function that adds multiple registrations) */ module(moduleFunc) { moduleFunc(this); return this; } /** * Resolve interface type names to tokens * @internal */ resolveInterfaceTokens(container) { for (const config of this.registrations) { if (config.interfaceType !== void 0 && !config.token) { config.token = container.interfaceToken(config.interfaceType); } } } /** * Identify tokens that have non-default registrations * @internal */ identifyNonDefaultTokens() { const tokensWithNonDefaults = /* @__PURE__ */ new Set(); for (const config of this.registrations) { if (!config.isDefault && !config.name && config.key === void 0) { tokensWithNonDefaults.add(config.token); } } return tokensWithNonDefaults; } /** * Check if registration should be skipped * @internal */ shouldSkipRegistration(config, tokensWithNonDefaults, registeredTokens) { if (config.isDefault && !config.name && config.key === void 0 && tokensWithNonDefaults.has(config.token)) { return true; } if (config.ifNotRegistered && registeredTokens.has(config.token)) { return true; } if (config.isDefault && registeredTokens.has(config.token)) { return true; } return false; } /** * Create binding token for registration (named, keyed, or multi) * @internal */ createBindingToken(config, namedRegistrations, keyedRegistrations, multiRegistrations) { if (config.name) { const bindingToken = Token(`__named_${config.name}`); namedRegistrations.set(config.name, { ...config, token: bindingToken }); return bindingToken; } else if (config.key !== void 0) { const keyStr = typeof config.key === "symbol" ? config.key.toString() : config.key; const bindingToken = Token(`__keyed_${keyStr}`); keyedRegistrations.set(config.key, { ...config, token: bindingToken }); return bindingToken; } else { if (multiRegistrations.has(config.token)) { const bindingToken = Token(`__multi_${config.token.toString()}_${multiRegistrations.get(config.token).length}`); multiRegistrations.get(config.token).push(bindingToken); return bindingToken; } else { multiRegistrations.set(config.token, [config.token]); return config.token; } } } /** * Register additional interfaces for a config * @internal */ registerAdditionalInterfaces(container, config, bindingToken, registeredTokens) { if (config.additionalTokens) { for (const additionalToken of config.additionalTokens) { container.bindFactory(additionalToken, (c) => c.resolve(bindingToken), { lifetime: config.lifetime }); registeredTokens.add(additionalToken); } } } /** * Build the container with all registered bindings */ build() { const container = this.baseContainer.createChild(); this.resolveInterfaceTokens(container); const registeredTokens = /* @__PURE__ */ new Set(); const namedRegistrations = /* @__PURE__ */ new Map(); const keyedRegistrations = /* @__PURE__ */ new Map(); const multiRegistrations = /* @__PURE__ */ new Map(); const tokensWithNonDefaults = this.identifyNonDefaultTokens(); for (const config of this.registrations) { if (this.shouldSkipRegistration(config, tokensWithNonDefaults, registeredTokens)) { continue; } const bindingToken = this.createBindingToken(config, namedRegistrations, keyedRegistrations, multiRegistrations); this.applyRegistration(container, { ...config, token: bindingToken }); registeredTokens.add(config.token); this.registerAdditionalInterfaces(container, config, bindingToken, registeredTokens); } ; container.__namedRegistrations = namedRegistrations; container.__keyedRegistrations = keyedRegistrations; container.__multiRegistrations = multiRegistrations; return container; } /** * Analyze constructor to detect dependencies * @internal */ analyzeConstructor(constructor) { const constructorStr = constructor.toString(); const hasDependencies = /constructor\s*\([^)]+\)/.test(constructorStr); return { hasDependencies }; } /** * Create optimized factory for zero-dependency constructors * @internal */ createOptimizedFactory(container, config, options) { if (config.lifetime === "singleton") { const instance = new config.constructor(); container.bindValue(config.token, instance); } else if (config.lifetime === "transient") { const ctor = config.constructor; const fastFactory = () => new ctor(); container.fastTransientCache.set(config.token, fastFactory); container.bindFactory(config.token, fastFactory, options); } else { const factory = () => new config.constructor(); container.bindFactory(config.token, factory, options); } } /** * Create autowire factory * @internal */ createAutoWireFactory(container, config, options) { const factory = (c) => { const resolvedDeps = autowire(config.constructor, c, config.autowireOptions); return new config.constructor(...resolvedDeps); }; container.bindFactory(config.token, factory, options); } /** * Create withParameters factory * @internal */ createParameterFactory(container, config, options) { const factory = () => { const values = Object.values(config.parameterValues); return new config.constructor(...values); }; container.bindFactory(config.token, factory, options); } /** * Apply type registration (class constructor) * @internal */ applyTypeRegistration(container, config, options) { const { hasDependencies } = this.analyzeConstructor(config.constructor); if (!hasDependencies && !config.autowireOptions && !config.parameterValues) { this.createOptimizedFactory(container, config, options); return; } if (config.autowireOptions) { this.createAutoWireFactory(container, config, options); return; } if (config.parameterValues) { this.createParameterFactory(container, config, options); return; } if (hasDependencies) { const className = config.constructor.name || "UnnamedClass"; throw new Error(`Service "${className}" has constructor dependencies but no autowiring configuration. Solutions: 1. \u2B50 Use the NovaDI transformer (recommended): - Add "@novadi/core/unplugin" to your build config - Transformer automatically generates .autoWire() for all dependencies 2. Add manual autowiring: .autoWire({ map: { /* param: resolver */ } }) 3. Use a factory function: .register((c) => new ${className}(...)) See docs: https://github.com/janus007/NovaDI#autowire`); } const factory = () => new config.constructor(); container.bindFactory(config.token, factory, options); } applyRegistration(container, config) { const options = { lifetime: config.lifetime }; switch (config.type) { case "instance": container.bindValue(config.token, config.value); break; case "factory": container.bindFactory(config.token, config.factory, options); break; case "type": this.applyTypeRegistration(container, config, options); break; } } }; // node_modules/@novadi/core/dist/container.js function isDisposable(obj) { return obj && typeof obj.dispose === "function"; } var ResolutionContext = class { constructor() { this.resolvingStack = /* @__PURE__ */ new Set(); this.perRequestCache = /* @__PURE__ */ new Map(); } isResolving(token2) { return this.resolvingStack.has(token2); } enterResolve(token2) { this.resolvingStack.add(token2); } exitResolve(token2) { this.resolvingStack.delete(token2); this.path = void 0; } getPath() { if (!this.path) { this.path = Array.from(this.resolvingStack).map((t) => t.toString()); } return [...this.path]; } cachePerRequest(token2, instance) { this.perRequestCache.set(token2, instance); } getPerRequest(token2) { return this.perRequestCache.get(token2); } hasPerRequest(token2) { return this.perRequestCache.has(token2); } /** * Reset context for reuse in object pool * Performance: Reusing contexts avoids heap allocations */ reset() { this.resolvingStack.clear(); this.perRequestCache.clear(); this.path = void 0; } }; var ResolutionContextPool = class { constructor() { this.pool = []; this.maxSize = 10; } acquire() { const context = this.pool.pop(); if (context) { context.reset(); return context; } return new ResolutionContext(); } release(context) { if (this.pool.length < this.maxSize) { this.pool.push(context); } } }; var Container = class _Container { constructor(parent) { this.bindings = /* @__PURE__ */ new Map(); this.singletonCache = /* @__PURE__ */ new Map(); this.singletonOrder = []; this.interfaceRegistry = /* @__PURE__ */ new Map(); this.interfaceTokenCache = /* @__PURE__ */ new Map(); this.fastTransientCache = /* @__PURE__ */ new Map(); this.ultraFastSingletonCache = /* @__PURE__ */ new Map(); this.parent = parent; } /** * Bind a pre-created value to a token */ bindValue(token2, value) { this.bindings.set(token2, { type: "value", lifetime: "singleton", value, constructor: void 0 }); this.invalidateBindingCache(); } /** * Bind a factory function to a token */ bindFactory(token2, factory, options) { this.bindings.set(token2, { type: "factory", lifetime: options?.lifetime || "transient", factory, dependencies: options?.dependencies, constructor: void 0 }); this.invalidateBindingCache(); } /** * Bind a class constructor to a token */ bindClass(token2, constructor, options) { const binding = { type: "class", lifetime: options?.lifetime || "transient", constructor, dependencies: options?.dependencies }; this.bindings.set(token2, binding); this.invalidateBindingCache(); if (binding.lifetime === "transient" && (!binding.dependencies || binding.dependencies.length === 0)) { this.fastTransientCache.set(token2, () => new constructor()); } } /** * Resolve a dependency synchronously * Performance optimized with multiple fast paths */ resolve(token2) { const cached = this.tryGetFromCaches(token2); if (cached !== void 0) { return cached; } if (this.currentContext) { return this.resolveWithContext(token2, this.currentContext); } const context = _Container.contextPool.acquire(); this.currentContext = context; try { return this.resolveWithContext(token2, context); } finally { this.currentContext = void 0; _Container.contextPool.release(context); } } /** * SPECIALIZED: Ultra-fast singleton resolve (no safety checks) * Use ONLY when you're 100% sure the token is a registered singleton * @internal For performance-critical paths only */ resolveSingletonUnsafe(token2) { return this.ultraFastSingletonCache.get(token2) ?? this.singletonCache.get(token2); } /** * SPECIALIZED: Fast transient resolve for zero-dependency classes * Skips all context creation and circular dependency checks * @internal For performance-critical paths only */ resolveTransientSimple(token2) { const factory = this.fastTransientCache.get(token2); if (factory) { return factory(); } return this.resolve(token2); } /** * SPECIALIZED: Batch resolve multiple dependencies at once * More efficient than multiple individual resolves */ resolveBatch(tokens) { const wasResolving = !!this.currentContext; const context = this.currentContext || _Container.contextPool.acquire(); if (!wasResolving) { this.currentContext = context; } try { const results = tokens.map((token2) => { const cached = this.tryGetFromCaches(token2); if (cached !== void 0) return cached; return this.resolveWithContext(token2, context); }); return results; } finally { if (!wasResolving) { this.currentContext = void 0; _Container.contextPool.release(context); } } } /** * Resolve a dependency asynchronously (supports async factories) */ async resolveAsync(token2) { if (this.currentContext) { return this.resolveAsyncWithContext(token2, this.currentContext); } const context = _Container.contextPool.acquire(); this.currentContext = context; try { return await this.resolveAsyncWithContext(token2, context); } finally { this.currentContext = void 0; _Container.contextPool.release(context); } } /** * Try to get instance from all cache levels * Returns undefined if not cached * @internal */ tryGetFromCaches(token2) { const ultraFast = this.ultraFastSingletonCache.get(token2); if (ultraFast !== void 0) { return ultraFast; } if (this.singletonCache.has(token2)) { const cached = this.singletonCache.get(token2); this.ultraFastSingletonCache.set(token2, cached); return cached; } const fastFactory = this.fastTransientCache.get(token2); if (fastFactory) { return fastFactory(); } return void 0; } /** * Cache instance based on lifetime strategy * @internal */ cacheInstance(token2, instance, lifetime, context) { if (lifetime === "singleton") { this.singletonCache.set(token2, instance); this.singletonOrder.push(token2); this.ultraFastSingletonCache.set(token2, instance); } else if (lifetime === "per-request" && context) { context.cachePerRequest(token2, instance); } } /** * Validate and get binding with circular dependency check * Returns binding or throws error * @internal */ validateAndGetBinding(token2, context) { if (context.isResolving(token2)) { throw new CircularDependencyError([...context.getPath(), token2.toString()]); } const binding = this.getBinding(token2); if (!binding) { throw new BindingNotFoundError(token2.toString(), context.getPath()); } return binding; } /** * Instantiate from binding synchronously * @internal */ instantiateBindingSync(binding, token2, context) { switch (binding.type) { case "value": return binding.value; case "factory": const result = binding.factory(this); if (result instanceof Promise) { throw new Error(`Async factory detected for ${token2.toString()}. Use resolveAsync() instead.`); } return result; case "class": const deps = binding.dependencies || []; const resolvedDeps = deps.map((dep) => this.resolveWithContext(dep, context)); return new binding.constructor(...resolvedDeps); case "inline-class": return new binding.constructor(); default: throw new Error(`Unknown binding type: ${binding.type}`); } } /** * Instantiate from binding asynchronously * @internal */ async instantiateBindingAsync(binding, context) { switch (binding.type) { case "value": return binding.value; case "factory": return await Promise.resolve(binding.factory(this)); case "class": const deps = binding.dependencies || []; const resolvedDeps = await Promise.all(deps.map((dep) => this.resolveAsyncWithContext(dep, context))); return new binding.constructor(...resolvedDeps); case "inline-class": return new binding.constructor(); default: throw new Error(`Unknown binding type: ${binding.type}`); } } /** * Create a child container that inherits bindings from this container */ createChild() { return new _Container(this); } /** * Dispose all singleton instances in reverse registration order */ async dispose() { const errors = []; for (let i = this.singletonOrder.length - 1; i >= 0; i--) { const token2 = this.singletonOrder[i]; const instance = this.singletonCache.get(token2); if (instance && isDisposable(instance)) { try { await instance.dispose(); } catch (error) { errors.push(error); } } } this.singletonCache.clear(); this.singletonOrder.length = 0; } /** * Create a fluent builder for registering dependencies */ builder() { return new Builder(this); } /** * Resolve a named service */ resolveNamed(name) { const namedRegistrations = this.__namedRegistrations; if (!namedRegistrations) { throw new Error(`Named service "${name}" not found. No named registrations exist.`); } const config = namedRegistrations.get(name); if (!config) { throw new Error(`Named service "${name}" not found`); } return this.resolve(config.token); } /** * Resolve a keyed service */ resolveKeyed(key) { const keyedRegistrations = this.__keyedRegistrations; if (!keyedRegistrations) { throw new Error(`Keyed service not found. No keyed registrations exist.`); } const config = keyedRegistrations.get(key); if (!config) { const keyStr = typeof key === "symbol" ? key.toString() : `"${key}"`; throw new Error(`Keyed service ${keyStr} not found`); } return this.resolve(config.token); } /** * Resolve all registrations for a token */ resolveAll(token2) { const multiRegistrations = this.__multiRegistrations; if (!multiRegistrations) { return []; } const tokens = multiRegistrations.get(token2); if (!tokens || tokens.length === 0) { return []; } return tokens.map((t) => this.resolve(t)); } /** * Get registry information for debugging/visualization * Returns array of binding information */ getRegistry() { const registry = []; this.bindings.forEach((binding, token2) => { registry.push({ token: token2.description || token2.symbol.toString(), type: binding.type, lifetime: binding.lifetime, dependencies: binding.dependencies?.map((d) => d.description || d.symbol.toString()) }); }); return registry; } /** * Get or create a token for an interface type * Uses a type name hash as key for the interface registry */ interfaceToken(typeName) { const key = typeName || `Interface_${Math.random().toString(36).substr(2, 9)}`; if (this.interfaceRegistry.has(key)) { return this.interfaceRegistry.get(key); } if (this.parent) { const parentToken = this.parent.interfaceToken(key); return parentToken; } const token2 = Token(key); this.interfaceRegistry.set(key, token2); return token2; } /** * Resolve a dependency by interface type without explicit token */ resolveType(typeName) { const key = typeName || ""; let token2 = this.interfaceTokenCache.get(key); if (!token2) { token2 = this.interfaceToken(typeName); this.interfaceTokenCache.set(key, token2); } return this.resolve(token2); } /** * Resolve a keyed interface */ resolveTypeKeyed(key, _typeName) { return this.resolveKeyed(key); } /** * Resolve all registrations for an interface type */ resolveTypeAll(typeName) { const token2 = this.interfaceToken(typeName); return this.resolveAll(token2); } /** * Internal: Resolve with context for circular dependency detection */ resolveWithContext(token2, context) { const binding = this.validateAndGetBinding(token2, context); if (binding.lifetime === "per-request" && context.hasPerRequest(token2)) { return context.getPerRequest(token2); } if (binding.lifetime === "singleton" && this.singletonCache.has(token2)) { return this.singletonCache.get(token2); } context.enterResolve(token2); try { const instance = this.instantiateBindingSync(binding, token2, context); this.cacheInstance(token2, instance, binding.lifetime, context); return instance; } finally { context.exitResolve(token2); } } /** * Internal: Async resolve with context */ async resolveAsyncWithContext(token2, context) { const binding = this.validateAndGetBinding(token2, context); if (binding.lifetime === "per-request" && context.hasPerRequest(token2)) { return context.getPerRequest(token2); } if (binding.lifetime === "singleton" && this.singletonCache.has(token2)) { return this.singletonCache.get(token2); } context.enterResolve(token2); try { const instance = await this.instantiateBindingAsync(binding, context); this.cacheInstance(token2, instance, binding.lifetime, context); return instance; } finally { context.exitResolve(token2); } } /** * Get binding from this container or parent chain * Performance optimized: Uses flat cache to avoid recursive parent lookups */ getBinding(token2) { if (!this.bindingCache) { this.buildBindingCache(); } return this.bindingCache.get(token2); } /** * Build flat cache of all bindings including parent chain * This converts O(n) parent chain traversal to O(1) lookup */ buildBindingCache() { this.bindingCache = /* @__PURE__ */ new Map(); let current = this; while (current) { current.bindings.forEach((binding, token2) => { if (!this.bindingCache.has(token2)) { this.bindingCache.set(token2, binding); } }); current = current.parent; } } /** * Invalidate binding cache when new bindings are added * Called by bindValue, bindFactory, bindClass */ invalidateBindingCache() { this.bindingCache = void 0; this.ultraFastSingletonCache.clear(); } }; Container.contextPool = new ResolutionContextPool(); // node_modules/calendar/dist/chunk-OPIZ4QQE.js var BaseGroupingRenderer = class { /** * Main render method - handles common logic */ async render(context) { const allowedIds = context.filter[this.type] || []; if (allowedIds.length === 0) return; const entities = await this.getEntities(allowedIds); const dateCount = context.filter["date"]?.length || 1; const childIds = context.childType ? context.filter[context.childType] || [] : []; for (const entity of entities) { const entityChildIds = context.parentChildMap?.[entity.id] || []; const childCount = entityChildIds.filter((id) => childIds.includes(id)).length; const colspan = childCount * dateCount; const header = document.createElement(this.config.elementTag); header.dataset[this.config.idAttribute] = entity.id; header.style.setProperty(this.config.colspanVar, String(colspan)); this.renderHeader(entity, header, context); context.headerContainer.appendChild(header); } } /** * Override this method for custom header rendering * Default: just sets textContent to display name */ renderHeader(entity, header, _context) { header.textContent = this.getDisplayName(entity); } /** * Helper to render a single entity header. * Can be used by subclasses that override render() but want consistent header creation. */ createHeader(entity, context) { const header = document.createElement(this.config.elementTag); header.dataset[this.config.idAttribute] = entity.id; this.renderHeader(entity, header, context); return header; } }; // node_modules/calendar/dist/chunk-CMOI3H5F.js var CoreEvents = { // Lifecycle events INITIALIZED: "core:initialized", READY: "core:ready", DESTROYED: "core:destroyed", // View events VIEW_CHANGED: "view:changed", VIEW_RENDERED: "view:rendered", // Navigation events DATE_CHANGED: "nav:date-changed", NAVIGATION_COMPLETED: "nav:navigation-completed", // Data events DATA_LOADING: "data:loading", DATA_LOADED: "data:loaded", DATA_ERROR: "data:error", // Grid events GRID_RENDERED: "grid:rendered", GRID_CLICKED: "grid:clicked", // Event management EVENT_CREATED: "event:created", EVENT_UPDATED: "event:updated", EVENT_DELETED: "event:deleted", EVENT_SELECTED: "event:selected", // Event drag-drop EVENT_DRAG_START: "event:drag-start", EVENT_DRAG_MOVE: "event:drag-move", EVENT_DRAG_END: "event:drag-end", EVENT_DRAG_CANCEL: "event:drag-cancel", EVENT_DRAG_COLUMN_CHANGE: "event:drag-column-change", // Header drag (timed → header conversion) EVENT_DRAG_ENTER_HEADER: "event:drag-enter-header", EVENT_DRAG_MOVE_HEADER: "event:drag-move-header", EVENT_DRAG_LEAVE_HEADER: "event:drag-leave-header", // Event resize EVENT_RESIZE_START: "event:resize-start", EVENT_RESIZE_END: "event:resize-end", // Edge scroll EDGE_SCROLL_TICK: "edge-scroll:tick", EDGE_SCROLL_STARTED: "edge-scroll:started", EDGE_SCROLL_STOPPED: "edge-scroll:stopped", // System events ERROR: "system:error", // Sync events SYNC_STARTED: "sync:started", SYNC_COMPLETED: "sync:completed", SYNC_FAILED: "sync:failed", // Entity events - for audit and sync ENTITY_SAVED: "entity:saved", ENTITY_DELETED: "entity:deleted", // Audit events AUDIT_LOGGED: "audit:logged", // Rendering events EVENTS_RENDERED: "events:rendered" }; var SyncPlugin = class { constructor(service) { this.service = service; } /** * Mark entity as successfully synced */ async markAsSynced(id) { const entity = await this.service.get(id); if (entity) { entity.syncStatus = "synced"; await this.service.save(entity); } } /** * Mark entity as sync error */ async markAsError(id) { const entity = await this.service.get(id); if (entity) { entity.syncStatus = "error"; await this.service.save(entity); } } /** * Get current sync status for an entity */ async getSyncStatus(id) { const entity = await this.service.get(id); return entity ? entity.syncStatus : null; } /** * Get entities by sync status using IndexedDB index */ async getBySyncStatus(syncStatus) { return new Promise((resolve, reject) => { const transaction = this.service.db.transaction([this.service.storeName], "readonly"); const store = transaction.objectStore(this.service.storeName); const index = store.index("syncStatus"); const request = index.getAll(syncStatus); request.onsuccess = () => { const data = request.result; const entities = data.map((item) => this.service.deserialize(item)); resolve(entities); }; request.onerror = () => { reject(new Error(`Failed to get by sync status ${syncStatus}: ${request.error}`)); }; }); } }; function arrayDifference(first, second) { const secondSet = new Set(second); return first.filter((item) => !secondSet.has(item)); } function arrayIntersection(first, second) { const secondSet = new Set(second); return first.filter((item) => secondSet.has(item)); } function keyBy(arr, getKey2) { const result = {}; for (const item of arr) { result[String(getKey2(item))] = item; } return result; } function diff(oldObj, newObj, options = {}) { let { embeddedObjKeys } = options; const { keysToSkip, treatTypeChangeAsReplace } = options; if (embeddedObjKeys instanceof Map) { embeddedObjKeys = new Map( Array.from(embeddedObjKeys.entries()).map(([key, value]) => [ key instanceof RegExp ? key : key.replace(/^\./, ""), value ]) ); } else if (embeddedObjKeys) { embeddedObjKeys = Object.fromEntries( Object.entries(embeddedObjKeys).map(([key, value]) => [key.replace(/^\./, ""), value]) ); } return compare(oldObj, newObj, [], [], { embeddedObjKeys, keysToSkip: keysToSkip ?? [], treatTypeChangeAsReplace: treatTypeChangeAsReplace ?? true }); } var getTypeOfObj = (obj) => { if (typeof obj === "undefined") { return "undefined"; } if (obj === null) { return null; } return Object.prototype.toString.call(obj).match(/^\[object\s(.*)\]$/)[1]; }; var getKey = (path) => { const left = path[path.length - 1]; return left != null ? left : "$root"; }; var compare = (oldObj, newObj, path, keyPath, options) => { let changes = []; const currentPath = keyPath.join("."); if (options.keysToSkip?.some((skipPath) => { if (currentPath === skipPath) { return true; } if (skipPath.includes(".") && skipPath.startsWith(currentPath + ".")) { return false; } if (skipPath.includes(".")) { const skipParts = skipPath.split("."); const currentParts = currentPath.split("."); if (currentParts.length >= skipParts.length) { for (let i = 0; i < skipParts.length; i++) { if (skipParts[i] !== currentParts[i]) { return false; } } return true; } } return false; })) { return changes; } const typeOfOldObj = getTypeOfObj(oldObj); const typeOfNewObj = getTypeOfObj(newObj); if (options.treatTypeChangeAsReplace && typeOfOldObj !== typeOfNewObj) { if (typeOfOldObj !== "undefined") { changes.push({ type: "REMOVE", key: getKey(path), value: oldObj }); } if (typeOfNewObj !== "undefined") { changes.push({ type: "ADD", key: getKey(path), value: newObj }); } return changes; } if (typeOfNewObj === "undefined" && typeOfOldObj !== "undefined") { changes.push({ type: "REMOVE", key: getKey(path), value: oldObj }); return changes; } if (typeOfNewObj === "Object" && typeOfOldObj === "Array") { changes.push({ type: "UPDATE", key: getKey(path), value: newObj, oldValue: oldObj }); return changes; } if (typeOfNewObj === null) { if (typeOfOldObj !== null) { changes.push({ type: "UPDATE", key: getKey(path), value: newObj, oldValue: oldObj }); } return changes; } switch (typeOfOldObj) { case "Date": if (typeOfNewObj === "Date") { changes = changes.concat( comparePrimitives(oldObj.getTime(), newObj.getTime(), path).map((x) => ({ ...x, value: new Date(x.value), oldValue: new Date(x.oldValue) })) ); } else { changes = changes.concat(comparePrimitives(oldObj, newObj, path)); } break; case "Object": { const diffs = compareObject(oldObj, newObj, path, keyPath, false, options); if (diffs.length) { if (path.length) { changes.push({ type: "UPDATE", key: getKey(path), changes: diffs }); } else { changes = changes.concat(diffs); } } break; } case "Array": changes = changes.concat(compareArray(oldObj, newObj, path, keyPath, options)); break; case "Function": break; default: changes = changes.concat(comparePrimitives(oldObj, newObj, path)); } return changes; }; var compareObject = (oldObj, newObj, path, keyPath, skipPath = false, options = {}) => { let k; let newKeyPath; let newPath; if (skipPath == null) { skipPath = false; } let changes = []; const oldObjKeys = Object.keys(oldObj); const newObjKeys = Object.keys(newObj); const intersectionKeys = arrayIntersection(oldObjKeys, newObjKeys); for (k of intersectionKeys) { newPath = path.concat([k]); newKeyPath = skipPath ? keyPath : keyPath.concat([k]); const diffs = compare(oldObj[k], newObj[k], newPath, newKeyPath, options); if (diffs.length) { changes = changes.concat(diffs); } } const addedKeys = arrayDifference(newObjKeys, oldObjKeys); for (k of addedKeys) { newPath = path.concat([k]); newKeyPath = skipPath ? keyPath : keyPath.concat([k]); const currentPath = newKeyPath.join("."); if (options.keysToSkip?.some((skipPath2) => currentPath === skipPath2 || currentPath.startsWith(skipPath2 + "."))) { continue; } changes.push({ type: "ADD", key: getKey(newPath), value: newObj[k] }); } const deletedKeys = arrayDifference(oldObjKeys, newObjKeys); for (k of deletedKeys) { newPath = path.concat([k]); newKeyPath = skipPath ? keyPath : keyPath.concat([k]); const currentPath = newKeyPath.join("."); if (options.keysToSkip?.some((skipPath2) => currentPath === skipPath2 || currentPath.startsWith(skipPath2 + "."))) { continue; } changes.push({ type: "REMOVE", key: getKey(newPath), value: oldObj[k] }); } return changes; }; var compareArray = (oldObj, newObj, path, keyPath, options) => { if (getTypeOfObj(newObj) !== "Array") { return [{ type: "UPDATE", key: getKey(path), value: newObj, oldValue: oldObj }]; } const left = getObjectKey(options.embeddedObjKeys, keyPath); const uniqKey = left != null ? left : "$index"; const indexedOldObj = convertArrayToObj(oldObj, uniqKey); const indexedNewObj = convertArrayToObj(newObj, uniqKey); const diffs = compareObject(indexedOldObj, indexedNewObj, path, keyPath, true, options); if (diffs.length) { return [ { type: "UPDATE", key: getKey(path), embeddedKey: typeof uniqKey === "function" && uniqKey.length === 2 ? uniqKey(newObj[0], true) : uniqKey, changes: diffs } ]; } else { return []; } }; var getObjectKey = (embeddedObjKeys, keyPath) => { if (embeddedObjKeys != null) { const path = keyPath.join("."); if (embeddedObjKeys instanceof Map) { for (const [key2, value] of embeddedObjKeys.entries()) { if (key2 instanceof RegExp) { if (path.match(key2)) { return value; } } else if (path === key2) { return value; } } } const key = embeddedObjKeys[path]; if (key != null) { return key; } } return void 0; }; var convertArrayToObj = (arr, uniqKey) => { let obj = {}; if (uniqKey === "$value") { arr.forEach((value) => { obj[value] = value; }); } else if (uniqKey !== "$index") { const keyFunction = typeof uniqKey === "string" ? (item) => item[uniqKey] : uniqKey; obj = keyBy(arr, keyFunction); } else { for (let i = 0; i < arr.length; i++) { const value = arr[i]; obj[i] = value; } } return obj; }; var comparePrimitives = (oldObj, newObj, path) => { const changes = []; if (oldObj !== newObj) { changes.push({ type: "UPDATE", key: getKey(path), value: newObj, oldValue: oldObj }); } return changes; }; var BaseEntityService = class { constructor(context, eventBus) { this.context = context; this.eventBus = eventBus; this.syncPlugin = new SyncPlugin(this); } get db() { return this.context.getDatabase(); } /** * Serialize entity before storing in IndexedDB */ serialize(entity) { return entity; } /** * Deserialize data from IndexedDB back to entity */ deserialize(data) { return data; } /** * Get a single entity by ID */ async get(id) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], "readonly"); const store = transaction.objectStore(this.storeName); const request = store.get(id); request.onsuccess = () => { const data = request.result; resolve(data ? this.deserialize(data) : null); }; request.onerror = () => { reject(new Error(`Failed to get ${this.entityType} ${id}: ${request.error}`)); }; }); } /** * Get all entities */ async getAll() { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], "readonly"); const store = transaction.objectStore(this.storeName); const request = store.getAll(); request.onsuccess = () => { const data = request.result; const entities = data.map((item) => this.deserialize(item)); resolve(entities); }; request.onerror = () => { reject(new Error(`Failed to get all ${this.entityType}s: ${request.error}`)); }; }); } /** * Save an entity (create or update) * Emits ENTITY_SAVED event with operation type and changes (diff for updates) * @param entity - Entity to save * @param silent - If true, skip event emission (used for seeding) */ async save(entity, silent = false) { const entityId = entity.id; const existingEntity = await this.get(entityId); const isCreate = existingEntity === null; let changes; if (isCreate) { changes = entity; } else { const existingSerialized = this.serialize(existingEntity); const newSerialized = this.serialize(entity); changes = diff(existingSerialized, newSerialized); } const serialized = this.serialize(entity); return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], "readwrite"); const store = transaction.objectStore(this.storeName); const request = store.put(serialized); request.onsuccess = () => { if (!silent) { const payload = { entityType: this.entityType, entityId, operation: isCreate ? "create" : "update", changes, timestamp: Date.now() }; this.eventBus.emit(CoreEvents.ENTITY_SAVED, payload); } resolve(); }; request.onerror = () => { reject(new Error(`Failed to save ${this.entityType} ${entityId}: ${request.error}`)); }; }); } /** * Delete an entity * Emits ENTITY_DELETED event */ async delete(id) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], "readwrite"); const store = transaction.objectStore(this.storeName); const request = store.delete(id); request.onsuccess = () => { const payload = { entityType: this.entityType, entityId: id, operation: "delete", timestamp: Date.now() }; this.eventBus.emit(CoreEvents.ENTITY_DELETED, payload); resolve(); }; request.onerror = () => { reject(new Error(`Failed to delete ${this.entityType} ${id}: ${request.error}`)); }; }); } // Sync methods - delegate to SyncPlugin async markAsSynced(id) { return this.syncPlugin.markAsSynced(id); } async markAsError(id) { return this.syncPlugin.markAsError(id); } async getSyncStatus(id) { return this.syncPlugin.getSyncStatus(id); } async getBySyncStatus(syncStatus) { return this.syncPlugin.getBySyncStatus(syncStatus); } }; // node_modules/calendar/dist/index.js var import_dayjs = __toESM(require_dayjs_min(), 1); var import_utc = __toESM(require_utc(), 1); var import_timezone = __toESM(require_timezone(), 1); var import_isoWeek = __toESM(require_isoWeek(), 1); var NavigationAnimator = class { constructor(headerTrack, contentTrack, headerDrawer) { this.headerTrack = headerTrack; this.contentTrack = contentTrack; this.headerDrawer = headerDrawer; } async slide(direction, renderFn) { const out = direction === "left" ? "-100%" : "100%"; const into = direction === "left" ? "100%" : "-100%"; await this.animateOut(out); await renderFn(); await this.animateIn(into); } async animateOut(translate) { const animations = [ this.headerTrack.animate([{ transform: "translateX(0)" }, { transform: `translateX(${translate})` }], { duration: 200, easing: "ease-in" }).finished, this.contentTrack.animate([{ transform: "translateX(0)" }, { transform: `translateX(${translate})` }], { duration: 200, easing: "ease-in" }).finished ]; if (this.headerDrawer) { animations.push(this.headerDrawer.animate([{ transform: "translateX(0)" }, { transform: `translateX(${translate})` }], { duration: 200, easing: "ease-in" }).finished); } await Promise.all(animations); } async animateIn(translate) { const animations = [ this.headerTrack.animate([{ transform: `translateX(${translate})` }, { transform: "translateX(0)" }], { duration: 200, easing: "ease-out" }).finished, this.contentTrack.animate([{ transform: `translateX(${translate})` }, { transform: "translateX(0)" }], { duration: 200, easing: "ease-out" }).finished ]; if (this.headerDrawer) { animations.push(this.headerDrawer.animate([{ transform: `translateX(${translate})` }, { transform: "translateX(0)" }], { duration: 200, easing: "ease-out" }).finished); } await Promise.all(animations); } }; var CalendarEvents = { // Command events (host → calendar) CMD_NAVIGATE_PREV: "calendar:cmd:navigate:prev", CMD_NAVIGATE_NEXT: "calendar:cmd:navigate:next", CMD_DRAWER_TOGGLE: "calendar:cmd:drawer:toggle", CMD_RENDER: "calendar:cmd:render", CMD_WORKWEEK_CHANGE: "calendar:cmd:workweek:change", CMD_VIEW_UPDATE: "calendar:cmd:view:update" }; var CalendarApp = class { constructor(orchestrator, timeAxisRenderer, dateService, scrollManager, headerDrawerManager, dragDropManager, edgeScrollManager, resizeManager, headerDrawerRenderer, eventPersistenceManager, settingsService, viewConfigService, eventBus) { this.orchestrator = orchestrator; this.timeAxisRenderer = timeAxisRenderer; this.dateService = dateService; this.scrollManager = scrollManager; this.headerDrawerManager = headerDrawerManager; this.dragDropManager = dragDropManager; this.edgeScrollManager = edgeScrollManager; this.resizeManager = resizeManager; this.headerDrawerRenderer = headerDrawerRenderer; this.eventPersistenceManager = eventPersistenceManager; this.settingsService = settingsService; this.viewConfigService = viewConfigService; this.eventBus = eventBus; this.dayOffset = 0; this.currentViewId = "simple"; this.workweekPreset = null; this.groupingOverrides = /* @__PURE__ */ new Map(); } async init(container) { this.container = container; const gridSettings = await this.settingsService.getGridSettings(); if (!gridSettings) { throw new Error("GridSettings not found"); } this.workweekPreset = await this.settingsService.getDefaultWorkweekPreset(); this.animator = new NavigationAnimator(container.querySelector("swp-header-track"), container.querySelector("swp-content-track"), container.querySelector("swp-header-drawer")); this.timeAxisRenderer.render(container.querySelector("#time-axis"), gridSettings.dayStartHour, gridSettings.dayEndHour); this.scrollManager.init(container); this.headerDrawerManager.init(container); this.dragDropManager.init(container); this.resizeManager.init(container); const scrollableContent = container.querySelector("swp-scrollable-content"); this.edgeScrollManager.init(scrollableContent); this.setupEventListeners(); this.emitStatus("ready"); } setupEventListeners() { this.eventBus.on(CalendarEvents.CMD_NAVIGATE_PREV, () => { this.handleNavigatePrev(); }); this.eventBus.on(CalendarEvents.CMD_NAVIGATE_NEXT, () => { this.handleNavigateNext(); }); this.eventBus.on(CalendarEvents.CMD_DRAWER_TOGGLE, () => { this.headerDrawerManager.toggle(); }); this.eventBus.on(CalendarEvents.CMD_RENDER, (e) => { const { viewId } = e.detail; this.handleRenderCommand(viewId); }); this.eventBus.on(CalendarEvents.CMD_WORKWEEK_CHANGE, (e) => { const { presetId } = e.detail; this.handleWorkweekChange(presetId); }); this.eventBus.on(CalendarEvents.CMD_VIEW_UPDATE, (e) => { const { type, values } = e.detail; this.handleViewUpdate(type, values); }); } async handleRenderCommand(viewId) { this.currentViewId = viewId; await this.render(); this.emitStatus("rendered", { viewId }); } async handleNavigatePrev() { const step = this.workweekPreset?.periodDays ?? 7; this.dayOffset -= step; await this.animator.slide("right", () => this.render()); this.emitStatus("rendered", { viewId: this.currentViewId }); } async handleNavigateNext() { const step = this.workweekPreset?.periodDays ?? 7; this.dayOffset += step; await this.animator.slide("left", () => this.render()); this.emitStatus("rendered", { viewId: this.currentViewId }); } async handleWorkweekChange(presetId) { const preset = await this.settingsService.getWorkweekPreset(presetId); if (preset) { this.workweekPreset = preset; await this.render(); this.emitStatus("rendered", { viewId: this.currentViewId }); } } async handleViewUpdate(type, values) { this.groupingOverrides.set(type, values); await this.render(); this.emitStatus("rendered", { viewId: this.currentViewId }); } async render() { const storedConfig = await this.viewConfigService.getById(this.currentViewId); if (!storedConfig) { this.emitStatus("error", { message: `ViewConfig not found: ${this.currentViewId}` }); return; } const workDays = this.workweekPreset?.workDays || [1, 2, 3, 4, 5]; const periodDays = this.workweekPreset?.periodDays ?? 7; const dates = periodDays === 1 ? this.dateService.getDatesFromOffset(this.dayOffset, workDays.length) : this.dateService.getWorkDaysFromOffset(this.dayOffset, workDays); const viewConfig = { ...storedConfig, groupings: storedConfig.groupings.map((g) => { if (g.type === "date") { return { ...g, values: dates }; } const override = this.groupingOverrides.get(g.type); if (override) { return { ...g, values: override }; } return g; }) }; await this.orchestrator.render(viewConfig, this.container); } emitStatus(status, detail) { this.container.dispatchEvent(new CustomEvent(`calendar:status:${status}`, { detail, bubbles: true })); } }; function buildPipeline(renderers) { return { async run(context) { for (const renderer of renderers) { await renderer.render(context); } } }; } var FilterTemplate = class { constructor(dateService, entityResolver) { this.dateService = dateService; this.entityResolver = entityResolver; this.fields = []; } /** * Tilføj felt til template * @param idProperty - Property-navn (bruges pĂ¥ bĂ¥de event og column.dataset) * @param derivedFrom - Hvis feltet udledes fra anden property (f.eks. date fra start) */ addField(idProperty, derivedFrom) { this.fields.push({ idProperty, derivedFrom }); return this; } /** * Parse dot-notation string into components * @example 'resource.teamId' → { entityType: 'resource', property: 'teamId', foreignKey: 'resourceId' } */ parseDotNotation(idProperty) { if (!idProperty.includes(".")) return null; const [entityType, property] = idProperty.split("."); return { entityType, property, foreignKey: entityType + "Id" // Convention: resource → resourceId }; } /** * Get dataset key for column lookup * For dot-notation 'resource.teamId', we look for 'teamId' in dataset */ getDatasetKey(idProperty) { const dotNotation = this.parseDotNotation(idProperty); if (dotNotation) { return dotNotation.property; } return idProperty; } /** * Byg nøgle fra kolonne * Læser værdier fra column.dataset[idProperty] * For dot-notation, uses the property part (resource.teamId → teamId) */ buildKeyFromColumn(column) { return this.fields.map((f) => { const key = this.getDatasetKey(f.idProperty); return column.dataset[key] || ""; }).join(":"); } /** * Byg nøgle fra event * Læser værdier fra event[idProperty] eller udleder fra derivedFrom * For dot-notation, resolves via EntityResolver */ buildKeyFromEvent(event) { const eventRecord = event; return this.fields.map((f) => { const dotNotation = this.parseDotNotation(f.idProperty); if (dotNotation) { return this.resolveDotNotation(eventRecord, dotNotation); } if (f.derivedFrom) { const sourceValue = eventRecord[f.derivedFrom]; if (sourceValue instanceof Date) { return this.dateService.getDateKey(sourceValue); } return String(sourceValue || ""); } return String(eventRecord[f.idProperty] || ""); }).join(":"); } /** * Resolve dot-notation reference via EntityResolver */ resolveDotNotation(eventRecord, dotNotation) { if (!this.entityResolver) { console.warn(`FilterTemplate: EntityResolver required for dot-notation '${dotNotation.entityType}.${dotNotation.property}'`); return ""; } const foreignId = eventRecord[dotNotation.foreignKey]; if (!foreignId) return ""; const entity = this.entityResolver.resolve(dotNotation.entityType, String(foreignId)); if (!entity) return ""; return String(entity[dotNotation.property] || ""); } /** * Match event mod kolonne */ matches(event, column) { return this.buildKeyFromEvent(event) === this.buildKeyFromColumn(column); } }; var CalendarOrchestrator = class { constructor(allRenderers, eventRenderer, scheduleRenderer, headerDrawerRenderer, dateService, entityServices) { this.allRenderers = allRenderers; this.eventRenderer = eventRenderer; this.scheduleRenderer = scheduleRenderer; this.headerDrawerRenderer = headerDrawerRenderer; this.dateService = dateService; this.entityServices = entityServices; } async render(viewConfig, container) { const headerContainer = container.querySelector("swp-calendar-header"); const columnContainer = container.querySelector("swp-day-columns"); if (!headerContainer || !columnContainer) { throw new Error("Missing swp-calendar-header or swp-day-columns"); } const filter = {}; for (const grouping of viewConfig.groupings) { filter[grouping.type] = grouping.values; } const filterTemplate = new FilterTemplate(this.dateService); for (const grouping of viewConfig.groupings) { if (grouping.idProperty) { filterTemplate.addField(grouping.idProperty, grouping.derivedFrom); } } const { parentChildMap, childType } = await this.resolveBelongsTo(viewConfig.groupings, filter); const context = { headerContainer, columnContainer, filter, groupings: viewConfig.groupings, parentChildMap, childType }; headerContainer.innerHTML = ""; columnContainer.innerHTML = ""; const levels = viewConfig.groupings.map((g) => g.type).join(" "); headerContainer.dataset.levels = levels; const activeRenderers = this.selectRenderers(viewConfig); const pipeline = buildPipeline(activeRenderers); await pipeline.run(context); await this.scheduleRenderer.render(container, filter); await this.eventRenderer.render(container, filter, filterTemplate); await this.headerDrawerRenderer.render(container, filter, filterTemplate); } selectRenderers(viewConfig) { const types = viewConfig.groupings.map((g) => g.type); return types.map((type) => this.allRenderers.find((r) => r.type === type)).filter((r) => r !== void 0); } /** * Resolve belongsTo relations to build parent-child map * e.g., belongsTo: 'team.resourceIds' → { team1: ['EMP001', 'EMP002'], team2: [...] } * Also returns the childType (the grouping type that has belongsTo) */ async resolveBelongsTo(groupings, filter) { const childGrouping = groupings.find((g) => g.belongsTo); if (!childGrouping?.belongsTo) return {}; const [entityType, property] = childGrouping.belongsTo.split("."); if (!entityType || !property) return {}; const parentIds = filter[entityType] || []; if (parentIds.length === 0) return {}; const service = this.entityServices.find((s) => s.entityType.toLowerCase() === entityType); if (!service) return {}; const allEntities = await service.getAll(); const entities = allEntities.filter((e) => parentIds.includes(e.id)); const map = {}; for (const entity of entities) { const entityRecord = entity; const children = entityRecord[property] || []; map[entityRecord.id] = children; } return { parentChildMap: map, childType: childGrouping.type }; } }; var EventBus = class { constructor() { this.eventLog = []; this.debug = false; this.listeners = /* @__PURE__ */ new Set(); this.logConfig = { calendar: true, grid: true, event: true, scroll: true, navigation: true, view: true, default: true }; } /** * Subscribe to an event via DOM addEventListener */ on(eventType, handler, options) { document.addEventListener(eventType, handler, options); this.listeners.add({ eventType, handler, options }); return () => this.off(eventType, handler); } /** * Subscribe to an event once */ once(eventType, handler) { return this.on(eventType, handler, { once: true }); } /** * Unsubscribe from an event */ off(eventType, handler) { document.removeEventListener(eventType, handler); for (const listener of this.listeners) { if (listener.eventType === eventType && listener.handler === handler) { this.listeners.delete(listener); break; } } } /** * Emit an event via DOM CustomEvent */ emit(eventType, detail = {}) { if (!eventType) { return false; } const event = new CustomEvent(eventType, { detail: detail ?? {}, bubbles: true, cancelable: true }); if (this.debug) { this.logEventWithGrouping(eventType, detail); } this.eventLog.push({ type: eventType, detail: detail ?? {}, timestamp: Date.now() }); return !document.dispatchEvent(event); } /** * Log event with console grouping */ logEventWithGrouping(eventType, _detail) { const category = this.extractCategory(eventType); if (!this.logConfig[category]) { return; } this.getCategoryStyle(category); } /** * Extract category from event type */ extractCategory(eventType) { if (!eventType) { return "unknown"; } if (eventType.includes(":")) { return eventType.split(":")[0]; } const lowerType = eventType.toLowerCase(); if (lowerType.includes("grid") || lowerType.includes("rendered")) return "grid"; if (lowerType.includes("event") || lowerType.includes("sync")) return "event"; if (lowerType.includes("scroll")) return "scroll"; if (lowerType.includes("nav") || lowerType.includes("date")) return "navigation"; if (lowerType.includes("view")) return "view"; return "default"; } /** * Get styling for different categories */ getCategoryStyle(category) { const styles = { calendar: { emoji: "\u{1F4C5}", color: "#2196F3" }, grid: { emoji: "\u{1F4CA}", color: "#4CAF50" }, event: { emoji: "\u{1F4CC}", color: "#FF9800" }, scroll: { emoji: "\u{1F4DC}", color: "#9C27B0" }, navigation: { emoji: "\u{1F9ED}", color: "#F44336" }, view: { emoji: "\u{1F441}", color: "#00BCD4" }, default: { emoji: "\u{1F4E2}", color: "#607D8B" } }; return styles[category] || styles.default; } /** * Configure logging for specific categories */ setLogConfig(config) { this.logConfig = { ...this.logConfig, ...config }; } /** * Get current log configuration */ getLogConfig() { return { ...this.logConfig }; } /** * Get event history */ getEventLog(eventType) { if (eventType) { return this.eventLog.filter((e) => e.type === eventType); } return this.eventLog; } /** * Enable/disable debug mode */ setDebug(enabled) { this.debug = enabled; } }; import_dayjs.default.extend(import_utc.default); import_dayjs.default.extend(import_timezone.default); import_dayjs.default.extend(import_isoWeek.default); var DateService = class { constructor(config, baseDate) { this.config = config; this.timezone = config.timezone; this.baseDate = baseDate ? (0, import_dayjs.default)(baseDate) : (0, import_dayjs.default)(); } /** * Set a fixed base date (useful for demos with static mock data) */ setBaseDate(date) { this.baseDate = (0, import_dayjs.default)(date); } /** * Get the current base date (either fixed or today) */ getBaseDate() { return this.baseDate.toDate(); } parseISO(isoString) { return (0, import_dayjs.default)(isoString).toDate(); } getDayName(date, format = "short") { return new Intl.DateTimeFormat(this.config.locale, { weekday: format }).format(date); } /** * Get dates starting from a day offset * @param dayOffset - Day offset from base date * @param count - Number of consecutive days to return * @returns Array of date strings in YYYY-MM-DD format */ getDatesFromOffset(dayOffset, count) { const startDate = this.baseDate.add(dayOffset, "day"); return Array.from({ length: count }, (_, i) => startDate.add(i, "day").format("YYYY-MM-DD")); } /** * Get specific weekdays from the week containing the offset date * @param dayOffset - Day offset from base date * @param workDays - Array of ISO weekday numbers (1=Monday, 7=Sunday) * @returns Array of date strings in YYYY-MM-DD format */ getWorkDaysFromOffset(dayOffset, workDays) { const targetDate = this.baseDate.add(dayOffset, "day"); const monday = targetDate.startOf("week").add(1, "day"); return workDays.map((isoDay) => { const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1; return monday.add(daysFromMonday, "day").format("YYYY-MM-DD"); }); } // Legacy methods for backwards compatibility getWeekDates(weekOffset = 0, days = 7) { return this.getDatesFromOffset(weekOffset * 7, days); } getWorkWeekDates(weekOffset, workDays) { return this.getWorkDaysFromOffset(weekOffset * 7, workDays); } // ============================================ // FORMATTING // ============================================ formatTime(date, showSeconds = false) { const pattern = showSeconds ? "HH:mm:ss" : "HH:mm"; return (0, import_dayjs.default)(date).format(pattern); } formatTimeRange(start, end) { return `${this.formatTime(start)} - ${this.formatTime(end)}`; } formatDate(date) { return (0, import_dayjs.default)(date).format("YYYY-MM-DD"); } getDateKey(date) { return this.formatDate(date); } // ============================================ // COLUMN KEY // ============================================ /** * Build a uniform columnKey from grouping segments * Handles any combination of date, resource, team, etc. * * @example * buildColumnKey({ date: '2025-12-09' }) → "2025-12-09" * buildColumnKey({ date: '2025-12-09', resource: 'EMP001' }) → "2025-12-09:EMP001" */ buildColumnKey(segments) { const date = segments.date; const others = Object.entries(segments).filter(([k]) => k !== "date").sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v); return date ? [date, ...others].join(":") : others.join(":"); } /** * Parse a columnKey back into segments * Assumes format: "date:resource:..." or just "date" */ parseColumnKey(columnKey) { const parts = columnKey.split(":"); return { date: parts[0], resource: parts[1] }; } /** * Extract dateKey from columnKey (first segment) */ getDateFromColumnKey(columnKey) { return columnKey.split(":")[0]; } // ============================================ // TIME CALCULATIONS // ============================================ timeToMinutes(timeString) { const parts = timeString.split(":").map(Number); const hours = parts[0] || 0; const minutes = parts[1] || 0; return hours * 60 + minutes; } minutesToTime(totalMinutes) { const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; return (0, import_dayjs.default)().hour(hours).minute(minutes).format("HH:mm"); } getMinutesSinceMidnight(date) { const d = (0, import_dayjs.default)(date); return d.hour() * 60 + d.minute(); } // ============================================ // UTC CONVERSIONS // ============================================ toUTC(localDate) { return import_dayjs.default.tz(localDate, this.timezone).utc().toISOString(); } fromUTC(utcString) { return import_dayjs.default.utc(utcString).tz(this.timezone).toDate(); } // ============================================ // DATE CREATION // ============================================ createDateAtTime(baseDate, timeString) { const totalMinutes = this.timeToMinutes(timeString); const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; return (0, import_dayjs.default)(baseDate).startOf("day").hour(hours).minute(minutes).toDate(); } getISOWeekDay(date) { return (0, import_dayjs.default)(date).isoWeekday(); } }; var ScrollManager = class { init(container) { this.scrollableContent = container.querySelector("swp-scrollable-content"); this.timeAxisContent = container.querySelector("swp-time-axis-content"); this.calendarHeader = container.querySelector("swp-calendar-header"); this.headerDrawer = container.querySelector("swp-header-drawer"); this.headerViewport = container.querySelector("swp-header-viewport"); this.headerSpacer = container.querySelector("swp-header-spacer"); this.scrollableContent.addEventListener("scroll", () => this.onScroll()); this.resizeObserver = new ResizeObserver(() => this.syncHeaderSpacerHeight()); this.resizeObserver.observe(this.headerViewport); this.syncHeaderSpacerHeight(); } syncHeaderSpacerHeight() { const computedHeight = getComputedStyle(this.headerViewport).height; this.headerSpacer.style.height = computedHeight; } onScroll() { const { scrollTop, scrollLeft } = this.scrollableContent; this.timeAxisContent.style.transform = `translateY(-${scrollTop}px)`; this.calendarHeader.style.transform = `translateX(-${scrollLeft}px)`; this.headerDrawer.style.transform = `translateX(-${scrollLeft}px)`; } }; var HeaderDrawerManager = class { constructor() { this.expanded = false; this.currentRows = 0; this.rowHeight = 25; this.duration = 200; } init(container) { this.drawer = container.querySelector("swp-header-drawer"); if (!this.drawer) console.error("HeaderDrawerManager: swp-header-drawer not found"); } toggle() { this.expanded ? this.collapse() : this.expand(); } /** * Expand drawer to single row (legacy support) */ expand() { this.expandToRows(1); } /** * Expand drawer to fit specified number of rows */ expandToRows(rowCount) { const targetHeight = rowCount * this.rowHeight; const currentHeight = this.expanded ? this.currentRows * this.rowHeight : 0; if (this.expanded && this.currentRows === rowCount) return; this.currentRows = rowCount; this.expanded = true; this.animate(currentHeight, targetHeight); } collapse() { if (!this.expanded) return; const currentHeight = this.currentRows * this.rowHeight; this.expanded = false; this.currentRows = 0; this.animate(currentHeight, 0); } animate(from, to) { const keyframes = [ { height: `${from}px` }, { height: `${to}px` } ]; const options = { duration: this.duration, easing: "ease", fill: "forwards" }; this.drawer.animate(keyframes, options); } isExpanded() { return this.expanded; } getRowCount() { return this.currentRows; } }; var DateRenderer = class { constructor(dateService) { this.dateService = dateService; this.type = "date"; } render(context) { const dates = context.filter["date"] || []; const resourceIds = context.filter["resource"] || []; const dateGrouping = context.groupings?.find((g) => g.type === "date"); const hideHeader = dateGrouping?.hideHeader === true; const iterations = resourceIds.length || 1; let columnCount = 0; for (let r = 0; r < iterations; r++) { const resourceId = resourceIds[r]; for (const dateStr of dates) { const date = this.dateService.parseISO(dateStr); const segments = { date: dateStr }; if (resourceId) segments.resource = resourceId; const columnKey = this.dateService.buildColumnKey(segments); const header = document.createElement("swp-day-header"); header.dataset.date = dateStr; header.dataset.columnKey = columnKey; if (resourceId) { header.dataset.resourceId = resourceId; } if (hideHeader) { header.dataset.hidden = "true"; } header.innerHTML = ` ${this.dateService.getDayName(date, "short")} ${date.getDate()} `; context.headerContainer.appendChild(header); const column = document.createElement("swp-day-column"); column.dataset.date = dateStr; column.dataset.columnKey = columnKey; if (resourceId) { column.dataset.resourceId = resourceId; } column.innerHTML = ""; context.columnContainer.appendChild(column); columnCount++; } } const container = context.columnContainer.closest("swp-calendar-container"); if (container) { container.style.setProperty("--grid-columns", String(columnCount)); } } }; var ResourceRenderer = class extends BaseGroupingRenderer { constructor(resourceService) { super(); this.resourceService = resourceService; this.type = "resource"; this.config = { elementTag: "swp-resource-header", idAttribute: "resourceId", colspanVar: "--resource-cols" }; } getEntities(ids) { return this.resourceService.getByIds(ids); } getDisplayName(entity) { return entity.displayName; } /** * Override render to handle: * 1. Special ordering when parentChildMap exists (resources grouped by parent) * 2. Different colspan calculation (just dateCount, not childCount * dateCount) */ async render(context) { const resourceIds = context.filter["resource"] || []; const dateCount = context.filter["date"]?.length || 1; let orderedResourceIds; if (context.parentChildMap) { orderedResourceIds = []; for (const childIds of Object.values(context.parentChildMap)) { for (const childId of childIds) { if (resourceIds.includes(childId)) { orderedResourceIds.push(childId); } } } } else { orderedResourceIds = resourceIds; } const resources = await this.getEntities(orderedResourceIds); const resourceMap = new Map(resources.map((r) => [r.id, r])); for (const resourceId of orderedResourceIds) { const resource = resourceMap.get(resourceId); if (!resource) continue; const header = this.createHeader(resource, context); header.style.gridColumn = `span ${dateCount}`; context.headerContainer.appendChild(header); } } }; function calculateEventPosition(start, end, config) { const startMinutes = start.getHours() * 60 + start.getMinutes(); const endMinutes = end.getHours() * 60 + end.getMinutes(); const dayStartMinutes = config.dayStartHour * 60; const minuteHeight = config.hourHeight / 60; const top = (startMinutes - dayStartMinutes) * minuteHeight; const height = (endMinutes - startMinutes) * minuteHeight; return { top, height }; } function minutesToPixels(minutes, config) { return minutes / 60 * config.hourHeight; } function pixelsToMinutes(pixels, config) { return pixels / config.hourHeight * 60; } function snapToGrid(pixels, config) { const snapPixels = minutesToPixels(config.snapInterval, config); return Math.round(pixels / snapPixels) * snapPixels; } function eventsOverlap(a, b) { return a.start < b.end && a.end > b.start; } function eventsWithinThreshold(a, b, thresholdMinutes) { const thresholdMs = thresholdMinutes * 60 * 1e3; const startToStartDiff = Math.abs(a.start.getTime() - b.start.getTime()); if (startToStartDiff <= thresholdMs) return true; const bStartsBeforeAEnds = a.end.getTime() - b.start.getTime(); if (bStartsBeforeAEnds > 0 && bStartsBeforeAEnds <= thresholdMs) return true; const aStartsBeforeBEnds = b.end.getTime() - a.start.getTime(); if (aStartsBeforeBEnds > 0 && aStartsBeforeBEnds <= thresholdMs) return true; return false; } function findOverlapGroups(events) { if (events.length === 0) return []; const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime()); const used = /* @__PURE__ */ new Set(); const groups = []; for (const event of sorted) { if (used.has(event.id)) continue; const group = [event]; used.add(event.id); let expanded = true; while (expanded) { expanded = false; for (const candidate of sorted) { if (used.has(candidate.id)) continue; const connects = group.some((member) => eventsOverlap(member, candidate)); if (connects) { group.push(candidate); used.add(candidate.id); expanded = true; } } } groups.push(group); } return groups; } function findGridCandidates(events, thresholdMinutes) { if (events.length === 0) return []; const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime()); const used = /* @__PURE__ */ new Set(); const groups = []; for (const event of sorted) { if (used.has(event.id)) continue; const group = [event]; used.add(event.id); let expanded = true; while (expanded) { expanded = false; for (const candidate of sorted) { if (used.has(candidate.id)) continue; const connects = group.some((member) => eventsWithinThreshold(member, candidate, thresholdMinutes)); if (connects) { group.push(candidate); used.add(candidate.id); expanded = true; } } } groups.push(group); } return groups; } function calculateStackLevels(events) { const levels = /* @__PURE__ */ new Map(); const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime()); for (const event of sorted) { let maxOverlappingLevel = -1; for (const [id, level] of levels) { const other = events.find((e) => e.id === id); if (other && eventsOverlap(event, other)) { maxOverlappingLevel = Math.max(maxOverlappingLevel, level); } } levels.set(event.id, maxOverlappingLevel + 1); } return levels; } function allocateColumns(events) { const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime()); const columns = []; for (const event of sorted) { let placed = false; for (const column of columns) { const canFit = !column.some((e) => eventsOverlap(event, e)); if (canFit) { column.push(event); placed = true; break; } } if (!placed) { columns.push([event]); } } return columns; } function calculateColumnLayout(events, config) { const thresholdMinutes = config.gridStartThresholdMinutes ?? 10; const result = { grids: [], stacked: [] }; if (events.length === 0) return result; const overlapGroups = findOverlapGroups(events); for (const overlapGroup of overlapGroups) { if (overlapGroup.length === 1) { result.stacked.push({ event: overlapGroup[0], stackLevel: 0 }); continue; } const gridSubgroups = findGridCandidates(overlapGroup, thresholdMinutes); const largestGridCandidate = gridSubgroups.reduce((max, g) => g.length > max.length ? g : max, gridSubgroups[0]); if (largestGridCandidate.length === overlapGroup.length) { const columns = allocateColumns(overlapGroup); const earliest = overlapGroup.reduce((min, e) => e.start < min.start ? e : min, overlapGroup[0]); const position = calculateEventPosition(earliest.start, earliest.end, config); result.grids.push({ events: overlapGroup, columns, stackLevel: 0, position: { top: position.top } }); } else { const levels = calculateStackLevels(overlapGroup); for (const event of overlapGroup) { result.stacked.push({ event, stackLevel: levels.get(event.id) ?? 0 }); } } } return result; } var EventRenderer = class { constructor(eventService, dateService, gridConfig, eventBus) { this.eventService = eventService; this.dateService = dateService; this.gridConfig = gridConfig; this.eventBus = eventBus; this.container = null; this.setupListeners(); } /** * Setup listeners for drag-drop and update events */ setupListeners() { this.eventBus.on(CoreEvents.EVENT_DRAG_COLUMN_CHANGE, (e) => { const payload = e.detail; this.handleColumnChange(payload); }); this.eventBus.on(CoreEvents.EVENT_DRAG_MOVE, (e) => { const payload = e.detail; this.updateDragTimestamp(payload); }); this.eventBus.on(CoreEvents.EVENT_UPDATED, (e) => { const payload = e.detail; this.handleEventUpdated(payload); }); this.eventBus.on(CoreEvents.EVENT_DRAG_END, (e) => { const payload = e.detail; this.handleDragEnd(payload); }); this.eventBus.on(CoreEvents.EVENT_DRAG_LEAVE_HEADER, (e) => { const payload = e.detail; this.handleDragLeaveHeader(payload); }); } /** * Handle EVENT_DRAG_END - remove element if dropped in header */ handleDragEnd(payload) { if (payload.target === "header") { const element = this.container?.querySelector(`swp-content-viewport swp-event[data-event-id="${payload.swpEvent.eventId}"]`); element?.remove(); } } /** * Handle header item leaving header - create swp-event in grid */ handleDragLeaveHeader(payload) { if (payload.source !== "header") return; if (!payload.targetColumn || !payload.start || !payload.end) return; if (payload.element) { payload.element.classList.add("drag-ghost"); payload.element.style.opacity = "0.3"; payload.element.style.pointerEvents = "none"; } const event = { id: payload.eventId, title: payload.title || "", description: "", start: payload.start, end: payload.end, type: "customer", allDay: false, syncStatus: "pending" }; const element = this.createEventElement(event); let eventsLayer = payload.targetColumn.querySelector("swp-events-layer"); if (!eventsLayer) { eventsLayer = document.createElement("swp-events-layer"); payload.targetColumn.appendChild(eventsLayer); } eventsLayer.appendChild(element); element.classList.add("dragging"); } /** * Handle EVENT_UPDATED - re-render affected columns */ async handleEventUpdated(payload) { if (payload.sourceColumnKey !== payload.targetColumnKey) { await this.rerenderColumn(payload.sourceColumnKey); } await this.rerenderColumn(payload.targetColumnKey); } /** * Re-render a single column with fresh data from IndexedDB */ async rerenderColumn(columnKey) { const column = this.findColumn(columnKey); if (!column) return; const date = column.dataset.date; const resourceId = column.dataset.resourceId; if (!date) return; const startDate = new Date(date); const endDate = new Date(date); endDate.setHours(23, 59, 59, 999); const events = resourceId ? await this.eventService.getByResourceAndDateRange(resourceId, startDate, endDate) : await this.eventService.getByDateRange(startDate, endDate); const timedEvents = events.filter((event) => !event.allDay && this.dateService.getDateKey(event.start) === date); let eventsLayer = column.querySelector("swp-events-layer"); if (!eventsLayer) { eventsLayer = document.createElement("swp-events-layer"); column.appendChild(eventsLayer); } eventsLayer.innerHTML = ""; const layout = calculateColumnLayout(timedEvents, this.gridConfig); layout.grids.forEach((grid) => { const groupEl = this.renderGridGroup(grid); eventsLayer.appendChild(groupEl); }); layout.stacked.forEach((item) => { const eventEl = this.renderStackedEvent(item.event, item.stackLevel); eventsLayer.appendChild(eventEl); }); } /** * Find a column element by columnKey */ findColumn(columnKey) { if (!this.container) return null; return this.container.querySelector(`swp-day-column[data-column-key="${columnKey}"]`); } /** * Handle event moving to a new column during drag */ handleColumnChange(payload) { const eventsLayer = payload.newColumn.querySelector("swp-events-layer"); if (!eventsLayer) return; eventsLayer.appendChild(payload.element); payload.element.style.top = `${payload.currentY}px`; } /** * Update timestamp display during drag (snapped to grid) */ updateDragTimestamp(payload) { const timeEl = payload.element.querySelector("swp-event-time"); if (!timeEl) return; const snappedY = snapToGrid(payload.currentY, this.gridConfig); const minutesFromGridStart = pixelsToMinutes(snappedY, this.gridConfig); const startMinutes = this.gridConfig.dayStartHour * 60 + minutesFromGridStart; const height = parseFloat(payload.element.style.height) || this.gridConfig.hourHeight; const durationMinutes = pixelsToMinutes(height, this.gridConfig); const start = this.minutesToDate(startMinutes); const end = this.minutesToDate(startMinutes + durationMinutes); timeEl.textContent = this.dateService.formatTimeRange(start, end); } /** * Convert minutes since midnight to a Date object (today) */ minutesToDate(minutes) { const date = /* @__PURE__ */ new Date(); date.setHours(Math.floor(minutes / 60) % 24, minutes % 60, 0, 0); return date; } /** * Render events for visible dates into day columns * @param container - Calendar container element * @param filter - Filter with 'date' and optionally 'resource' arrays * @param filterTemplate - Template for matching events to columns */ async render(container, filter, filterTemplate) { this.container = container; const visibleDates = filter["date"] || []; if (visibleDates.length === 0) return; const startDate = new Date(visibleDates[0]); const endDate = new Date(visibleDates[visibleDates.length - 1]); endDate.setHours(23, 59, 59, 999); const events = await this.eventService.getByDateRange(startDate, endDate); const dayColumns = container.querySelector("swp-day-columns"); if (!dayColumns) return; const columns = dayColumns.querySelectorAll("swp-day-column"); columns.forEach((column) => { const columnEl = column; const columnEvents = events.filter((event) => filterTemplate.matches(event, columnEl)); let eventsLayer = column.querySelector("swp-events-layer"); if (!eventsLayer) { eventsLayer = document.createElement("swp-events-layer"); column.appendChild(eventsLayer); } eventsLayer.innerHTML = ""; const timedEvents = columnEvents.filter((event) => !event.allDay); const layout = calculateColumnLayout(timedEvents, this.gridConfig); layout.grids.forEach((grid) => { const groupEl = this.renderGridGroup(grid); eventsLayer.appendChild(groupEl); }); layout.stacked.forEach((item) => { const eventEl = this.renderStackedEvent(item.event, item.stackLevel); eventsLayer.appendChild(eventEl); }); }); } /** * Create a single event element * * CLEAN approach: * - Only data-id for lookup * - Visible content in innerHTML only */ createEventElement(event) { const element = document.createElement("swp-event"); element.dataset.eventId = event.id; if (event.resourceId) { element.dataset.resourceId = event.resourceId; } const position = calculateEventPosition(event.start, event.end, this.gridConfig); element.style.top = `${position.top}px`; element.style.height = `${position.height}px`; const colorClass = this.getColorClass(event); if (colorClass) { element.classList.add(colorClass); } element.innerHTML = ` ${this.dateService.formatTimeRange(event.start, event.end)} ${this.escapeHtml(event.title)} ${event.description ? `${this.escapeHtml(event.description)}` : ""} `; return element; } /** * Get color class based on metadata.color or event type */ getColorClass(event) { if (event.metadata?.color) { return `is-${event.metadata.color}`; } const typeColors = { "customer": "is-blue", "vacation": "is-green", "break": "is-amber", "meeting": "is-purple", "blocked": "is-red" }; return typeColors[event.type] || "is-blue"; } /** * Escape HTML to prevent XSS */ escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } /** * Render a GRID group with side-by-side columns * Used when multiple events start at the same time */ renderGridGroup(layout) { const group = document.createElement("swp-event-group"); group.classList.add(`cols-${layout.columns.length}`); group.style.top = `${layout.position.top}px`; if (layout.stackLevel > 0) { group.style.marginLeft = `${layout.stackLevel * 15}px`; group.style.zIndex = `${100 + layout.stackLevel}`; } let maxBottom = 0; for (const event of layout.events) { const pos = calculateEventPosition(event.start, event.end, this.gridConfig); const eventBottom = pos.top + pos.height; if (eventBottom > maxBottom) maxBottom = eventBottom; } const groupHeight = maxBottom - layout.position.top; group.style.height = `${groupHeight}px`; layout.columns.forEach((columnEvents) => { const wrapper = document.createElement("div"); wrapper.style.position = "relative"; columnEvents.forEach((event) => { const eventEl = this.createEventElement(event); const pos = calculateEventPosition(event.start, event.end, this.gridConfig); eventEl.style.top = `${pos.top - layout.position.top}px`; eventEl.style.position = "absolute"; eventEl.style.left = "0"; eventEl.style.right = "0"; wrapper.appendChild(eventEl); }); group.appendChild(wrapper); }); return group; } /** * Render a STACKED event with margin-left offset * Used for overlapping events that don't start at the same time */ renderStackedEvent(event, stackLevel) { const element = this.createEventElement(event); element.dataset.stackLink = JSON.stringify({ stackLevel }); if (stackLevel > 0) { element.style.marginLeft = `${stackLevel * 15}px`; element.style.zIndex = `${100 + stackLevel}`; } return element; } }; var TimeAxisRenderer = class { render(container, startHour = 6, endHour = 20) { container.innerHTML = ""; for (let hour = startHour; hour <= endHour; hour++) { const marker = document.createElement("swp-hour-marker"); marker.textContent = `${hour.toString().padStart(2, "0")}:00`; container.appendChild(marker); } } }; var HeaderDrawerRenderer = class { constructor(eventBus, gridConfig, headerDrawerManager, eventService, dateService) { this.eventBus = eventBus; this.gridConfig = gridConfig; this.headerDrawerManager = headerDrawerManager; this.eventService = eventService; this.dateService = dateService; this.currentItem = null; this.container = null; this.sourceElement = null; this.wasExpandedBeforeDrag = false; this.filterTemplate = null; this.setupListeners(); } /** * Render allDay events into the header drawer with row stacking * @param filterTemplate - Template for matching events to columns */ async render(container, filter, filterTemplate) { this.filterTemplate = filterTemplate; const drawer = container.querySelector("swp-header-drawer"); if (!drawer) return; const visibleDates = filter["date"] || []; if (visibleDates.length === 0) return; const visibleColumnKeys = this.getVisibleColumnKeysFromDOM(); if (visibleColumnKeys.length === 0) return; const startDate = new Date(visibleDates[0]); const endDate = new Date(visibleDates[visibleDates.length - 1]); endDate.setHours(23, 59, 59, 999); const events = await this.eventService.getByDateRange(startDate, endDate); const allDayEvents = events.filter((event) => event.allDay !== false); drawer.innerHTML = ""; if (allDayEvents.length === 0) return; const layouts = this.calculateLayout(allDayEvents, visibleColumnKeys); const rowCount = Math.max(1, ...layouts.map((l) => l.row)); layouts.forEach((layout) => { const item = this.createHeaderItem(layout); drawer.appendChild(item); }); this.headerDrawerManager.expandToRows(rowCount); } /** * Create a header item element from layout */ createHeaderItem(layout) { const { event, columnKey, row, colStart, colEnd } = layout; const item = document.createElement("swp-header-item"); item.dataset.eventId = event.id; item.dataset.itemType = "event"; item.dataset.start = event.start.toISOString(); item.dataset.end = event.end.toISOString(); item.dataset.columnKey = columnKey; item.textContent = event.title; const colorClass = this.getColorClass(event); if (colorClass) item.classList.add(colorClass); item.style.gridArea = `${row} / ${colStart} / ${row + 1} / ${colEnd}`; return item; } /** * Calculate layout for all events with row stacking * Uses track-based algorithm to find available rows for overlapping events */ calculateLayout(events, visibleColumnKeys) { const tracks = [new Array(visibleColumnKeys.length).fill(false)]; const layouts = []; for (const event of events) { const columnKey = this.buildColumnKeyFromEvent(event); const startCol = visibleColumnKeys.indexOf(columnKey); const endColumnKey = this.buildColumnKeyFromEvent(event, event.end); const endCol = visibleColumnKeys.indexOf(endColumnKey); if (startCol === -1 && endCol === -1) continue; const colStart = Math.max(0, startCol); const colEnd = (endCol !== -1 ? endCol : visibleColumnKeys.length - 1) + 1; const row = this.findAvailableRow(tracks, colStart, colEnd); for (let c = colStart; c < colEnd; c++) { tracks[row][c] = true; } layouts.push({ event, columnKey, row: row + 1, colStart: colStart + 1, colEnd: colEnd + 1 }); } return layouts; } /** * Build columnKey from event using FilterTemplate * Uses the same template that columns use for matching */ buildColumnKeyFromEvent(event, date) { if (!this.filterTemplate) { const dateStr = this.dateService.getDateKey(date || event.start); return dateStr; } if (date && date.getTime() !== event.start.getTime()) { const tempEvent = { ...event, start: date }; return this.filterTemplate.buildKeyFromEvent(tempEvent); } return this.filterTemplate.buildKeyFromEvent(event); } /** * Find available row for event spanning columns [colStart, colEnd) */ findAvailableRow(tracks, colStart, colEnd) { for (let row = 0; row < tracks.length; row++) { let available = true; for (let c = colStart; c < colEnd; c++) { if (tracks[row][c]) { available = false; break; } } if (available) return row; } tracks.push(new Array(tracks[0].length).fill(false)); return tracks.length - 1; } /** * Get color class based on event metadata or type */ getColorClass(event) { if (event.metadata?.color) { return `is-${event.metadata.color}`; } const typeColors = { "customer": "is-blue", "vacation": "is-green", "break": "is-amber", "meeting": "is-purple", "blocked": "is-red" }; return typeColors[event.type] || "is-blue"; } /** * Setup event listeners for drag events */ setupListeners() { this.eventBus.on(CoreEvents.EVENT_DRAG_ENTER_HEADER, (e) => { const payload = e.detail; this.handleDragEnter(payload); }); this.eventBus.on(CoreEvents.EVENT_DRAG_MOVE_HEADER, (e) => { const payload = e.detail; this.handleDragMove(payload); }); this.eventBus.on(CoreEvents.EVENT_DRAG_LEAVE_HEADER, (e) => { const payload = e.detail; this.handleDragLeave(payload); }); this.eventBus.on(CoreEvents.EVENT_DRAG_END, (e) => { const payload = e.detail; this.handleDragEnd(payload); }); this.eventBus.on(CoreEvents.EVENT_DRAG_CANCEL, () => { this.cleanup(); }); } /** * Handle drag entering header zone - create preview item */ handleDragEnter(payload) { this.container = document.querySelector("swp-header-drawer"); if (!this.container) return; this.wasExpandedBeforeDrag = this.headerDrawerManager.isExpanded(); if (!this.wasExpandedBeforeDrag) { this.headerDrawerManager.expandToRows(1); } this.sourceElement = payload.element; const item = document.createElement("swp-header-item"); item.dataset.eventId = payload.eventId; item.dataset.itemType = payload.itemType; item.dataset.duration = String(payload.duration); item.dataset.columnKey = payload.sourceColumnKey; item.textContent = payload.title; if (payload.colorClass) { item.classList.add(payload.colorClass); } item.classList.add("dragging"); const col = payload.sourceColumnIndex + 1; const endCol = col + payload.duration; item.style.gridArea = `1 / ${col} / 2 / ${endCol}`; this.container.appendChild(item); this.currentItem = item; payload.element.style.visibility = "hidden"; } /** * Handle drag moving within header - update column position */ handleDragMove(payload) { if (!this.currentItem) return; const col = payload.columnIndex + 1; const duration = parseInt(this.currentItem.dataset.duration || "1", 10); const endCol = col + duration; this.currentItem.style.gridArea = `1 / ${col} / 2 / ${endCol}`; this.currentItem.dataset.columnKey = payload.columnKey; } /** * Handle drag leaving header - cleanup for grid→header drag only */ handleDragLeave(payload) { if (payload.source === "grid") { this.cleanup(); } } /** * Handle drag end - finalize based on drop target */ handleDragEnd(payload) { if (payload.target === "header") { if (this.currentItem) { this.currentItem.classList.remove("dragging"); this.recalculateDrawerLayout(); this.currentItem = null; this.sourceElement = null; } } else { const ghost = document.querySelector(`swp-header-item.drag-ghost[data-event-id="${payload.swpEvent.eventId}"]`); ghost?.remove(); this.recalculateDrawerLayout(); } } /** * Recalculate layout for all items currently in the drawer * Called after drop to reposition items and adjust height */ recalculateDrawerLayout() { const drawer = document.querySelector("swp-header-drawer"); if (!drawer) return; const items = Array.from(drawer.querySelectorAll("swp-header-item")); if (items.length === 0) return; const visibleColumnKeys = this.getVisibleColumnKeysFromDOM(); if (visibleColumnKeys.length === 0) return; const itemData = items.map((item) => ({ element: item, columnKey: item.dataset.columnKey || "", duration: parseInt(item.dataset.duration || "1", 10) })); const tracks = [new Array(visibleColumnKeys.length).fill(false)]; for (const item of itemData) { const startCol = visibleColumnKeys.indexOf(item.columnKey); if (startCol === -1) continue; const colStart = startCol; const colEnd = Math.min(startCol + item.duration, visibleColumnKeys.length); const row = this.findAvailableRow(tracks, colStart, colEnd); for (let c = colStart; c < colEnd; c++) { tracks[row][c] = true; } item.element.style.gridArea = `${row + 1} / ${colStart + 1} / ${row + 2} / ${colEnd + 1}`; } const rowCount = tracks.length; this.headerDrawerManager.expandToRows(rowCount); } /** * Get visible column keys from DOM (preserves order for multi-resource views) * Uses filterTemplate.buildKeyFromColumn() for consistent key format with events */ getVisibleColumnKeysFromDOM() { if (!this.filterTemplate) return []; const columns = document.querySelectorAll("swp-day-column"); const columnKeys = []; columns.forEach((col) => { const columnKey = this.filterTemplate.buildKeyFromColumn(col); if (columnKey) columnKeys.push(columnKey); }); return columnKeys; } /** * Cleanup preview item and restore source visibility */ cleanup() { this.currentItem?.remove(); this.currentItem = null; if (this.sourceElement) { this.sourceElement.style.visibility = ""; this.sourceElement = null; } if (!this.wasExpandedBeforeDrag) { this.headerDrawerManager.collapse(); } } }; var ScheduleRenderer = class { constructor(scheduleService, dateService, gridConfig) { this.scheduleService = scheduleService; this.dateService = dateService; this.gridConfig = gridConfig; } /** * Render unavailable zones for visible columns * @param container - Calendar container element * @param filter - Filter with 'date' and 'resource' arrays */ async render(container, filter) { const dates = filter["date"] || []; const resourceIds = filter["resource"] || []; if (dates.length === 0) return; const dayColumns = container.querySelector("swp-day-columns"); if (!dayColumns) return; const columns = dayColumns.querySelectorAll("swp-day-column"); for (const column of columns) { const date = column.dataset.date; const resourceId = column.dataset.resourceId; if (!date || !resourceId) continue; let unavailableLayer = column.querySelector("swp-unavailable-layer"); if (!unavailableLayer) { unavailableLayer = document.createElement("swp-unavailable-layer"); column.insertBefore(unavailableLayer, column.firstChild); } unavailableLayer.innerHTML = ""; const schedule = await this.scheduleService.getScheduleForDate(resourceId, date); this.renderUnavailableZones(unavailableLayer, schedule); } } /** * Render unavailable time zones based on schedule */ renderUnavailableZones(layer, schedule) { const dayStartMinutes = this.gridConfig.dayStartHour * 60; const dayEndMinutes = this.gridConfig.dayEndHour * 60; const minuteHeight = this.gridConfig.hourHeight / 60; if (schedule === null) { const zone = this.createUnavailableZone(0, (dayEndMinutes - dayStartMinutes) * minuteHeight); layer.appendChild(zone); return; } const workStartMinutes = this.dateService.timeToMinutes(schedule.start); const workEndMinutes = this.dateService.timeToMinutes(schedule.end); if (workStartMinutes > dayStartMinutes) { const top = 0; const height = (workStartMinutes - dayStartMinutes) * minuteHeight; const zone = this.createUnavailableZone(top, height); layer.appendChild(zone); } if (workEndMinutes < dayEndMinutes) { const top = (workEndMinutes - dayStartMinutes) * minuteHeight; const height = (dayEndMinutes - workEndMinutes) * minuteHeight; const zone = this.createUnavailableZone(top, height); layer.appendChild(zone); } } /** * Create an unavailable zone element */ createUnavailableZone(top, height) { const zone = document.createElement("swp-unavailable-zone"); zone.style.top = `${top}px`; zone.style.height = `${height}px`; return zone; } }; var defaultDBConfig = { dbName: "CalendarDB", dbVersion: 4 }; var IndexedDBContext = class { constructor(stores, config) { this.db = null; this.initialized = false; this.stores = stores; this.config = config; } get dbName() { return this.config.dbName; } /** * Initialize and open the database */ async initialize() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.config.dbName, this.config.dbVersion); request.onerror = () => { reject(new Error(`Failed to open IndexedDB: ${request.error}`)); }; request.onsuccess = () => { this.db = request.result; this.initialized = true; resolve(); }; request.onupgradeneeded = (event) => { const db = event.target.result; this.stores.forEach((store) => { if (!db.objectStoreNames.contains(store.storeName)) { store.create(db); } }); }; }); } /** * Check if database is initialized */ isInitialized() { return this.initialized; } /** * Get IDBDatabase instance */ getDatabase() { if (!this.db) { throw new Error("IndexedDB not initialized. Call initialize() first."); } return this.db; } /** * Close database connection */ close() { if (this.db) { this.db.close(); this.db = null; this.initialized = false; } } /** * Delete entire database (for testing/reset) */ static async deleteDatabase(dbName = defaultDBConfig.dbName) { return new Promise((resolve, reject) => { const request = indexedDB.deleteDatabase(dbName); request.onsuccess = () => resolve(); request.onerror = () => reject(new Error(`Failed to delete database: ${request.error}`)); }); } }; var EventStore = class _EventStore { constructor() { this.storeName = _EventStore.STORE_NAME; } /** * Create the events ObjectStore with indexes */ create(db) { const store = db.createObjectStore(_EventStore.STORE_NAME, { keyPath: "id" }); store.createIndex("start", "start", { unique: false }); store.createIndex("end", "end", { unique: false }); store.createIndex("syncStatus", "syncStatus", { unique: false }); store.createIndex("resourceId", "resourceId", { unique: false }); store.createIndex("customerId", "customerId", { unique: false }); store.createIndex("bookingId", "bookingId", { unique: false }); store.createIndex("startEnd", ["start", "end"], { unique: false }); } }; EventStore.STORE_NAME = "events"; var EventSerialization = class { /** * Serialize event for IndexedDB storage */ static serialize(event) { return { ...event, start: event.start instanceof Date ? event.start.toISOString() : event.start, end: event.end instanceof Date ? event.end.toISOString() : event.end }; } /** * Deserialize event from IndexedDB storage */ static deserialize(data) { return { ...data, start: typeof data.start === "string" ? new Date(data.start) : data.start, end: typeof data.end === "string" ? new Date(data.end) : data.end }; } }; var EventService = class extends BaseEntityService { constructor(context, eventBus) { super(context, eventBus); this.storeName = EventStore.STORE_NAME; this.entityType = "Event"; } serialize(event) { return EventSerialization.serialize(event); } deserialize(data) { return EventSerialization.deserialize(data); } /** * Get events within a date range */ async getByDateRange(start, end) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], "readonly"); const store = transaction.objectStore(this.storeName); const index = store.index("start"); const range = IDBKeyRange.lowerBound(start.toISOString()); const request = index.getAll(range); request.onsuccess = () => { const data = request.result; const events = data.map((item) => this.deserialize(item)).filter((event) => event.start <= end); resolve(events); }; request.onerror = () => { reject(new Error(`Failed to get events by date range: ${request.error}`)); }; }); } /** * Get events for a specific resource */ async getByResource(resourceId) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], "readonly"); const store = transaction.objectStore(this.storeName); const index = store.index("resourceId"); const request = index.getAll(resourceId); request.onsuccess = () => { const data = request.result; const events = data.map((item) => this.deserialize(item)); resolve(events); }; request.onerror = () => { reject(new Error(`Failed to get events for resource ${resourceId}: ${request.error}`)); }; }); } /** * Get events for a resource within a date range */ async getByResourceAndDateRange(resourceId, start, end) { const resourceEvents = await this.getByResource(resourceId); return resourceEvents.filter((event) => event.start >= start && event.start <= end); } }; var ResourceStore = class _ResourceStore { constructor() { this.storeName = _ResourceStore.STORE_NAME; } create(db) { const store = db.createObjectStore(_ResourceStore.STORE_NAME, { keyPath: "id" }); store.createIndex("type", "type", { unique: false }); store.createIndex("syncStatus", "syncStatus", { unique: false }); store.createIndex("isActive", "isActive", { unique: false }); } }; ResourceStore.STORE_NAME = "resources"; var ResourceService = class extends BaseEntityService { constructor(context, eventBus) { super(context, eventBus); this.storeName = ResourceStore.STORE_NAME; this.entityType = "Resource"; } /** * Get all active resources */ async getActive() { const all = await this.getAll(); return all.filter((r) => r.isActive !== false); } /** * Get resources by IDs */ async getByIds(ids) { if (ids.length === 0) return []; const results = await Promise.all(ids.map((id) => this.get(id))); return results.filter((r) => r !== null); } /** * Get resources by type */ async getByType(type) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.storeName], "readonly"); const store = transaction.objectStore(this.storeName); const index = store.index("type"); const request = index.getAll(type); request.onsuccess = () => { const data = request.result; resolve(data); }; request.onerror = () => { reject(new Error(`Failed to get resources by type ${type}: ${request.error}`)); }; }); } }; var SettingsIds = { WORKWEEK: "workweek", GRID: "grid", TIME_FORMAT: "timeFormat", VIEWS: "views" }; var SettingsStore = class _SettingsStore { constructor() { this.storeName = _SettingsStore.STORE_NAME; } create(db) { db.createObjectStore(_SettingsStore.STORE_NAME, { keyPath: "id" }); } }; SettingsStore.STORE_NAME = "settings"; var SettingsService = class extends BaseEntityService { constructor(context, eventBus) { super(context, eventBus); this.storeName = SettingsStore.STORE_NAME; this.entityType = "Settings"; } /** * Get workweek settings */ async getWorkweekSettings() { return this.get(SettingsIds.WORKWEEK); } /** * Get grid settings */ async getGridSettings() { return this.get(SettingsIds.GRID); } /** * Get time format settings */ async getTimeFormatSettings() { return this.get(SettingsIds.TIME_FORMAT); } /** * Get view settings */ async getViewSettings() { return this.get(SettingsIds.VIEWS); } /** * Get workweek preset by ID */ async getWorkweekPreset(presetId) { const settings = await this.getWorkweekSettings(); if (!settings) return null; return settings.presets[presetId] || null; } /** * Get the default workweek preset */ async getDefaultWorkweekPreset() { const settings = await this.getWorkweekSettings(); if (!settings) return null; return settings.presets[settings.defaultPreset] || null; } /** * Get all available workweek presets */ async getWorkweekPresets() { const settings = await this.getWorkweekSettings(); if (!settings) return []; return Object.values(settings.presets); } }; var ViewConfigStore = class _ViewConfigStore { constructor() { this.storeName = _ViewConfigStore.STORE_NAME; } create(db) { db.createObjectStore(_ViewConfigStore.STORE_NAME, { keyPath: "id" }); } }; ViewConfigStore.STORE_NAME = "viewconfigs"; var ViewConfigService = class extends BaseEntityService { constructor(context, eventBus) { super(context, eventBus); this.storeName = ViewConfigStore.STORE_NAME; this.entityType = "ViewConfig"; } async getById(id) { return this.get(id); } }; var SwpEvent = class _SwpEvent { constructor(element, columnKey, start, end) { this.element = element; this.columnKey = columnKey; this._start = start; this._end = end; } /** Event ID from element.dataset.eventId */ get eventId() { return this.element.dataset.eventId || ""; } get start() { return this._start; } get end() { return this._end; } /** Duration in minutes */ get durationMinutes() { return (this._end.getTime() - this._start.getTime()) / (1e3 * 60); } /** Duration in milliseconds */ get durationMs() { return this._end.getTime() - this._start.getTime(); } /** * Factory: Create SwpEvent from element + columnKey * Reads top/height from element.style to calculate start/end * @param columnKey - Opaque column identifier (do NOT parse - use only for matching) * @param date - Date string (YYYY-MM-DD) for time calculations */ static fromElement(element, columnKey, date, gridConfig) { const topPixels = parseFloat(element.style.top) || 0; const heightPixels = parseFloat(element.style.height) || 0; const startMinutesFromGrid = topPixels / gridConfig.hourHeight * 60; const totalMinutes = gridConfig.dayStartHour * 60 + startMinutesFromGrid; const start = new Date(date); start.setHours(Math.floor(totalMinutes / 60), totalMinutes % 60, 0, 0); const durationMinutes = heightPixels / gridConfig.hourHeight * 60; const end = new Date(start.getTime() + durationMinutes * 60 * 1e3); return new _SwpEvent(element, columnKey, start, end); } }; var DragDropManager = class { constructor(eventBus, gridConfig) { this.eventBus = eventBus; this.gridConfig = gridConfig; this.dragState = null; this.mouseDownPosition = null; this.pendingElement = null; this.pendingMouseOffset = null; this.container = null; this.inHeader = false; this.DRAG_THRESHOLD = 5; this.INTERPOLATION_FACTOR = 0.3; this.handlePointerDown = (e) => { const target = e.target; if (target.closest("swp-resize-handle")) return; const eventElement = target.closest("swp-event"); const headerItem = target.closest("swp-header-item"); const draggable = eventElement || headerItem; if (!draggable) return; this.mouseDownPosition = { x: e.clientX, y: e.clientY }; this.pendingElement = draggable; const rect = draggable.getBoundingClientRect(); this.pendingMouseOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; draggable.setPointerCapture(e.pointerId); }; this.handlePointerMove = (e) => { if (!this.mouseDownPosition || !this.pendingElement) { if (this.dragState) { this.updateDragTarget(e); } return; } const deltaX = Math.abs(e.clientX - this.mouseDownPosition.x); const deltaY = Math.abs(e.clientY - this.mouseDownPosition.y); const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); if (distance < this.DRAG_THRESHOLD) return; this.initializeDrag(this.pendingElement, this.pendingMouseOffset, e); this.mouseDownPosition = null; this.pendingElement = null; this.pendingMouseOffset = null; }; this.handlePointerUp = (_e) => { this.mouseDownPosition = null; this.pendingElement = null; this.pendingMouseOffset = null; if (!this.dragState) return; cancelAnimationFrame(this.dragState.animationId); if (this.dragState.dragSource === "header") { this.handleHeaderItemDragEnd(); } else { this.handleGridEventDragEnd(); } this.dragState.element.classList.remove("dragging"); this.dragState = null; this.inHeader = false; }; this.animateDrag = () => { if (!this.dragState) return; const diff2 = this.dragState.targetY - this.dragState.currentY; if (Math.abs(diff2) <= 0.5) { this.dragState.animationId = 0; return; } this.dragState.currentY += diff2 * this.INTERPOLATION_FACTOR; this.dragState.element.style.top = `${this.dragState.currentY}px`; if (this.dragState.columnElement) { const payload = { eventId: this.dragState.eventId, element: this.dragState.element, currentY: this.dragState.currentY, columnElement: this.dragState.columnElement }; this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE, payload); } this.dragState.animationId = requestAnimationFrame(this.animateDrag); }; this.setupScrollListener(); } setupScrollListener() { this.eventBus.on(CoreEvents.EDGE_SCROLL_TICK, (e) => { if (!this.dragState) return; const { scrollDelta } = e.detail; this.dragState.targetY += scrollDelta; this.dragState.currentY += scrollDelta; this.dragState.element.style.top = `${this.dragState.currentY}px`; }); } /** * Initialize drag-drop on a container element */ init(container) { this.container = container; container.addEventListener("pointerdown", this.handlePointerDown); document.addEventListener("pointermove", this.handlePointerMove); document.addEventListener("pointerup", this.handlePointerUp); } /** * Handle drag end for header items */ handleHeaderItemDragEnd() { if (!this.dragState) return; if (!this.inHeader && this.dragState.currentColumn) { const gridEvent = this.dragState.currentColumn.querySelector(`swp-event[data-event-id="${this.dragState.eventId}"]`); if (gridEvent) { const columnKey = this.dragState.currentColumn.dataset.columnKey || ""; const date = this.dragState.currentColumn.dataset.date || ""; const swpEvent = SwpEvent.fromElement(gridEvent, columnKey, date, this.gridConfig); const payload = { swpEvent, sourceColumnKey: this.dragState.sourceColumnKey, target: "grid" }; this.eventBus.emit(CoreEvents.EVENT_DRAG_END, payload); } } } /** * Handle drag end for grid events */ handleGridEventDragEnd() { if (!this.dragState || !this.dragState.columnElement) return; const snappedY = snapToGrid(this.dragState.currentY, this.gridConfig); this.dragState.element.style.top = `${snappedY}px`; this.dragState.ghostElement?.remove(); const columnKey = this.dragState.columnElement.dataset.columnKey || ""; const date = this.dragState.columnElement.dataset.date || ""; const swpEvent = SwpEvent.fromElement(this.dragState.element, columnKey, date, this.gridConfig); const payload = { swpEvent, sourceColumnKey: this.dragState.sourceColumnKey, target: this.inHeader ? "header" : "grid" }; this.eventBus.emit(CoreEvents.EVENT_DRAG_END, payload); } initializeDrag(element, mouseOffset, e) { const eventId = element.dataset.eventId || ""; const isHeaderItem = element.tagName.toLowerCase() === "swp-header-item"; const columnElement = element.closest("swp-day-column"); if (!isHeaderItem && !columnElement) return; if (isHeaderItem) { this.initializeHeaderItemDrag(element, mouseOffset, eventId); } else { this.initializeGridEventDrag(element, mouseOffset, e, columnElement, eventId); } } /** * Initialize drag for a header item (allDay event) */ initializeHeaderItemDrag(element, mouseOffset, eventId) { element.classList.add("dragging"); this.dragState = { eventId, element, ghostElement: null, // No ghost for header items startY: 0, mouseOffset, columnElement: null, currentColumn: null, targetY: 0, currentY: 0, animationId: 0, sourceColumnKey: "", // Will be set from header item data dragSource: "header" }; this.inHeader = true; } /** * Initialize drag for a grid event */ initializeGridEventDrag(element, mouseOffset, e, columnElement, eventId) { const elementRect = element.getBoundingClientRect(); const columnRect = columnElement.getBoundingClientRect(); const startY = elementRect.top - columnRect.top; const group = element.closest("swp-event-group"); if (group) { const eventsLayer = columnElement.querySelector("swp-events-layer"); if (eventsLayer) { eventsLayer.appendChild(element); } } element.style.position = "absolute"; element.style.top = `${startY}px`; element.style.left = "2px"; element.style.right = "2px"; element.style.marginLeft = "0"; const ghostElement = element.cloneNode(true); ghostElement.classList.add("drag-ghost"); ghostElement.style.opacity = "0.3"; ghostElement.style.pointerEvents = "none"; element.parentNode?.insertBefore(ghostElement, element); element.classList.add("dragging"); const targetY = e.clientY - columnRect.top - mouseOffset.y; this.dragState = { eventId, element, ghostElement, startY, mouseOffset, columnElement, currentColumn: columnElement, targetY: Math.max(0, targetY), currentY: startY, animationId: 0, sourceColumnKey: columnElement.dataset.columnKey || "", dragSource: "grid" }; const payload = { eventId, element, ghostElement, startY, mouseOffset, columnElement }; this.eventBus.emit(CoreEvents.EVENT_DRAG_START, payload); this.animateDrag(); } updateDragTarget(e) { if (!this.dragState) return; this.checkHeaderZone(e); if (this.inHeader) return; const columnAtPoint = this.getColumnAtPoint(e.clientX); if (this.dragState.dragSource === "header" && columnAtPoint && !this.dragState.currentColumn) { this.dragState.currentColumn = columnAtPoint; this.dragState.columnElement = columnAtPoint; } if (columnAtPoint && columnAtPoint !== this.dragState.currentColumn && this.dragState.currentColumn) { const payload = { eventId: this.dragState.eventId, element: this.dragState.element, previousColumn: this.dragState.currentColumn, newColumn: columnAtPoint, currentY: this.dragState.currentY }; this.eventBus.emit(CoreEvents.EVENT_DRAG_COLUMN_CHANGE, payload); this.dragState.currentColumn = columnAtPoint; this.dragState.columnElement = columnAtPoint; } if (!this.dragState.columnElement) return; const columnRect = this.dragState.columnElement.getBoundingClientRect(); const targetY = e.clientY - columnRect.top - this.dragState.mouseOffset.y; this.dragState.targetY = Math.max(0, targetY); if (!this.dragState.animationId) { this.animateDrag(); } } /** * Check if pointer is in header zone and emit appropriate events */ checkHeaderZone(e) { if (!this.dragState) return; const headerViewport = document.querySelector("swp-header-viewport"); if (!headerViewport) return; const rect = headerViewport.getBoundingClientRect(); const isInHeader = e.clientY < rect.bottom; if (isInHeader && !this.inHeader) { this.inHeader = true; if (this.dragState.dragSource === "grid" && this.dragState.columnElement) { const payload = { eventId: this.dragState.eventId, element: this.dragState.element, sourceColumnIndex: this.getColumnIndex(this.dragState.columnElement), sourceColumnKey: this.dragState.columnElement.dataset.columnKey || "", title: this.dragState.element.querySelector("swp-event-title")?.textContent || "", colorClass: [...this.dragState.element.classList].find((c) => c.startsWith("is-")), itemType: "event", duration: 1 }; this.eventBus.emit(CoreEvents.EVENT_DRAG_ENTER_HEADER, payload); } } else if (!isInHeader && this.inHeader) { this.inHeader = false; const targetColumn = this.getColumnAtPoint(e.clientX); if (this.dragState.dragSource === "header") { const payload = { eventId: this.dragState.eventId, source: "header", element: this.dragState.element, targetColumn: targetColumn || void 0, start: this.dragState.element.dataset.start ? new Date(this.dragState.element.dataset.start) : void 0, end: this.dragState.element.dataset.end ? new Date(this.dragState.element.dataset.end) : void 0, title: this.dragState.element.textContent || "", colorClass: [...this.dragState.element.classList].find((c) => c.startsWith("is-")) }; this.eventBus.emit(CoreEvents.EVENT_DRAG_LEAVE_HEADER, payload); if (targetColumn) { const newElement = targetColumn.querySelector(`swp-event[data-event-id="${this.dragState.eventId}"]`); if (newElement) { this.dragState.element = newElement; this.dragState.columnElement = targetColumn; this.dragState.currentColumn = targetColumn; this.animateDrag(); } } } else { const payload = { eventId: this.dragState.eventId, source: "grid" }; this.eventBus.emit(CoreEvents.EVENT_DRAG_LEAVE_HEADER, payload); } } else if (isInHeader) { const column = this.getColumnAtX(e.clientX); if (column) { const payload = { eventId: this.dragState.eventId, columnIndex: this.getColumnIndex(column), columnKey: column.dataset.columnKey || "" }; this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE_HEADER, payload); } } } /** * Get column index (0-based) for a column element */ getColumnIndex(column) { if (!this.container || !column) return 0; const columns = Array.from(this.container.querySelectorAll("swp-day-column")); return columns.indexOf(column); } /** * Get column at X coordinate (alias for getColumnAtPoint) */ getColumnAtX(clientX) { return this.getColumnAtPoint(clientX); } /** * Find column element at given X coordinate */ getColumnAtPoint(clientX) { if (!this.container) return null; const columns = this.container.querySelectorAll("swp-day-column"); for (const col of columns) { const rect = col.getBoundingClientRect(); if (clientX >= rect.left && clientX <= rect.right) { return col; } } return null; } /** * Cancel drag and animate back to start position */ cancelDrag() { if (!this.dragState) return; cancelAnimationFrame(this.dragState.animationId); const { element, ghostElement, startY, eventId } = this.dragState; element.style.transition = "top 200ms ease-out"; element.style.top = `${startY}px`; setTimeout(() => { ghostElement?.remove(); element.style.transition = ""; element.classList.remove("dragging"); }, 200); const payload = { eventId, element, startY }; this.eventBus.emit(CoreEvents.EVENT_DRAG_CANCEL, payload); this.dragState = null; this.inHeader = false; } }; var EdgeScrollManager = class { constructor(eventBus) { this.eventBus = eventBus; this.scrollableContent = null; this.timeGrid = null; this.draggedElement = null; this.scrollRAF = null; this.mouseY = 0; this.isDragging = false; this.isScrolling = false; this.lastTs = 0; this.rect = null; this.initialScrollTop = 0; this.OUTER_ZONE = 100; this.INNER_ZONE = 50; this.SLOW_SPEED = 140; this.FAST_SPEED = 640; this.trackMouse = (e) => { if (this.isDragging) { this.mouseY = e.clientY; } }; this.scrollTick = (ts) => { if (!this.isDragging || !this.scrollableContent) return; const dt = this.lastTs ? (ts - this.lastTs) / 1e3 : 0; this.lastTs = ts; this.rect ?? (this.rect = this.scrollableContent.getBoundingClientRect()); const velocity = this.calculateVelocity(); if (velocity !== 0 && !this.isAtBoundary(velocity)) { const scrollDelta = velocity * dt; this.scrollableContent.scrollTop += scrollDelta; this.rect = null; this.eventBus.emit(CoreEvents.EDGE_SCROLL_TICK, { scrollDelta }); this.setScrollingState(true); } else { this.setScrollingState(false); } this.scrollRAF = requestAnimationFrame(this.scrollTick); }; this.subscribeToEvents(); document.addEventListener("pointermove", this.trackMouse); } init(scrollableContent) { this.scrollableContent = scrollableContent; this.timeGrid = scrollableContent.querySelector("swp-time-grid"); this.scrollableContent.style.scrollBehavior = "auto"; } subscribeToEvents() { this.eventBus.on(CoreEvents.EVENT_DRAG_START, (event) => { const payload = event.detail; this.draggedElement = payload.element; this.startDrag(); }); this.eventBus.on(CoreEvents.EVENT_DRAG_END, () => this.stopDrag()); this.eventBus.on(CoreEvents.EVENT_DRAG_CANCEL, () => this.stopDrag()); } startDrag() { this.isDragging = true; this.isScrolling = false; this.lastTs = 0; this.initialScrollTop = this.scrollableContent?.scrollTop ?? 0; if (this.scrollRAF === null) { this.scrollRAF = requestAnimationFrame(this.scrollTick); } } stopDrag() { this.isDragging = false; this.setScrollingState(false); if (this.scrollRAF !== null) { cancelAnimationFrame(this.scrollRAF); this.scrollRAF = null; } this.rect = null; this.lastTs = 0; this.initialScrollTop = 0; } calculateVelocity() { if (!this.rect) return 0; const distTop = this.mouseY - this.rect.top; const distBot = this.rect.bottom - this.mouseY; if (distTop < this.INNER_ZONE) return -this.FAST_SPEED; if (distTop < this.OUTER_ZONE) return -this.SLOW_SPEED; if (distBot < this.INNER_ZONE) return this.FAST_SPEED; if (distBot < this.OUTER_ZONE) return this.SLOW_SPEED; return 0; } isAtBoundary(velocity) { if (!this.scrollableContent || !this.timeGrid || !this.draggedElement) return false; const atTop = this.scrollableContent.scrollTop <= 0 && velocity < 0; const atBottom = velocity > 0 && this.draggedElement.getBoundingClientRect().bottom >= this.timeGrid.getBoundingClientRect().bottom; return atTop || atBottom; } setScrollingState(scrolling) { if (this.isScrolling === scrolling) return; this.isScrolling = scrolling; if (scrolling) { this.eventBus.emit(CoreEvents.EDGE_SCROLL_STARTED, {}); } else { this.initialScrollTop = this.scrollableContent?.scrollTop ?? 0; this.eventBus.emit(CoreEvents.EDGE_SCROLL_STOPPED, {}); } } }; var ResizeManager = class { constructor(eventBus, gridConfig, dateService) { this.eventBus = eventBus; this.gridConfig = gridConfig; this.dateService = dateService; this.container = null; this.resizeState = null; this.Z_INDEX_RESIZING = "1000"; this.ANIMATION_SPEED = 0.35; this.MIN_HEIGHT_MINUTES = 15; this.handleMouseOver = (e) => { const target = e.target; const eventElement = target.closest("swp-event"); if (!eventElement || this.resizeState) return; if (!eventElement.querySelector(":scope > swp-resize-handle")) { const handle = this.createResizeHandle(); eventElement.appendChild(handle); } }; this.handlePointerDown = (e) => { const handle = e.target.closest("swp-resize-handle"); if (!handle) return; const element = handle.parentElement; if (!element) return; const eventId = element.dataset.eventId || ""; const startHeight = element.offsetHeight; const startDurationMinutes = pixelsToMinutes(startHeight, this.gridConfig); const container = element.closest("swp-event-group") ?? element; const prevZIndex = container.style.zIndex; this.resizeState = { eventId, element, handleElement: handle, startY: e.clientY, startHeight, startDurationMinutes, pointerId: e.pointerId, prevZIndex, // Animation state currentHeight: startHeight, targetHeight: startHeight, animationId: null }; container.style.zIndex = this.Z_INDEX_RESIZING; try { handle.setPointerCapture(e.pointerId); } catch (err) { console.warn("Pointer capture failed:", err); } document.documentElement.classList.add("swp--resizing"); this.eventBus.emit(CoreEvents.EVENT_RESIZE_START, { eventId, element, startHeight }); e.preventDefault(); }; this.handlePointerMove = (e) => { if (!this.resizeState) return; const deltaY = e.clientY - this.resizeState.startY; const minHeight = this.MIN_HEIGHT_MINUTES / 60 * this.gridConfig.hourHeight; const newHeight = Math.max(minHeight, this.resizeState.startHeight + deltaY); this.resizeState.targetHeight = newHeight; if (this.resizeState.animationId === null) { this.animateHeight(); } }; this.animateHeight = () => { if (!this.resizeState) return; const diff2 = this.resizeState.targetHeight - this.resizeState.currentHeight; if (Math.abs(diff2) < 0.5) { this.resizeState.animationId = null; return; } this.resizeState.currentHeight += diff2 * this.ANIMATION_SPEED; this.resizeState.element.style.height = `${this.resizeState.currentHeight}px`; this.updateTimestampDisplay(); this.resizeState.animationId = requestAnimationFrame(this.animateHeight); }; this.handlePointerUp = (e) => { if (!this.resizeState) return; if (this.resizeState.animationId !== null) { cancelAnimationFrame(this.resizeState.animationId); } try { this.resizeState.handleElement.releasePointerCapture(e.pointerId); } catch (err) { console.warn("Pointer release failed:", err); } this.snapToGridFinal(); this.updateTimestampDisplay(); const container = this.resizeState.element.closest("swp-event-group") ?? this.resizeState.element; container.style.zIndex = this.resizeState.prevZIndex; document.documentElement.classList.remove("swp--resizing"); const column = this.resizeState.element.closest("swp-day-column"); const columnKey = column?.dataset.columnKey || ""; const date = column?.dataset.date || ""; const swpEvent = SwpEvent.fromElement(this.resizeState.element, columnKey, date, this.gridConfig); this.eventBus.emit(CoreEvents.EVENT_RESIZE_END, { swpEvent }); this.resizeState = null; }; } /** * Initialize resize functionality on container */ init(container) { this.container = container; container.addEventListener("mouseover", this.handleMouseOver, true); document.addEventListener("pointerdown", this.handlePointerDown, true); document.addEventListener("pointermove", this.handlePointerMove, true); document.addEventListener("pointerup", this.handlePointerUp, true); } /** * Create resize handle element */ createResizeHandle() { const handle = document.createElement("swp-resize-handle"); handle.setAttribute("aria-label", "Resize event"); handle.setAttribute("role", "separator"); return handle; } /** * Update timestamp display with snapped end time */ updateTimestampDisplay() { if (!this.resizeState) return; const timeEl = this.resizeState.element.querySelector("swp-event-time"); if (!timeEl) return; const top = parseFloat(this.resizeState.element.style.top) || 0; const startMinutesFromGrid = pixelsToMinutes(top, this.gridConfig); const startMinutes = this.gridConfig.dayStartHour * 60 + startMinutesFromGrid; const snappedHeight = snapToGrid(this.resizeState.currentHeight, this.gridConfig); const durationMinutes = pixelsToMinutes(snappedHeight, this.gridConfig); const endMinutes = startMinutes + durationMinutes; const start = this.minutesToDate(startMinutes); const end = this.minutesToDate(endMinutes); timeEl.textContent = this.dateService.formatTimeRange(start, end); } /** * Convert minutes since midnight to Date */ minutesToDate(minutes) { const date = /* @__PURE__ */ new Date(); date.setHours(Math.floor(minutes / 60) % 24, minutes % 60, 0, 0); return date; } /** * Snap final height to grid interval */ snapToGridFinal() { if (!this.resizeState) return; const currentHeight = this.resizeState.element.offsetHeight; const snappedHeight = snapToGrid(currentHeight, this.gridConfig); const minHeight = minutesToPixels(this.MIN_HEIGHT_MINUTES, this.gridConfig); const finalHeight = Math.max(minHeight, snappedHeight); this.resizeState.element.style.height = `${finalHeight}px`; this.resizeState.currentHeight = finalHeight; } }; var EventPersistenceManager = class { constructor(eventService, eventBus, dateService) { this.eventService = eventService; this.eventBus = eventBus; this.dateService = dateService; this.handleDragEnd = async (e) => { const payload = e.detail; const { swpEvent } = payload; const event = await this.eventService.get(swpEvent.eventId); if (!event) { console.warn(`EventPersistenceManager: Event ${swpEvent.eventId} not found`); return; } const { resource } = this.dateService.parseColumnKey(swpEvent.columnKey); const updatedEvent = { ...event, start: swpEvent.start, end: swpEvent.end, resourceId: resource ?? event.resourceId, allDay: payload.target === "header", syncStatus: "pending" }; await this.eventService.save(updatedEvent); const updatePayload = { eventId: updatedEvent.id, sourceColumnKey: payload.sourceColumnKey, targetColumnKey: swpEvent.columnKey }; this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload); }; this.handleResizeEnd = async (e) => { const payload = e.detail; const { swpEvent } = payload; const event = await this.eventService.get(swpEvent.eventId); if (!event) { console.warn(`EventPersistenceManager: Event ${swpEvent.eventId} not found`); return; } const updatedEvent = { ...event, end: swpEvent.end, syncStatus: "pending" }; await this.eventService.save(updatedEvent); const updatePayload = { eventId: updatedEvent.id, sourceColumnKey: swpEvent.columnKey, targetColumnKey: swpEvent.columnKey }; this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload); }; this.setupListeners(); } setupListeners() { this.eventBus.on(CoreEvents.EVENT_DRAG_END, this.handleDragEnd); this.eventBus.on(CoreEvents.EVENT_RESIZE_END, this.handleResizeEnd); } }; var defaultTimeFormatConfig = { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, use24HourFormat: true, locale: "da-DK", dateFormat: "locale", showSeconds: false }; var defaultGridConfig = { hourHeight: 64, dayStartHour: 6, dayEndHour: 18, snapInterval: 15, gridStartThresholdMinutes: 30 }; function registerCoreServices(builder, options) { const timeConfig = options?.timeConfig ?? defaultTimeFormatConfig; const gridConfig = options?.gridConfig ?? defaultGridConfig; const dbConfig = options?.dbConfig ?? defaultDBConfig; builder.registerInstance(timeConfig).as("ITimeFormatConfig"); builder.registerInstance(gridConfig).as("IGridConfig"); builder.registerInstance(dbConfig).as("IDBConfig"); builder.registerType(EventBus).as("EventBus"); builder.registerType(EventBus).as("IEventBus"); builder.registerType(DateService).as("DateService").autoWire({ mapResolvers: [ (c) => c.resolveType("ITimeFormatConfig"), void 0 ] }); builder.registerType(IndexedDBContext).as("IndexedDBContext").autoWire({ mapResolvers: [ (c) => c.resolveTypeAll("IStore"), (c) => c.resolveType("IDBConfig") ] }); builder.registerType(EventStore).as("IStore"); builder.registerType(ResourceStore).as("IStore"); builder.registerType(SettingsStore).as("IStore"); builder.registerType(ViewConfigStore).as("IStore"); builder.registerType(EventService).as("IEntityService").autoWire({ mapResolvers: [ (c) => c.resolveType("IndexedDBContext"), (c) => c.resolveType("IEventBus") ] }); builder.registerType(EventService).as("IEntityService").autoWire({ mapResolvers: [ (c) => c.resolveType("IndexedDBContext"), (c) => c.resolveType("IEventBus") ] }); builder.registerType(EventService).as("EventService").autoWire({ mapResolvers: [ (c) => c.resolveType("IndexedDBContext"), (c) => c.resolveType("IEventBus") ] }); builder.registerType(ResourceService).as("IEntityService").autoWire({ mapResolvers: [ (c) => c.resolveType("IndexedDBContext"), (c) => c.resolveType("IEventBus") ] }); builder.registerType(ResourceService).as("IEntityService").autoWire({ mapResolvers: [ (c) => c.resolveType("IndexedDBContext"), (c) => c.resolveType("IEventBus") ] }); builder.registerType(ResourceService).as("ResourceService").autoWire({ mapResolvers: [ (c) => c.resolveType("IndexedDBContext"), (c) => c.resolveType("IEventBus") ] }); builder.registerType(SettingsService).as("IEntityService").autoWire({ mapResolvers: [ (c) => c.resolveType("IndexedDBContext"), (c) => c.resolveType("IEventBus") ] }); builder.registerType(SettingsService).as("IEntityService").autoWire({ mapResolvers: [ (c) => c.resolveType("IndexedDBContext"), (c) => c.resolveType("IEventBus") ] }); builder.registerType(SettingsService).as("SettingsService").autoWire({ mapResolvers: [ (c) => c.resolveType("IndexedDBContext"), (c) => c.resolveType("IEventBus") ] }); builder.registerType(ViewConfigService).as("IEntityService").autoWire({ mapResolvers: [ (c) => c.resolveType("IndexedDBContext"), (c) => c.resolveType("IEventBus") ] }); builder.registerType(ViewConfigService).as("IEntityService").autoWire({ mapResolvers: [ (c) => c.resolveType("IndexedDBContext"), (c) => c.resolveType("IEventBus") ] }); builder.registerType(ViewConfigService).as("ViewConfigService").autoWire({ mapResolvers: [ (c) => c.resolveType("IndexedDBContext"), (c) => c.resolveType("IEventBus") ] }); builder.registerType(EventRenderer).as("EventRenderer").autoWire({ mapResolvers: [ (c) => c.resolveType("EventService"), (c) => c.resolveType("DateService"), (c) => c.resolveType("IGridConfig"), (c) => c.resolveType("IEventBus") ] }); builder.registerType(ScheduleRenderer).as("ScheduleRenderer").autoWire({ mapResolvers: [ (c) => c.resolveType("ResourceScheduleService"), (c) => c.resolveType("DateService"), (c) => c.resolveType("IGridConfig") ] }); builder.registerType(HeaderDrawerRenderer).as("HeaderDrawerRenderer").autoWire({ mapResolvers: [ (c) => c.resolveType("IEventBus"), (c) => c.resolveType("IGridConfig"), (c) => c.resolveType("HeaderDrawerManager"), (c) => c.resolveType("EventService"), (c) => c.resolveType("DateService") ] }); builder.registerType(TimeAxisRenderer).as("TimeAxisRenderer"); builder.registerType(DateRenderer).as("IRenderer").autoWire({ mapResolvers: [ (c) => c.resolveType("DateService") ] }); builder.registerType(ResourceRenderer).as("IRenderer").autoWire({ mapResolvers: [ (c) => c.resolveType("ResourceService") ] }); builder.registerType(ScrollManager).as("ScrollManager"); builder.registerType(HeaderDrawerManager).as("HeaderDrawerManager"); builder.registerType(DragDropManager).as("DragDropManager").autoWire({ mapResolvers: [ (c) => c.resolveType("IEventBus"), (c) => c.resolveType("IGridConfig") ] }); builder.registerType(EdgeScrollManager).as("EdgeScrollManager").autoWire({ mapResolvers: [ (c) => c.resolveType("IEventBus") ] }); builder.registerType(ResizeManager).as("ResizeManager").autoWire({ mapResolvers: [ (c) => c.resolveType("IEventBus"), (c) => c.resolveType("IGridConfig"), (c) => c.resolveType("DateService") ] }); builder.registerType(EventPersistenceManager).as("EventPersistenceManager").autoWire({ mapResolvers: [ (c) => c.resolveType("EventService"), (c) => c.resolveType("IEventBus"), (c) => c.resolveType("DateService") ] }); builder.registerType(CalendarOrchestrator).as("CalendarOrchestrator").autoWire({ mapResolvers: [ (c) => c.resolveTypeAll("IRenderer"), (c) => c.resolveType("EventRenderer"), (c) => c.resolveType("ScheduleRenderer"), (c) => c.resolveType("HeaderDrawerRenderer"), (c) => c.resolveType("DateService"), (c) => c.resolveTypeAll("IEntityService") ] }); builder.registerType(CalendarApp).as("CalendarApp").autoWire({ mapResolvers: [ (c) => c.resolveType("CalendarOrchestrator"), (c) => c.resolveType("TimeAxisRenderer"), (c) => c.resolveType("DateService"), (c) => c.resolveType("ScrollManager"), (c) => c.resolveType("HeaderDrawerManager"), (c) => c.resolveType("DragDropManager"), (c) => c.resolveType("EdgeScrollManager"), (c) => c.resolveType("ResizeManager"), (c) => c.resolveType("HeaderDrawerRenderer"), (c) => c.resolveType("EventPersistenceManager"), (c) => c.resolveType("SettingsService"), (c) => c.resolveType("ViewConfigService"), (c) => c.resolveType("IEventBus") ] }); } // node_modules/calendar/dist/extensions/schedules/index.js var ScheduleOverrideStore = class _ScheduleOverrideStore { constructor() { this.storeName = _ScheduleOverrideStore.STORE_NAME; } create(db) { const store = db.createObjectStore(_ScheduleOverrideStore.STORE_NAME, { keyPath: "id" }); store.createIndex("resourceId", "resourceId", { unique: false }); store.createIndex("date", "date", { unique: false }); store.createIndex("resourceId_date", ["resourceId", "date"], { unique: true }); store.createIndex("syncStatus", "syncStatus", { unique: false }); } }; ScheduleOverrideStore.STORE_NAME = "scheduleOverrides"; var ScheduleOverrideService = class { constructor(context) { this.context = context; } get db() { return this.context.getDatabase(); } /** * Get override for a specific resource and date */ async getOverride(resourceId, date) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([ScheduleOverrideStore.STORE_NAME], "readonly"); const store = transaction.objectStore(ScheduleOverrideStore.STORE_NAME); const index = store.index("resourceId_date"); const request = index.get([resourceId, date]); request.onsuccess = () => { resolve(request.result || null); }; request.onerror = () => { reject(new Error(`Failed to get override for ${resourceId} on ${date}: ${request.error}`)); }; }); } /** * Get all overrides for a resource */ async getByResource(resourceId) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([ScheduleOverrideStore.STORE_NAME], "readonly"); const store = transaction.objectStore(ScheduleOverrideStore.STORE_NAME); const index = store.index("resourceId"); const request = index.getAll(resourceId); request.onsuccess = () => { resolve(request.result || []); }; request.onerror = () => { reject(new Error(`Failed to get overrides for ${resourceId}: ${request.error}`)); }; }); } /** * Get overrides for a date range */ async getByDateRange(resourceId, startDate, endDate) { const all = await this.getByResource(resourceId); return all.filter((o) => o.date >= startDate && o.date <= endDate); } /** * Save an override */ async save(override) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([ScheduleOverrideStore.STORE_NAME], "readwrite"); const store = transaction.objectStore(ScheduleOverrideStore.STORE_NAME); const request = store.put(override); request.onsuccess = () => resolve(); request.onerror = () => { reject(new Error(`Failed to save override ${override.id}: ${request.error}`)); }; }); } /** * Delete an override */ async delete(id) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([ScheduleOverrideStore.STORE_NAME], "readwrite"); const store = transaction.objectStore(ScheduleOverrideStore.STORE_NAME); const request = store.delete(id); request.onsuccess = () => resolve(); request.onerror = () => { reject(new Error(`Failed to delete override ${id}: ${request.error}`)); }; }); } }; var ResourceScheduleService = class { constructor(resourceService, overrideService, dateService) { this.resourceService = resourceService; this.overrideService = overrideService; this.dateService = dateService; } /** * Get effective schedule for a resource on a specific date * * @param resourceId - Resource ID * @param date - Date string "YYYY-MM-DD" * @returns ITimeSlot or null (fri/closed) */ async getScheduleForDate(resourceId, date) { const override = await this.overrideService.getOverride(resourceId, date); if (override) { return override.schedule; } const resource = await this.resourceService.get(resourceId); if (!resource || !resource.defaultSchedule) { return null; } const weekDay = this.dateService.getISOWeekDay(date); return resource.defaultSchedule[weekDay] || null; } /** * Get schedules for multiple dates * * @param resourceId - Resource ID * @param dates - Array of date strings "YYYY-MM-DD" * @returns Map of date -> ITimeSlot | null */ async getSchedulesForDates(resourceId, dates) { const result = /* @__PURE__ */ new Map(); const resource = await this.resourceService.get(resourceId); const overrides = dates.length > 0 ? await this.overrideService.getByDateRange(resourceId, dates[0], dates[dates.length - 1]) : []; const overrideMap = new Map(overrides.map((o) => [o.date, o.schedule])); for (const date of dates) { if (overrideMap.has(date)) { result.set(date, overrideMap.get(date)); continue; } if (resource?.defaultSchedule) { const weekDay = this.dateService.getISOWeekDay(date); result.set(date, resource.defaultSchedule[weekDay] || null); } else { result.set(date, null); } } return result; } }; function registerSchedules(builder) { builder.registerType(ScheduleOverrideStore).as("IStore"); builder.registerType(ScheduleOverrideService).as("ScheduleOverrideService").autoWire({ mapResolvers: [ (c) => c.resolveType("IndexedDBContext") ] }); builder.registerType(ResourceScheduleService).as("ResourceScheduleService").autoWire({ mapResolvers: [ (c) => c.resolveType("ResourceService"), (c) => c.resolveType("ScheduleOverrideService"), (c) => c.resolveType("DateService") ] }); } // src/index.ts async function init() { const databases = await indexedDB.databases(); const dbExists = databases.some((db) => db.name === "CalendarTestDB"); const container = new Container(); const builder = container.builder(); registerCoreServices(builder, { dbConfig: { dbName: "CalendarTestDB", dbVersion: 4 } }); registerSchedules(builder); const app = builder.build(); console.log("Container created"); const dbContext = app.resolveType("IndexedDBContext"); await dbContext.initialize(); console.log("IndexedDB initialized"); const settingsService = app.resolveType("SettingsService"); const viewConfigService = app.resolveType("ViewConfigService"); const eventService = app.resolveType("EventService"); if (dbExists) { console.log("Database exists, skipping seed"); } else { console.log("Seeding data..."); await seedData(settingsService, viewConfigService, eventService); } const calendarApp = app.resolveType("CalendarApp"); const containerEl = document.querySelector("swp-calendar-container"); await calendarApp.init(containerEl); const eventBus = app.resolveType("EventBus"); eventBus.emit(CalendarEvents.CMD_RENDER, { viewId: "simple" }); console.log("Calendar rendered"); document.addEventListener("event:drag-end", (e) => { console.log("event:drag-end:", e.detail); }); document.addEventListener("event:updated", (e) => { console.log("event:updated:", e.detail); }); const persistenceManager = app.resolveType("EventPersistenceManager"); console.log("EventPersistenceManager resolved:", persistenceManager); } async function seedData(settingsService, viewConfigService, eventService) { await settingsService.save({ id: "grid", dayStartHour: 8, dayEndHour: 17, workStartHour: 9, workEndHour: 16, hourHeight: 64, snapInterval: 15, syncStatus: "synced" }); await settingsService.save({ id: "workweek", presets: { standard: { id: "standard", label: "Standard", workDays: [1, 2, 3, 4, 5], periodDays: 7 } }, defaultPreset: "standard", firstDayOfWeek: 1, syncStatus: "synced" }); await viewConfigService.save({ id: "simple", groupings: [{ type: "date", values: [], idProperty: "date", derivedFrom: "start" }], syncStatus: "synced" }); const today = /* @__PURE__ */ new Date(); today.setHours(0, 0, 0, 0); console.log("Event date:", today.toISOString()); const start1 = new Date(today); start1.setHours(9, 0, 0, 0); const end1 = new Date(today); end1.setHours(10, 0, 0, 0); await eventService.save({ id: "1", title: "Morgenm\xF8de", start: start1, end: end1, type: "meeting", allDay: false, syncStatus: "synced" }); const start2 = new Date(today); start2.setHours(12, 0, 0, 0); const end2 = new Date(today); end2.setHours(13, 0, 0, 0); await eventService.save({ id: "2", title: "Frokost", start: start2, end: end2, type: "break", allDay: false, syncStatus: "synced" }); } init().catch(console.error);