6464 lines
795 KiB
JavaScript
6464 lines
795 KiB
JavaScript
|
|
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 __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
||
|
|
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 = /* @__PURE__ */ __name(function(t2, e2, n2) {
|
||
|
|
var r2 = String(t2);
|
||
|
|
return !r2 || r2.length >= e2 ? t2 : "" + Array(e2 + 1 - r2.length).join(n2) + t2;
|
||
|
|
}, "m"), 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: /* @__PURE__ */ __name(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);
|
||
|
|
}, "t"), 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 = /* @__PURE__ */ __name(function(t2) {
|
||
|
|
return t2 instanceof _ || !(!t2 || !t2[p]);
|
||
|
|
}, "S"), w = /* @__PURE__ */ __name(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;
|
||
|
|
}, "t"), O = /* @__PURE__ */ __name(function(t2, e2) {
|
||
|
|
if (S(t2))
|
||
|
|
return t2.clone();
|
||
|
|
var n2 = "object" == typeof e2 ? e2 : {};
|
||
|
|
return n2.date = t2, n2.args = arguments, new _(n2);
|
||
|
|
}, "O"), 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;
|
||
|
|
}
|
||
|
|
__name(M2, "M");
|
||
|
|
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 = /* @__PURE__ */ __name(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);
|
||
|
|
}, "l"), $2 = /* @__PURE__ */ __name(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 = /* @__PURE__ */ __name(function(t2) {
|
||
|
|
var e2 = O(l2);
|
||
|
|
return b.w(e2.date(e2.date() + Math.round(t2 * r2)), l2);
|
||
|
|
}, "y");
|
||
|
|
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 = /* @__PURE__ */ __name(function(t3, n3, i3, s3) {
|
||
|
|
return t3 && (t3[n3] || t3(e2, r2)) || i3[n3].slice(0, s3);
|
||
|
|
}, "h"), d2 = /* @__PURE__ */ __name(function(t3) {
|
||
|
|
return b.s(s2 % 12 || 12, t3, "0");
|
||
|
|
}, "d"), $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 = /* @__PURE__ */ __name(function() {
|
||
|
|
return b.m(y2, m3);
|
||
|
|
}, "D");
|
||
|
|
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 = /* @__PURE__ */ __name(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);
|
||
|
|
}, "a"), u = /* @__PURE__ */ __name(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;
|
||
|
|
}, "u"), 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 = /* @__PURE__ */ __name(function(t2) {
|
||
|
|
return t2.add(4 - t2.isoWeekday(), e);
|
||
|
|
}, "a"), 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 = Symbol(description ? `Token(${description})` : `Token#${id}`);
|
||
|
|
const token2 = {
|
||
|
|
symbol: sym,
|
||
|
|
description,
|
||
|
|
toString() {
|
||
|
|
return description ? `Token<${description}>` : `Token<#${id}>`;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
return token2;
|
||
|
|
}
|
||
|
|
__name(Token, "Token");
|
||
|
|
|
||
|
|
// node_modules/@novadi/core/dist/errors.js
|
||
|
|
var _ContainerError = class _ContainerError extends Error {
|
||
|
|
constructor(message) {
|
||
|
|
super(message);
|
||
|
|
this.name = "ContainerError";
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ContainerError, "ContainerError");
|
||
|
|
var ContainerError = _ContainerError;
|
||
|
|
var _BindingNotFoundError = class _BindingNotFoundError 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";
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_BindingNotFoundError, "BindingNotFoundError");
|
||
|
|
var BindingNotFoundError = _BindingNotFoundError;
|
||
|
|
var _CircularDependencyError = class _CircularDependencyError extends ContainerError {
|
||
|
|
constructor(path) {
|
||
|
|
super(`Circular dependency detected: ${path.join(" -> ")}`);
|
||
|
|
this.name = "CircularDependencyError";
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_CircularDependencyError, "CircularDependencyError");
|
||
|
|
var CircularDependencyError = _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;
|
||
|
|
}
|
||
|
|
__name(extractParameterNames, "extractParameterNames");
|
||
|
|
function resolveByMap(constructor, container2, 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(container2));
|
||
|
|
} else {
|
||
|
|
resolvedDeps.push(container2.resolve(resolver));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return resolvedDeps;
|
||
|
|
}
|
||
|
|
__name(resolveByMap, "resolveByMap");
|
||
|
|
function resolveByMapResolvers(_constructor, container2, 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(container2));
|
||
|
|
} else {
|
||
|
|
resolvedDeps.push(container2.resolve(resolver));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return resolvedDeps;
|
||
|
|
}
|
||
|
|
__name(resolveByMapResolvers, "resolveByMapResolvers");
|
||
|
|
function autowire(constructor, container2, options) {
|
||
|
|
const opts = {
|
||
|
|
by: "paramName",
|
||
|
|
strict: false,
|
||
|
|
...options
|
||
|
|
};
|
||
|
|
if (opts.mapResolvers && opts.mapResolvers.length > 0) {
|
||
|
|
return resolveByMapResolvers(constructor, container2, opts);
|
||
|
|
}
|
||
|
|
if (opts.map && Object.keys(opts.map).length > 0) {
|
||
|
|
return resolveByMap(constructor, container2, opts);
|
||
|
|
}
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
__name(autowire, "autowire");
|
||
|
|
|
||
|
|
// node_modules/@novadi/core/dist/builder.js
|
||
|
|
var _RegistrationBuilder = class _RegistrationBuilder {
|
||
|
|
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<U>} 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<IEventBus>().autoWire()
|
||
|
|
*
|
||
|
|
* // Strategy 2: map (minify-safe, explicit)
|
||
|
|
* builder.registerType(EventBus).as<IEventBus>().autoWire({
|
||
|
|
* map: {
|
||
|
|
* logger: (c) => c.resolveType<ILogger>()
|
||
|
|
* }
|
||
|
|
* })
|
||
|
|
*
|
||
|
|
* // Strategy 3: class (requires build-time codegen)
|
||
|
|
* builder.registerType(EventBus).as<IEventBus>().autoWire({ by: 'class' })
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
autoWire(options) {
|
||
|
|
for (const config of this.configs) {
|
||
|
|
config.autowireOptions = options || { by: "paramName", strict: false };
|
||
|
|
}
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_RegistrationBuilder, "RegistrationBuilder");
|
||
|
|
var RegistrationBuilder = _RegistrationBuilder;
|
||
|
|
var _Builder = class _Builder {
|
||
|
|
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(container2) {
|
||
|
|
for (const config of this.registrations) {
|
||
|
|
if (config.interfaceType !== void 0 && !config.token) {
|
||
|
|
config.token = container2.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(container2, config, bindingToken, registeredTokens) {
|
||
|
|
if (config.additionalTokens) {
|
||
|
|
for (const additionalToken of config.additionalTokens) {
|
||
|
|
container2.bindFactory(additionalToken, (c) => c.resolve(bindingToken), { lifetime: config.lifetime });
|
||
|
|
registeredTokens.add(additionalToken);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Build the container with all registered bindings
|
||
|
|
*/
|
||
|
|
build() {
|
||
|
|
const container2 = this.baseContainer.createChild();
|
||
|
|
this.resolveInterfaceTokens(container2);
|
||
|
|
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(container2, { ...config, token: bindingToken });
|
||
|
|
registeredTokens.add(config.token);
|
||
|
|
this.registerAdditionalInterfaces(container2, config, bindingToken, registeredTokens);
|
||
|
|
}
|
||
|
|
;
|
||
|
|
container2.__namedRegistrations = namedRegistrations;
|
||
|
|
container2.__keyedRegistrations = keyedRegistrations;
|
||
|
|
container2.__multiRegistrations = multiRegistrations;
|
||
|
|
return container2;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* 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(container2, config, options) {
|
||
|
|
if (config.lifetime === "singleton") {
|
||
|
|
const instance = new config.constructor();
|
||
|
|
container2.bindValue(config.token, instance);
|
||
|
|
} else if (config.lifetime === "transient") {
|
||
|
|
const ctor = config.constructor;
|
||
|
|
const fastFactory = /* @__PURE__ */ __name(() => new ctor(), "fastFactory");
|
||
|
|
container2.fastTransientCache.set(config.token, fastFactory);
|
||
|
|
container2.bindFactory(config.token, fastFactory, options);
|
||
|
|
} else {
|
||
|
|
const factory = /* @__PURE__ */ __name(() => new config.constructor(), "factory");
|
||
|
|
container2.bindFactory(config.token, factory, options);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Create autowire factory
|
||
|
|
* @internal
|
||
|
|
*/
|
||
|
|
createAutoWireFactory(container2, config, options) {
|
||
|
|
const factory = /* @__PURE__ */ __name((c) => {
|
||
|
|
const resolvedDeps = autowire(config.constructor, c, config.autowireOptions);
|
||
|
|
return new config.constructor(...resolvedDeps);
|
||
|
|
}, "factory");
|
||
|
|
container2.bindFactory(config.token, factory, options);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Create withParameters factory
|
||
|
|
* @internal
|
||
|
|
*/
|
||
|
|
createParameterFactory(container2, config, options) {
|
||
|
|
const factory = /* @__PURE__ */ __name(() => {
|
||
|
|
const values = Object.values(config.parameterValues);
|
||
|
|
return new config.constructor(...values);
|
||
|
|
}, "factory");
|
||
|
|
container2.bindFactory(config.token, factory, options);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Apply type registration (class constructor)
|
||
|
|
* @internal
|
||
|
|
*/
|
||
|
|
applyTypeRegistration(container2, config, options) {
|
||
|
|
const { hasDependencies } = this.analyzeConstructor(config.constructor);
|
||
|
|
if (!hasDependencies && !config.autowireOptions && !config.parameterValues) {
|
||
|
|
this.createOptimizedFactory(container2, config, options);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (config.autowireOptions) {
|
||
|
|
this.createAutoWireFactory(container2, config, options);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (config.parameterValues) {
|
||
|
|
this.createParameterFactory(container2, 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 = /* @__PURE__ */ __name(() => new config.constructor(), "factory");
|
||
|
|
container2.bindFactory(config.token, factory, options);
|
||
|
|
}
|
||
|
|
applyRegistration(container2, config) {
|
||
|
|
const options = { lifetime: config.lifetime };
|
||
|
|
switch (config.type) {
|
||
|
|
case "instance":
|
||
|
|
container2.bindValue(config.token, config.value);
|
||
|
|
break;
|
||
|
|
case "factory":
|
||
|
|
container2.bindFactory(config.token, config.factory, options);
|
||
|
|
break;
|
||
|
|
case "type":
|
||
|
|
this.applyTypeRegistration(container2, config, options);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_Builder, "Builder");
|
||
|
|
var Builder = _Builder;
|
||
|
|
|
||
|
|
// node_modules/@novadi/core/dist/container.js
|
||
|
|
function isDisposable(obj) {
|
||
|
|
return obj && typeof obj.dispose === "function";
|
||
|
|
}
|
||
|
|
__name(isDisposable, "isDisposable");
|
||
|
|
var _ResolutionContext = class _ResolutionContext {
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ResolutionContext, "ResolutionContext");
|
||
|
|
var ResolutionContext = _ResolutionContext;
|
||
|
|
var _ResolutionContextPool = class _ResolutionContextPool {
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ResolutionContextPool, "ResolutionContextPool");
|
||
|
|
var ResolutionContextPool = _ResolutionContextPool;
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_Container, "Container");
|
||
|
|
var Container = _Container;
|
||
|
|
Container.contextPool = new ResolutionContextPool();
|
||
|
|
|
||
|
|
// src/v2/features/date/DateRenderer.ts
|
||
|
|
var _DateRenderer = class _DateRenderer {
|
||
|
|
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 = `
|
||
|
|
<swp-day-name>${this.dateService.getDayName(date, "short")}</swp-day-name>
|
||
|
|
<swp-day-date>${date.getDate()}</swp-day-date>
|
||
|
|
`;
|
||
|
|
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 = "<swp-events-layer></swp-events-layer>";
|
||
|
|
context.columnContainer.appendChild(column);
|
||
|
|
columnCount++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const container2 = context.columnContainer.closest("swp-calendar-container");
|
||
|
|
if (container2) {
|
||
|
|
container2.style.setProperty("--grid-columns", String(columnCount));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_DateRenderer, "DateRenderer");
|
||
|
|
var DateRenderer = _DateRenderer;
|
||
|
|
|
||
|
|
// src/v2/core/DateService.ts
|
||
|
|
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);
|
||
|
|
import_dayjs.default.extend(import_utc.default);
|
||
|
|
import_dayjs.default.extend(import_timezone.default);
|
||
|
|
import_dayjs.default.extend(import_isoWeek.default);
|
||
|
|
var _DateService = class _DateService {
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
getWeekDates(offset = 0, days = 7) {
|
||
|
|
const monday = this.baseDate.startOf("week").add(1, "day").add(offset, "week");
|
||
|
|
return Array.from({ length: days }, (_, i) => monday.add(i, "day").format("YYYY-MM-DD"));
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get dates for specific weekdays within a week
|
||
|
|
* @param offset - Week offset from base date (0 = current week)
|
||
|
|
* @param workDays - Array of ISO weekday numbers (1=Monday, 7=Sunday)
|
||
|
|
* @returns Array of date strings in YYYY-MM-DD format
|
||
|
|
*/
|
||
|
|
getWorkWeekDates(offset, workDays) {
|
||
|
|
const monday = this.baseDate.startOf("week").add(1, "day").add(offset, "week");
|
||
|
|
return workDays.map((isoDay) => {
|
||
|
|
const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1;
|
||
|
|
return monday.add(daysFromMonday, "day").format("YYYY-MM-DD");
|
||
|
|
});
|
||
|
|
}
|
||
|
|
// ============================================
|
||
|
|
// 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();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_DateService, "DateService");
|
||
|
|
var DateService = _DateService;
|
||
|
|
|
||
|
|
// src/v2/core/BaseGroupingRenderer.ts
|
||
|
|
var _BaseGroupingRenderer = class _BaseGroupingRenderer {
|
||
|
|
/**
|
||
|
|
* 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;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_BaseGroupingRenderer, "BaseGroupingRenderer");
|
||
|
|
var BaseGroupingRenderer = _BaseGroupingRenderer;
|
||
|
|
|
||
|
|
// src/v2/features/resource/ResourceRenderer.ts
|
||
|
|
var _ResourceRenderer = class _ResourceRenderer 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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ResourceRenderer, "ResourceRenderer");
|
||
|
|
var ResourceRenderer = _ResourceRenderer;
|
||
|
|
|
||
|
|
// src/v2/features/team/TeamRenderer.ts
|
||
|
|
var _TeamRenderer = class _TeamRenderer extends BaseGroupingRenderer {
|
||
|
|
constructor(teamService) {
|
||
|
|
super();
|
||
|
|
this.teamService = teamService;
|
||
|
|
this.type = "team";
|
||
|
|
this.config = {
|
||
|
|
elementTag: "swp-team-header",
|
||
|
|
idAttribute: "teamId",
|
||
|
|
colspanVar: "--team-cols"
|
||
|
|
};
|
||
|
|
}
|
||
|
|
getEntities(ids) {
|
||
|
|
return this.teamService.getByIds(ids);
|
||
|
|
}
|
||
|
|
getDisplayName(entity) {
|
||
|
|
return entity.name;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_TeamRenderer, "TeamRenderer");
|
||
|
|
var TeamRenderer = _TeamRenderer;
|
||
|
|
|
||
|
|
// src/v2/features/department/DepartmentRenderer.ts
|
||
|
|
var _DepartmentRenderer = class _DepartmentRenderer extends BaseGroupingRenderer {
|
||
|
|
constructor(departmentService) {
|
||
|
|
super();
|
||
|
|
this.departmentService = departmentService;
|
||
|
|
this.type = "department";
|
||
|
|
this.config = {
|
||
|
|
elementTag: "swp-department-header",
|
||
|
|
idAttribute: "departmentId",
|
||
|
|
colspanVar: "--department-cols"
|
||
|
|
};
|
||
|
|
}
|
||
|
|
getEntities(ids) {
|
||
|
|
return this.departmentService.getByIds(ids);
|
||
|
|
}
|
||
|
|
getDisplayName(entity) {
|
||
|
|
return entity.name;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_DepartmentRenderer, "DepartmentRenderer");
|
||
|
|
var DepartmentRenderer = _DepartmentRenderer;
|
||
|
|
|
||
|
|
// src/v2/core/RenderBuilder.ts
|
||
|
|
function buildPipeline(renderers) {
|
||
|
|
return {
|
||
|
|
async run(context) {
|
||
|
|
for (const renderer of renderers) {
|
||
|
|
await renderer.render(context);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
__name(buildPipeline, "buildPipeline");
|
||
|
|
|
||
|
|
// src/v2/core/FilterTemplate.ts
|
||
|
|
var _FilterTemplate = class _FilterTemplate {
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_FilterTemplate, "FilterTemplate");
|
||
|
|
var FilterTemplate = _FilterTemplate;
|
||
|
|
|
||
|
|
// src/v2/core/CalendarOrchestrator.ts
|
||
|
|
var _CalendarOrchestrator = class _CalendarOrchestrator {
|
||
|
|
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, container2) {
|
||
|
|
const headerContainer = container2.querySelector("swp-calendar-header");
|
||
|
|
const columnContainer = container2.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(container2, filter);
|
||
|
|
await this.eventRenderer.render(container2, filter, filterTemplate);
|
||
|
|
await this.headerDrawerRenderer.render(container2, 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 };
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_CalendarOrchestrator, "CalendarOrchestrator");
|
||
|
|
var CalendarOrchestrator = _CalendarOrchestrator;
|
||
|
|
|
||
|
|
// src/v2/core/NavigationAnimator.ts
|
||
|
|
var _NavigationAnimator = class _NavigationAnimator {
|
||
|
|
constructor(headerTrack, contentTrack) {
|
||
|
|
this.headerTrack = headerTrack;
|
||
|
|
this.contentTrack = contentTrack;
|
||
|
|
}
|
||
|
|
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) {
|
||
|
|
await Promise.all([
|
||
|
|
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
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
async animateIn(translate) {
|
||
|
|
await Promise.all([
|
||
|
|
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
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_NavigationAnimator, "NavigationAnimator");
|
||
|
|
var NavigationAnimator = _NavigationAnimator;
|
||
|
|
|
||
|
|
// src/v2/core/CalendarEvents.ts
|
||
|
|
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"
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/v2/core/CalendarApp.ts
|
||
|
|
var _CalendarApp = class _CalendarApp {
|
||
|
|
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.weekOffset = 0;
|
||
|
|
this.currentViewId = "simple";
|
||
|
|
this.workweekPreset = null;
|
||
|
|
this.groupingOverrides = /* @__PURE__ */ new Map();
|
||
|
|
}
|
||
|
|
async init(container2) {
|
||
|
|
this.container = container2;
|
||
|
|
const gridSettings = await this.settingsService.getGridSettings();
|
||
|
|
if (!gridSettings) {
|
||
|
|
throw new Error("GridSettings not found");
|
||
|
|
}
|
||
|
|
this.workweekPreset = await this.settingsService.getDefaultWorkweekPreset();
|
||
|
|
this.animator = new NavigationAnimator(container2.querySelector("swp-header-track"), container2.querySelector("swp-content-track"));
|
||
|
|
this.timeAxisRenderer.render(container2.querySelector("#time-axis"), gridSettings.dayStartHour, gridSettings.dayEndHour);
|
||
|
|
this.scrollManager.init(container2);
|
||
|
|
this.headerDrawerManager.init(container2);
|
||
|
|
this.dragDropManager.init(container2);
|
||
|
|
this.resizeManager.init(container2);
|
||
|
|
const scrollableContent = container2.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() {
|
||
|
|
this.weekOffset--;
|
||
|
|
await this.animator.slide("right", () => this.render());
|
||
|
|
this.emitStatus("rendered", { viewId: this.currentViewId });
|
||
|
|
}
|
||
|
|
async handleNavigateNext() {
|
||
|
|
this.weekOffset++;
|
||
|
|
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 dates = this.currentViewId === "day" ? this.dateService.getWeekDates(this.weekOffset, 1) : this.dateService.getWorkWeekDates(this.weekOffset, 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
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_CalendarApp, "CalendarApp");
|
||
|
|
var CalendarApp = _CalendarApp;
|
||
|
|
|
||
|
|
// src/v2/features/timeaxis/TimeAxisRenderer.ts
|
||
|
|
var _TimeAxisRenderer = class _TimeAxisRenderer {
|
||
|
|
render(container2, startHour = 6, endHour = 20) {
|
||
|
|
container2.innerHTML = "";
|
||
|
|
for (let hour = startHour; hour <= endHour; hour++) {
|
||
|
|
const marker = document.createElement("swp-hour-marker");
|
||
|
|
marker.textContent = `${hour.toString().padStart(2, "0")}:00`;
|
||
|
|
container2.appendChild(marker);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_TimeAxisRenderer, "TimeAxisRenderer");
|
||
|
|
var TimeAxisRenderer = _TimeAxisRenderer;
|
||
|
|
|
||
|
|
// src/v2/core/ScrollManager.ts
|
||
|
|
var _ScrollManager = class _ScrollManager {
|
||
|
|
init(container2) {
|
||
|
|
this.scrollableContent = container2.querySelector("swp-scrollable-content");
|
||
|
|
this.timeAxisContent = container2.querySelector("swp-time-axis-content");
|
||
|
|
this.calendarHeader = container2.querySelector("swp-calendar-header");
|
||
|
|
this.headerDrawer = container2.querySelector("swp-header-drawer");
|
||
|
|
this.headerViewport = container2.querySelector("swp-header-viewport");
|
||
|
|
this.headerSpacer = container2.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)`;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ScrollManager, "ScrollManager");
|
||
|
|
var ScrollManager = _ScrollManager;
|
||
|
|
|
||
|
|
// src/v2/core/HeaderDrawerManager.ts
|
||
|
|
var _HeaderDrawerManager = class _HeaderDrawerManager {
|
||
|
|
constructor() {
|
||
|
|
this.expanded = false;
|
||
|
|
this.currentRows = 0;
|
||
|
|
this.rowHeight = 25;
|
||
|
|
this.duration = 200;
|
||
|
|
}
|
||
|
|
init(container2) {
|
||
|
|
this.drawer = container2.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;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_HeaderDrawerManager, "HeaderDrawerManager");
|
||
|
|
var HeaderDrawerManager = _HeaderDrawerManager;
|
||
|
|
|
||
|
|
// src/v2/demo/MockStores.ts
|
||
|
|
var _MockTeamStore = class _MockTeamStore {
|
||
|
|
constructor() {
|
||
|
|
this.type = "team";
|
||
|
|
this.teams = [
|
||
|
|
{ id: "alpha", name: "Team Alpha" },
|
||
|
|
{ id: "beta", name: "Team Beta" }
|
||
|
|
];
|
||
|
|
}
|
||
|
|
getByIds(ids) {
|
||
|
|
return this.teams.filter((t) => ids.includes(t.id));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_MockTeamStore, "MockTeamStore");
|
||
|
|
var MockTeamStore = _MockTeamStore;
|
||
|
|
var _MockResourceStore = class _MockResourceStore {
|
||
|
|
constructor() {
|
||
|
|
this.type = "resource";
|
||
|
|
this.resources = [
|
||
|
|
{ id: "alice", name: "Alice", teamId: "alpha" },
|
||
|
|
{ id: "bob", name: "Bob", teamId: "alpha" },
|
||
|
|
{ id: "carol", name: "Carol", teamId: "beta" },
|
||
|
|
{ id: "dave", name: "Dave", teamId: "beta" }
|
||
|
|
];
|
||
|
|
}
|
||
|
|
getByIds(ids) {
|
||
|
|
return this.resources.filter((r) => ids.includes(r.id));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_MockResourceStore, "MockResourceStore");
|
||
|
|
var MockResourceStore = _MockResourceStore;
|
||
|
|
|
||
|
|
// src/v2/demo/DemoApp.ts
|
||
|
|
var _DemoApp = class _DemoApp {
|
||
|
|
constructor(indexedDBContext, dataSeeder, auditService, calendarApp, dateService, resourceService, eventBus) {
|
||
|
|
this.indexedDBContext = indexedDBContext;
|
||
|
|
this.dataSeeder = dataSeeder;
|
||
|
|
this.auditService = auditService;
|
||
|
|
this.calendarApp = calendarApp;
|
||
|
|
this.dateService = dateService;
|
||
|
|
this.resourceService = resourceService;
|
||
|
|
this.eventBus = eventBus;
|
||
|
|
this.currentView = "simple";
|
||
|
|
}
|
||
|
|
async init() {
|
||
|
|
this.dateService.setBaseDate(/* @__PURE__ */ new Date("2025-12-08"));
|
||
|
|
await this.indexedDBContext.initialize();
|
||
|
|
console.log("[DemoApp] IndexedDB initialized");
|
||
|
|
await this.dataSeeder.seedIfEmpty();
|
||
|
|
console.log("[DemoApp] Data seeding complete");
|
||
|
|
this.container = document.querySelector("swp-calendar-container");
|
||
|
|
await this.calendarApp.init(this.container);
|
||
|
|
console.log("[DemoApp] CalendarApp initialized");
|
||
|
|
this.setupNavigation();
|
||
|
|
this.setupDrawerToggle();
|
||
|
|
this.setupViewSwitching();
|
||
|
|
this.setupWorkweekSelector();
|
||
|
|
await this.setupResourceSelector();
|
||
|
|
this.setupStatusListeners();
|
||
|
|
this.eventBus.emit(CalendarEvents.CMD_RENDER, { viewId: this.currentView });
|
||
|
|
}
|
||
|
|
setupNavigation() {
|
||
|
|
document.getElementById("btn-prev").onclick = () => {
|
||
|
|
this.eventBus.emit(CalendarEvents.CMD_NAVIGATE_PREV);
|
||
|
|
};
|
||
|
|
document.getElementById("btn-next").onclick = () => {
|
||
|
|
this.eventBus.emit(CalendarEvents.CMD_NAVIGATE_NEXT);
|
||
|
|
};
|
||
|
|
}
|
||
|
|
setupViewSwitching() {
|
||
|
|
const chips = document.querySelectorAll(".view-chip");
|
||
|
|
chips.forEach((chip) => {
|
||
|
|
chip.addEventListener("click", () => {
|
||
|
|
chips.forEach((c) => c.classList.remove("active"));
|
||
|
|
chip.classList.add("active");
|
||
|
|
const view = chip.dataset.view;
|
||
|
|
if (view) {
|
||
|
|
this.currentView = view;
|
||
|
|
this.updateSelectorVisibility();
|
||
|
|
this.eventBus.emit(CalendarEvents.CMD_RENDER, { viewId: view });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
updateSelectorVisibility() {
|
||
|
|
const selector = document.querySelector("swp-resource-selector");
|
||
|
|
const showSelector = this.currentView === "picker" || this.currentView === "day";
|
||
|
|
selector?.classList.toggle("hidden", !showSelector);
|
||
|
|
}
|
||
|
|
setupDrawerToggle() {
|
||
|
|
document.getElementById("btn-drawer").onclick = () => {
|
||
|
|
this.eventBus.emit(CalendarEvents.CMD_DRAWER_TOGGLE);
|
||
|
|
};
|
||
|
|
}
|
||
|
|
setupWorkweekSelector() {
|
||
|
|
const workweekSelect = document.getElementById("workweek-select");
|
||
|
|
workweekSelect?.addEventListener("change", () => {
|
||
|
|
const presetId = workweekSelect.value;
|
||
|
|
this.eventBus.emit(CalendarEvents.CMD_WORKWEEK_CHANGE, { presetId });
|
||
|
|
});
|
||
|
|
}
|
||
|
|
async setupResourceSelector() {
|
||
|
|
const resources = await this.resourceService.getAll();
|
||
|
|
const container2 = document.querySelector(".resource-checkboxes");
|
||
|
|
if (!container2)
|
||
|
|
return;
|
||
|
|
container2.innerHTML = "";
|
||
|
|
resources.forEach((r) => {
|
||
|
|
const label = document.createElement("label");
|
||
|
|
label.innerHTML = `
|
||
|
|
<input type="checkbox" value="${r.id}" checked>
|
||
|
|
${r.displayName}
|
||
|
|
`;
|
||
|
|
container2.appendChild(label);
|
||
|
|
});
|
||
|
|
container2.addEventListener("change", () => {
|
||
|
|
const checked = container2.querySelectorAll("input:checked");
|
||
|
|
const values = Array.from(checked).map((cb) => cb.value);
|
||
|
|
this.eventBus.emit(CalendarEvents.CMD_VIEW_UPDATE, { type: "resource", values });
|
||
|
|
});
|
||
|
|
}
|
||
|
|
setupStatusListeners() {
|
||
|
|
this.container.addEventListener("calendar:status:ready", () => {
|
||
|
|
console.log("[DemoApp] Calendar ready");
|
||
|
|
});
|
||
|
|
this.container.addEventListener("calendar:status:rendered", (e) => {
|
||
|
|
console.log("[DemoApp] Calendar rendered:", e.detail.viewId);
|
||
|
|
});
|
||
|
|
this.container.addEventListener("calendar:status:error", (e) => {
|
||
|
|
console.error("[DemoApp] Calendar error:", e.detail.message);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_DemoApp, "DemoApp");
|
||
|
|
var DemoApp = _DemoApp;
|
||
|
|
|
||
|
|
// src/v2/core/EventBus.ts
|
||
|
|
var _EventBus = class _EventBus {
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_EventBus, "EventBus");
|
||
|
|
var EventBus = _EventBus;
|
||
|
|
|
||
|
|
// src/v2/storage/IndexedDBContext.ts
|
||
|
|
var _IndexedDBContext = class _IndexedDBContext {
|
||
|
|
constructor(stores) {
|
||
|
|
this.db = null;
|
||
|
|
this.initialized = false;
|
||
|
|
this.stores = stores;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Initialize and open the database
|
||
|
|
*/
|
||
|
|
async initialize() {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const request = indexedDB.open(_IndexedDBContext.DB_NAME, _IndexedDBContext.DB_VERSION);
|
||
|
|
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() {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const request = indexedDB.deleteDatabase(_IndexedDBContext.DB_NAME);
|
||
|
|
request.onsuccess = () => resolve();
|
||
|
|
request.onerror = () => reject(new Error(`Failed to delete database: ${request.error}`));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_IndexedDBContext, "IndexedDBContext");
|
||
|
|
var IndexedDBContext = _IndexedDBContext;
|
||
|
|
IndexedDBContext.DB_NAME = "CalendarV2DB";
|
||
|
|
IndexedDBContext.DB_VERSION = 4;
|
||
|
|
|
||
|
|
// src/v2/storage/events/EventStore.ts
|
||
|
|
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 });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_EventStore, "EventStore");
|
||
|
|
var EventStore = _EventStore;
|
||
|
|
EventStore.STORE_NAME = "events";
|
||
|
|
|
||
|
|
// src/v2/storage/events/EventSerialization.ts
|
||
|
|
var _EventSerialization = class _EventSerialization {
|
||
|
|
/**
|
||
|
|
* 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
|
||
|
|
};
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_EventSerialization, "EventSerialization");
|
||
|
|
var EventSerialization = _EventSerialization;
|
||
|
|
|
||
|
|
// src/v2/storage/SyncPlugin.ts
|
||
|
|
var _SyncPlugin = class _SyncPlugin {
|
||
|
|
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}`));
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_SyncPlugin, "SyncPlugin");
|
||
|
|
var SyncPlugin = _SyncPlugin;
|
||
|
|
|
||
|
|
// src/v2/constants/CoreEvents.ts
|
||
|
|
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"
|
||
|
|
};
|
||
|
|
|
||
|
|
// node_modules/json-diff-ts/dist/index.js
|
||
|
|
function arrayDifference(first, second) {
|
||
|
|
const secondSet = new Set(second);
|
||
|
|
return first.filter((item) => !secondSet.has(item));
|
||
|
|
}
|
||
|
|
__name(arrayDifference, "arrayDifference");
|
||
|
|
function arrayIntersection(first, second) {
|
||
|
|
const secondSet = new Set(second);
|
||
|
|
return first.filter((item) => secondSet.has(item));
|
||
|
|
}
|
||
|
|
__name(arrayIntersection, "arrayIntersection");
|
||
|
|
function keyBy(arr, getKey2) {
|
||
|
|
const result = {};
|
||
|
|
for (const item of arr) {
|
||
|
|
result[String(getKey2(item))] = item;
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
__name(keyBy, "keyBy");
|
||
|
|
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
|
||
|
|
});
|
||
|
|
}
|
||
|
|
__name(diff, "diff");
|
||
|
|
var getTypeOfObj = /* @__PURE__ */ __name((obj) => {
|
||
|
|
if (typeof obj === "undefined") {
|
||
|
|
return "undefined";
|
||
|
|
}
|
||
|
|
if (obj === null) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return Object.prototype.toString.call(obj).match(/^\[object\s(.*)\]$/)[1];
|
||
|
|
}, "getTypeOfObj");
|
||
|
|
var getKey = /* @__PURE__ */ __name((path) => {
|
||
|
|
const left = path[path.length - 1];
|
||
|
|
return left != null ? left : "$root";
|
||
|
|
}, "getKey");
|
||
|
|
var compare = /* @__PURE__ */ __name((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;
|
||
|
|
}, "compare");
|
||
|
|
var compareObject = /* @__PURE__ */ __name((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;
|
||
|
|
}, "compareObject");
|
||
|
|
var compareArray = /* @__PURE__ */ __name((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 [];
|
||
|
|
}
|
||
|
|
}, "compareArray");
|
||
|
|
var getObjectKey = /* @__PURE__ */ __name((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;
|
||
|
|
}, "getObjectKey");
|
||
|
|
var convertArrayToObj = /* @__PURE__ */ __name((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;
|
||
|
|
}, "convertArrayToObj");
|
||
|
|
var comparePrimitives = /* @__PURE__ */ __name((oldObj, newObj, path) => {
|
||
|
|
const changes = [];
|
||
|
|
if (oldObj !== newObj) {
|
||
|
|
changes.push({
|
||
|
|
type: "UPDATE",
|
||
|
|
key: getKey(path),
|
||
|
|
value: newObj,
|
||
|
|
oldValue: oldObj
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return changes;
|
||
|
|
}, "comparePrimitives");
|
||
|
|
|
||
|
|
// src/v2/storage/BaseEntityService.ts
|
||
|
|
var _BaseEntityService = class _BaseEntityService {
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_BaseEntityService, "BaseEntityService");
|
||
|
|
var BaseEntityService = _BaseEntityService;
|
||
|
|
|
||
|
|
// src/v2/storage/events/EventService.ts
|
||
|
|
var _EventService = class _EventService 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);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_EventService, "EventService");
|
||
|
|
var EventService = _EventService;
|
||
|
|
|
||
|
|
// src/v2/storage/resources/ResourceStore.ts
|
||
|
|
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 });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ResourceStore, "ResourceStore");
|
||
|
|
var ResourceStore = _ResourceStore;
|
||
|
|
ResourceStore.STORE_NAME = "resources";
|
||
|
|
|
||
|
|
// src/v2/storage/resources/ResourceService.ts
|
||
|
|
var _ResourceService = class _ResourceService 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}`));
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ResourceService, "ResourceService");
|
||
|
|
var ResourceService = _ResourceService;
|
||
|
|
|
||
|
|
// src/v2/storage/bookings/BookingStore.ts
|
||
|
|
var _BookingStore = class _BookingStore {
|
||
|
|
constructor() {
|
||
|
|
this.storeName = _BookingStore.STORE_NAME;
|
||
|
|
}
|
||
|
|
create(db) {
|
||
|
|
const store = db.createObjectStore(_BookingStore.STORE_NAME, { keyPath: "id" });
|
||
|
|
store.createIndex("customerId", "customerId", { unique: false });
|
||
|
|
store.createIndex("status", "status", { unique: false });
|
||
|
|
store.createIndex("syncStatus", "syncStatus", { unique: false });
|
||
|
|
store.createIndex("createdAt", "createdAt", { unique: false });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_BookingStore, "BookingStore");
|
||
|
|
var BookingStore = _BookingStore;
|
||
|
|
BookingStore.STORE_NAME = "bookings";
|
||
|
|
|
||
|
|
// src/v2/storage/bookings/BookingService.ts
|
||
|
|
var _BookingService = class _BookingService extends BaseEntityService {
|
||
|
|
constructor(context, eventBus) {
|
||
|
|
super(context, eventBus);
|
||
|
|
this.storeName = BookingStore.STORE_NAME;
|
||
|
|
this.entityType = "Booking";
|
||
|
|
}
|
||
|
|
serialize(booking) {
|
||
|
|
return {
|
||
|
|
...booking,
|
||
|
|
createdAt: booking.createdAt.toISOString()
|
||
|
|
};
|
||
|
|
}
|
||
|
|
deserialize(data) {
|
||
|
|
const raw = data;
|
||
|
|
return {
|
||
|
|
...raw,
|
||
|
|
createdAt: new Date(raw.createdAt)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get bookings for a customer
|
||
|
|
*/
|
||
|
|
async getByCustomer(customerId) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const transaction = this.db.transaction([this.storeName], "readonly");
|
||
|
|
const store = transaction.objectStore(this.storeName);
|
||
|
|
const index = store.index("customerId");
|
||
|
|
const request = index.getAll(customerId);
|
||
|
|
request.onsuccess = () => {
|
||
|
|
const data = request.result;
|
||
|
|
const bookings = data.map((item) => this.deserialize(item));
|
||
|
|
resolve(bookings);
|
||
|
|
};
|
||
|
|
request.onerror = () => {
|
||
|
|
reject(new Error(`Failed to get bookings for customer ${customerId}: ${request.error}`));
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get bookings by status
|
||
|
|
*/
|
||
|
|
async getByStatus(status) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const transaction = this.db.transaction([this.storeName], "readonly");
|
||
|
|
const store = transaction.objectStore(this.storeName);
|
||
|
|
const index = store.index("status");
|
||
|
|
const request = index.getAll(status);
|
||
|
|
request.onsuccess = () => {
|
||
|
|
const data = request.result;
|
||
|
|
const bookings = data.map((item) => this.deserialize(item));
|
||
|
|
resolve(bookings);
|
||
|
|
};
|
||
|
|
request.onerror = () => {
|
||
|
|
reject(new Error(`Failed to get bookings with status ${status}: ${request.error}`));
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_BookingService, "BookingService");
|
||
|
|
var BookingService = _BookingService;
|
||
|
|
|
||
|
|
// src/v2/storage/customers/CustomerStore.ts
|
||
|
|
var _CustomerStore = class _CustomerStore {
|
||
|
|
constructor() {
|
||
|
|
this.storeName = _CustomerStore.STORE_NAME;
|
||
|
|
}
|
||
|
|
create(db) {
|
||
|
|
const store = db.createObjectStore(_CustomerStore.STORE_NAME, { keyPath: "id" });
|
||
|
|
store.createIndex("name", "name", { unique: false });
|
||
|
|
store.createIndex("phone", "phone", { unique: false });
|
||
|
|
store.createIndex("syncStatus", "syncStatus", { unique: false });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_CustomerStore, "CustomerStore");
|
||
|
|
var CustomerStore = _CustomerStore;
|
||
|
|
CustomerStore.STORE_NAME = "customers";
|
||
|
|
|
||
|
|
// src/v2/storage/customers/CustomerService.ts
|
||
|
|
var _CustomerService = class _CustomerService extends BaseEntityService {
|
||
|
|
constructor(context, eventBus) {
|
||
|
|
super(context, eventBus);
|
||
|
|
this.storeName = CustomerStore.STORE_NAME;
|
||
|
|
this.entityType = "Customer";
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Search customers by name (case-insensitive contains)
|
||
|
|
*/
|
||
|
|
async searchByName(query) {
|
||
|
|
const all = await this.getAll();
|
||
|
|
const lowerQuery = query.toLowerCase();
|
||
|
|
return all.filter((c) => c.name.toLowerCase().includes(lowerQuery));
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Find customer by phone
|
||
|
|
*/
|
||
|
|
async getByPhone(phone) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const transaction = this.db.transaction([this.storeName], "readonly");
|
||
|
|
const store = transaction.objectStore(this.storeName);
|
||
|
|
const index = store.index("phone");
|
||
|
|
const request = index.get(phone);
|
||
|
|
request.onsuccess = () => {
|
||
|
|
const data = request.result;
|
||
|
|
resolve(data ? data : null);
|
||
|
|
};
|
||
|
|
request.onerror = () => {
|
||
|
|
reject(new Error(`Failed to find customer by phone ${phone}: ${request.error}`));
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_CustomerService, "CustomerService");
|
||
|
|
var CustomerService = _CustomerService;
|
||
|
|
|
||
|
|
// src/v2/storage/teams/TeamStore.ts
|
||
|
|
var _TeamStore = class _TeamStore {
|
||
|
|
constructor() {
|
||
|
|
this.storeName = _TeamStore.STORE_NAME;
|
||
|
|
}
|
||
|
|
create(db) {
|
||
|
|
db.createObjectStore(_TeamStore.STORE_NAME, { keyPath: "id" });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_TeamStore, "TeamStore");
|
||
|
|
var TeamStore = _TeamStore;
|
||
|
|
TeamStore.STORE_NAME = "teams";
|
||
|
|
|
||
|
|
// src/v2/storage/teams/TeamService.ts
|
||
|
|
var _TeamService = class _TeamService extends BaseEntityService {
|
||
|
|
constructor(context, eventBus) {
|
||
|
|
super(context, eventBus);
|
||
|
|
this.storeName = TeamStore.STORE_NAME;
|
||
|
|
this.entityType = "Team";
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get teams by IDs
|
||
|
|
*/
|
||
|
|
async getByIds(ids) {
|
||
|
|
if (ids.length === 0)
|
||
|
|
return [];
|
||
|
|
const results = await Promise.all(ids.map((id) => this.get(id)));
|
||
|
|
return results.filter((t) => t !== null);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Build reverse lookup: resourceId → teamId
|
||
|
|
*/
|
||
|
|
async buildResourceToTeamMap() {
|
||
|
|
const teams = await this.getAll();
|
||
|
|
const map = {};
|
||
|
|
for (const team of teams) {
|
||
|
|
for (const resourceId of team.resourceIds) {
|
||
|
|
map[resourceId] = team.id;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return map;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_TeamService, "TeamService");
|
||
|
|
var TeamService = _TeamService;
|
||
|
|
|
||
|
|
// src/v2/storage/departments/DepartmentStore.ts
|
||
|
|
var _DepartmentStore = class _DepartmentStore {
|
||
|
|
constructor() {
|
||
|
|
this.storeName = _DepartmentStore.STORE_NAME;
|
||
|
|
}
|
||
|
|
create(db) {
|
||
|
|
db.createObjectStore(_DepartmentStore.STORE_NAME, { keyPath: "id" });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_DepartmentStore, "DepartmentStore");
|
||
|
|
var DepartmentStore = _DepartmentStore;
|
||
|
|
DepartmentStore.STORE_NAME = "departments";
|
||
|
|
|
||
|
|
// src/v2/storage/departments/DepartmentService.ts
|
||
|
|
var _DepartmentService = class _DepartmentService extends BaseEntityService {
|
||
|
|
constructor(context, eventBus) {
|
||
|
|
super(context, eventBus);
|
||
|
|
this.storeName = DepartmentStore.STORE_NAME;
|
||
|
|
this.entityType = "Department";
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get departments by IDs
|
||
|
|
*/
|
||
|
|
async getByIds(ids) {
|
||
|
|
if (ids.length === 0)
|
||
|
|
return [];
|
||
|
|
const results = await Promise.all(ids.map((id) => this.get(id)));
|
||
|
|
return results.filter((d) => d !== null);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_DepartmentService, "DepartmentService");
|
||
|
|
var DepartmentService = _DepartmentService;
|
||
|
|
|
||
|
|
// src/v2/storage/settings/SettingsStore.ts
|
||
|
|
var _SettingsStore = class _SettingsStore {
|
||
|
|
constructor() {
|
||
|
|
this.storeName = _SettingsStore.STORE_NAME;
|
||
|
|
}
|
||
|
|
create(db) {
|
||
|
|
db.createObjectStore(_SettingsStore.STORE_NAME, { keyPath: "id" });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_SettingsStore, "SettingsStore");
|
||
|
|
var SettingsStore = _SettingsStore;
|
||
|
|
SettingsStore.STORE_NAME = "settings";
|
||
|
|
|
||
|
|
// src/v2/types/SettingsTypes.ts
|
||
|
|
var SettingsIds = {
|
||
|
|
WORKWEEK: "workweek",
|
||
|
|
GRID: "grid",
|
||
|
|
TIME_FORMAT: "timeFormat",
|
||
|
|
VIEWS: "views"
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/v2/storage/settings/SettingsService.ts
|
||
|
|
var _SettingsService = class _SettingsService 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);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_SettingsService, "SettingsService");
|
||
|
|
var SettingsService = _SettingsService;
|
||
|
|
|
||
|
|
// src/v2/storage/viewconfigs/ViewConfigStore.ts
|
||
|
|
var _ViewConfigStore = class _ViewConfigStore {
|
||
|
|
constructor() {
|
||
|
|
this.storeName = _ViewConfigStore.STORE_NAME;
|
||
|
|
}
|
||
|
|
create(db) {
|
||
|
|
db.createObjectStore(_ViewConfigStore.STORE_NAME, { keyPath: "id" });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ViewConfigStore, "ViewConfigStore");
|
||
|
|
var ViewConfigStore = _ViewConfigStore;
|
||
|
|
ViewConfigStore.STORE_NAME = "viewconfigs";
|
||
|
|
|
||
|
|
// src/v2/storage/viewconfigs/ViewConfigService.ts
|
||
|
|
var _ViewConfigService = class _ViewConfigService extends BaseEntityService {
|
||
|
|
constructor(context, eventBus) {
|
||
|
|
super(context, eventBus);
|
||
|
|
this.storeName = ViewConfigStore.STORE_NAME;
|
||
|
|
this.entityType = "ViewConfig";
|
||
|
|
}
|
||
|
|
async getById(id) {
|
||
|
|
return this.get(id);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ViewConfigService, "ViewConfigService");
|
||
|
|
var ViewConfigService = _ViewConfigService;
|
||
|
|
|
||
|
|
// src/v2/storage/audit/AuditStore.ts
|
||
|
|
var _AuditStore = class _AuditStore {
|
||
|
|
constructor() {
|
||
|
|
this.storeName = "audit";
|
||
|
|
}
|
||
|
|
create(db) {
|
||
|
|
const store = db.createObjectStore(this.storeName, { keyPath: "id" });
|
||
|
|
store.createIndex("syncStatus", "syncStatus", { unique: false });
|
||
|
|
store.createIndex("synced", "synced", { unique: false });
|
||
|
|
store.createIndex("entityId", "entityId", { unique: false });
|
||
|
|
store.createIndex("timestamp", "timestamp", { unique: false });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_AuditStore, "AuditStore");
|
||
|
|
var AuditStore = _AuditStore;
|
||
|
|
|
||
|
|
// src/v2/storage/audit/AuditService.ts
|
||
|
|
var _AuditService = class _AuditService extends BaseEntityService {
|
||
|
|
constructor(context, eventBus) {
|
||
|
|
super(context, eventBus);
|
||
|
|
this.storeName = "audit";
|
||
|
|
this.entityType = "Audit";
|
||
|
|
this.setupEventListeners();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Setup listeners for ENTITY_SAVED and ENTITY_DELETED events
|
||
|
|
*/
|
||
|
|
setupEventListeners() {
|
||
|
|
this.eventBus.on(CoreEvents.ENTITY_SAVED, (event) => {
|
||
|
|
const detail = event.detail;
|
||
|
|
this.handleEntitySaved(detail);
|
||
|
|
});
|
||
|
|
this.eventBus.on(CoreEvents.ENTITY_DELETED, (event) => {
|
||
|
|
const detail = event.detail;
|
||
|
|
this.handleEntityDeleted(detail);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Handle ENTITY_SAVED event - create audit entry
|
||
|
|
*/
|
||
|
|
async handleEntitySaved(payload) {
|
||
|
|
if (payload.entityType === "Audit")
|
||
|
|
return;
|
||
|
|
const auditEntry = {
|
||
|
|
id: crypto.randomUUID(),
|
||
|
|
entityType: payload.entityType,
|
||
|
|
entityId: payload.entityId,
|
||
|
|
operation: payload.operation,
|
||
|
|
userId: _AuditService.DEFAULT_USER_ID,
|
||
|
|
timestamp: payload.timestamp,
|
||
|
|
changes: payload.changes,
|
||
|
|
synced: false,
|
||
|
|
syncStatus: "pending"
|
||
|
|
};
|
||
|
|
await this.save(auditEntry);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Handle ENTITY_DELETED event - create audit entry
|
||
|
|
*/
|
||
|
|
async handleEntityDeleted(payload) {
|
||
|
|
if (payload.entityType === "Audit")
|
||
|
|
return;
|
||
|
|
const auditEntry = {
|
||
|
|
id: crypto.randomUUID(),
|
||
|
|
entityType: payload.entityType,
|
||
|
|
entityId: payload.entityId,
|
||
|
|
operation: "delete",
|
||
|
|
userId: _AuditService.DEFAULT_USER_ID,
|
||
|
|
timestamp: payload.timestamp,
|
||
|
|
changes: { id: payload.entityId },
|
||
|
|
// For delete, just store the ID
|
||
|
|
synced: false,
|
||
|
|
syncStatus: "pending"
|
||
|
|
};
|
||
|
|
await this.save(auditEntry);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Override save to NOT trigger ENTITY_SAVED event
|
||
|
|
* Instead, emits AUDIT_LOGGED for SyncManager to listen
|
||
|
|
*
|
||
|
|
* This prevents infinite loops:
|
||
|
|
* - BaseEntityService.save() emits ENTITY_SAVED
|
||
|
|
* - AuditService listens to ENTITY_SAVED and creates audit
|
||
|
|
* - If AuditService.save() also emitted ENTITY_SAVED, it would loop
|
||
|
|
*/
|
||
|
|
async save(entity) {
|
||
|
|
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 = () => {
|
||
|
|
const payload = {
|
||
|
|
auditId: entity.id,
|
||
|
|
entityType: entity.entityType,
|
||
|
|
entityId: entity.entityId,
|
||
|
|
operation: entity.operation,
|
||
|
|
timestamp: entity.timestamp
|
||
|
|
};
|
||
|
|
this.eventBus.emit(CoreEvents.AUDIT_LOGGED, payload);
|
||
|
|
resolve();
|
||
|
|
};
|
||
|
|
request.onerror = () => {
|
||
|
|
reject(new Error(`Failed to save audit entry ${entity.id}: ${request.error}`));
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Override delete to NOT trigger ENTITY_DELETED event
|
||
|
|
* Audit entries should never be deleted (compliance requirement)
|
||
|
|
*/
|
||
|
|
async delete(_id) {
|
||
|
|
throw new Error("Audit entries cannot be deleted (compliance requirement)");
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get pending audit entries (for sync)
|
||
|
|
*/
|
||
|
|
async getPendingAudits() {
|
||
|
|
return this.getBySyncStatus("pending");
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get audit entries for a specific entity
|
||
|
|
*/
|
||
|
|
async getByEntityId(entityId) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const transaction = this.db.transaction([this.storeName], "readonly");
|
||
|
|
const store = transaction.objectStore(this.storeName);
|
||
|
|
const index = store.index("entityId");
|
||
|
|
const request = index.getAll(entityId);
|
||
|
|
request.onsuccess = () => {
|
||
|
|
const entries = request.result;
|
||
|
|
resolve(entries);
|
||
|
|
};
|
||
|
|
request.onerror = () => {
|
||
|
|
reject(new Error(`Failed to get audit entries for entity ${entityId}: ${request.error}`));
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_AuditService, "AuditService");
|
||
|
|
var AuditService = _AuditService;
|
||
|
|
AuditService.DEFAULT_USER_ID = "00000000-0000-0000-0000-000000000001";
|
||
|
|
|
||
|
|
// src/v2/repositories/MockEventRepository.ts
|
||
|
|
var _MockEventRepository = class _MockEventRepository {
|
||
|
|
constructor() {
|
||
|
|
this.entityType = "Event";
|
||
|
|
this.dataUrl = "data/mock-events.json";
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Fetch all events from mock JSON file
|
||
|
|
*/
|
||
|
|
async fetchAll() {
|
||
|
|
try {
|
||
|
|
const response = await fetch(this.dataUrl);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`Failed to load mock events: ${response.status} ${response.statusText}`);
|
||
|
|
}
|
||
|
|
const rawData = await response.json();
|
||
|
|
return this.processCalendarData(rawData);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load event data:", error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async sendCreate(_event) {
|
||
|
|
throw new Error("MockEventRepository does not support sendCreate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendUpdate(_id, _updates) {
|
||
|
|
throw new Error("MockEventRepository does not support sendUpdate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendDelete(_id) {
|
||
|
|
throw new Error("MockEventRepository does not support sendDelete. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
processCalendarData(data) {
|
||
|
|
return data.map((event) => {
|
||
|
|
if (event.type === "customer") {
|
||
|
|
if (!event.bookingId)
|
||
|
|
console.warn(`Customer event ${event.id} missing bookingId`);
|
||
|
|
if (!event.resourceId)
|
||
|
|
console.warn(`Customer event ${event.id} missing resourceId`);
|
||
|
|
if (!event.customerId)
|
||
|
|
console.warn(`Customer event ${event.id} missing customerId`);
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
id: event.id,
|
||
|
|
title: event.title,
|
||
|
|
description: event.description,
|
||
|
|
start: new Date(event.start),
|
||
|
|
end: new Date(event.end),
|
||
|
|
type: event.type,
|
||
|
|
allDay: event.allDay || false,
|
||
|
|
bookingId: event.bookingId,
|
||
|
|
resourceId: event.resourceId,
|
||
|
|
customerId: event.customerId,
|
||
|
|
recurringId: event.recurringId,
|
||
|
|
metadata: event.metadata,
|
||
|
|
syncStatus: "synced"
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_MockEventRepository, "MockEventRepository");
|
||
|
|
var MockEventRepository = _MockEventRepository;
|
||
|
|
|
||
|
|
// src/v2/repositories/MockResourceRepository.ts
|
||
|
|
var _MockResourceRepository = class _MockResourceRepository {
|
||
|
|
constructor() {
|
||
|
|
this.entityType = "Resource";
|
||
|
|
this.dataUrl = "data/mock-resources.json";
|
||
|
|
}
|
||
|
|
async fetchAll() {
|
||
|
|
try {
|
||
|
|
const response = await fetch(this.dataUrl);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`Failed to load mock resources: ${response.status} ${response.statusText}`);
|
||
|
|
}
|
||
|
|
const rawData = await response.json();
|
||
|
|
return this.processResourceData(rawData);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load resource data:", error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async sendCreate(_resource) {
|
||
|
|
throw new Error("MockResourceRepository does not support sendCreate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendUpdate(_id, _updates) {
|
||
|
|
throw new Error("MockResourceRepository does not support sendUpdate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendDelete(_id) {
|
||
|
|
throw new Error("MockResourceRepository does not support sendDelete. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
processResourceData(data) {
|
||
|
|
return data.map((resource) => ({
|
||
|
|
id: resource.id,
|
||
|
|
name: resource.name,
|
||
|
|
displayName: resource.displayName,
|
||
|
|
type: resource.type,
|
||
|
|
avatarUrl: resource.avatarUrl,
|
||
|
|
color: resource.color,
|
||
|
|
isActive: resource.isActive,
|
||
|
|
defaultSchedule: resource.defaultSchedule,
|
||
|
|
metadata: resource.metadata,
|
||
|
|
syncStatus: "synced"
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_MockResourceRepository, "MockResourceRepository");
|
||
|
|
var MockResourceRepository = _MockResourceRepository;
|
||
|
|
|
||
|
|
// src/v2/repositories/MockBookingRepository.ts
|
||
|
|
var _MockBookingRepository = class _MockBookingRepository {
|
||
|
|
constructor() {
|
||
|
|
this.entityType = "Booking";
|
||
|
|
this.dataUrl = "data/mock-bookings.json";
|
||
|
|
}
|
||
|
|
async fetchAll() {
|
||
|
|
try {
|
||
|
|
const response = await fetch(this.dataUrl);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`Failed to load mock bookings: ${response.status} ${response.statusText}`);
|
||
|
|
}
|
||
|
|
const rawData = await response.json();
|
||
|
|
return this.processBookingData(rawData);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load booking data:", error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async sendCreate(_booking) {
|
||
|
|
throw new Error("MockBookingRepository does not support sendCreate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendUpdate(_id, _updates) {
|
||
|
|
throw new Error("MockBookingRepository does not support sendUpdate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendDelete(_id) {
|
||
|
|
throw new Error("MockBookingRepository does not support sendDelete. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
processBookingData(data) {
|
||
|
|
return data.map((booking) => ({
|
||
|
|
id: booking.id,
|
||
|
|
customerId: booking.customerId,
|
||
|
|
status: booking.status,
|
||
|
|
createdAt: new Date(booking.createdAt),
|
||
|
|
services: booking.services,
|
||
|
|
totalPrice: booking.totalPrice,
|
||
|
|
tags: booking.tags,
|
||
|
|
notes: booking.notes,
|
||
|
|
syncStatus: "synced"
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_MockBookingRepository, "MockBookingRepository");
|
||
|
|
var MockBookingRepository = _MockBookingRepository;
|
||
|
|
|
||
|
|
// src/v2/repositories/MockCustomerRepository.ts
|
||
|
|
var _MockCustomerRepository = class _MockCustomerRepository {
|
||
|
|
constructor() {
|
||
|
|
this.entityType = "Customer";
|
||
|
|
this.dataUrl = "data/mock-customers.json";
|
||
|
|
}
|
||
|
|
async fetchAll() {
|
||
|
|
try {
|
||
|
|
const response = await fetch(this.dataUrl);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`Failed to load mock customers: ${response.status} ${response.statusText}`);
|
||
|
|
}
|
||
|
|
const rawData = await response.json();
|
||
|
|
return this.processCustomerData(rawData);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load customer data:", error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async sendCreate(_customer) {
|
||
|
|
throw new Error("MockCustomerRepository does not support sendCreate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendUpdate(_id, _updates) {
|
||
|
|
throw new Error("MockCustomerRepository does not support sendUpdate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendDelete(_id) {
|
||
|
|
throw new Error("MockCustomerRepository does not support sendDelete. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
processCustomerData(data) {
|
||
|
|
return data.map((customer) => ({
|
||
|
|
id: customer.id,
|
||
|
|
name: customer.name,
|
||
|
|
phone: customer.phone,
|
||
|
|
email: customer.email,
|
||
|
|
metadata: customer.metadata,
|
||
|
|
syncStatus: "synced"
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_MockCustomerRepository, "MockCustomerRepository");
|
||
|
|
var MockCustomerRepository = _MockCustomerRepository;
|
||
|
|
|
||
|
|
// src/v2/repositories/MockAuditRepository.ts
|
||
|
|
var _MockAuditRepository = class _MockAuditRepository {
|
||
|
|
constructor() {
|
||
|
|
this.entityType = "Audit";
|
||
|
|
}
|
||
|
|
async sendCreate(entity) {
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||
|
|
console.log("MockAuditRepository: Audit entry synced to backend:", {
|
||
|
|
id: entity.id,
|
||
|
|
entityType: entity.entityType,
|
||
|
|
entityId: entity.entityId,
|
||
|
|
operation: entity.operation,
|
||
|
|
timestamp: new Date(entity.timestamp).toISOString()
|
||
|
|
});
|
||
|
|
return entity;
|
||
|
|
}
|
||
|
|
async sendUpdate(_id, _entity) {
|
||
|
|
throw new Error("Audit entries cannot be updated");
|
||
|
|
}
|
||
|
|
async sendDelete(_id) {
|
||
|
|
throw new Error("Audit entries cannot be deleted");
|
||
|
|
}
|
||
|
|
async fetchAll() {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
async fetchById(_id) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_MockAuditRepository, "MockAuditRepository");
|
||
|
|
var MockAuditRepository = _MockAuditRepository;
|
||
|
|
|
||
|
|
// src/v2/repositories/MockTeamRepository.ts
|
||
|
|
var _MockTeamRepository = class _MockTeamRepository {
|
||
|
|
constructor() {
|
||
|
|
this.entityType = "Team";
|
||
|
|
this.dataUrl = "data/mock-teams.json";
|
||
|
|
}
|
||
|
|
async fetchAll() {
|
||
|
|
try {
|
||
|
|
const response = await fetch(this.dataUrl);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`Failed to load mock teams: ${response.status} ${response.statusText}`);
|
||
|
|
}
|
||
|
|
const rawData = await response.json();
|
||
|
|
return this.processTeamData(rawData);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load team data:", error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async sendCreate(_team) {
|
||
|
|
throw new Error("MockTeamRepository does not support sendCreate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendUpdate(_id, _updates) {
|
||
|
|
throw new Error("MockTeamRepository does not support sendUpdate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendDelete(_id) {
|
||
|
|
throw new Error("MockTeamRepository does not support sendDelete. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
processTeamData(data) {
|
||
|
|
return data.map((team) => ({
|
||
|
|
id: team.id,
|
||
|
|
name: team.name,
|
||
|
|
resourceIds: team.resourceIds,
|
||
|
|
syncStatus: "synced"
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_MockTeamRepository, "MockTeamRepository");
|
||
|
|
var MockTeamRepository = _MockTeamRepository;
|
||
|
|
|
||
|
|
// src/v2/repositories/MockDepartmentRepository.ts
|
||
|
|
var _MockDepartmentRepository = class _MockDepartmentRepository {
|
||
|
|
constructor() {
|
||
|
|
this.entityType = "Department";
|
||
|
|
this.dataUrl = "data/mock-departments.json";
|
||
|
|
}
|
||
|
|
async fetchAll() {
|
||
|
|
try {
|
||
|
|
const response = await fetch(this.dataUrl);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`Failed to load mock departments: ${response.status} ${response.statusText}`);
|
||
|
|
}
|
||
|
|
const rawData = await response.json();
|
||
|
|
return this.processDepartmentData(rawData);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load department data:", error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async sendCreate(_department) {
|
||
|
|
throw new Error("MockDepartmentRepository does not support sendCreate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendUpdate(_id, _updates) {
|
||
|
|
throw new Error("MockDepartmentRepository does not support sendUpdate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendDelete(_id) {
|
||
|
|
throw new Error("MockDepartmentRepository does not support sendDelete. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
processDepartmentData(data) {
|
||
|
|
return data.map((dept) => ({
|
||
|
|
id: dept.id,
|
||
|
|
name: dept.name,
|
||
|
|
resourceIds: dept.resourceIds,
|
||
|
|
syncStatus: "synced"
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_MockDepartmentRepository, "MockDepartmentRepository");
|
||
|
|
var MockDepartmentRepository = _MockDepartmentRepository;
|
||
|
|
|
||
|
|
// src/v2/repositories/MockSettingsRepository.ts
|
||
|
|
var _MockSettingsRepository = class _MockSettingsRepository {
|
||
|
|
constructor() {
|
||
|
|
this.entityType = "Settings";
|
||
|
|
this.dataUrl = "data/tenant-settings.json";
|
||
|
|
}
|
||
|
|
async fetchAll() {
|
||
|
|
try {
|
||
|
|
const response = await fetch(this.dataUrl);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`Failed to load tenant settings: ${response.status} ${response.statusText}`);
|
||
|
|
}
|
||
|
|
const settings = await response.json();
|
||
|
|
return settings.map((s) => ({
|
||
|
|
...s,
|
||
|
|
syncStatus: s.syncStatus || "synced"
|
||
|
|
}));
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load tenant settings:", error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async sendCreate(_settings) {
|
||
|
|
throw new Error("MockSettingsRepository does not support sendCreate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendUpdate(_id, _updates) {
|
||
|
|
throw new Error("MockSettingsRepository does not support sendUpdate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendDelete(_id) {
|
||
|
|
throw new Error("MockSettingsRepository does not support sendDelete. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_MockSettingsRepository, "MockSettingsRepository");
|
||
|
|
var MockSettingsRepository = _MockSettingsRepository;
|
||
|
|
|
||
|
|
// src/v2/repositories/MockViewConfigRepository.ts
|
||
|
|
var _MockViewConfigRepository = class _MockViewConfigRepository {
|
||
|
|
constructor() {
|
||
|
|
this.entityType = "ViewConfig";
|
||
|
|
this.dataUrl = "data/viewconfigs.json";
|
||
|
|
}
|
||
|
|
async fetchAll() {
|
||
|
|
try {
|
||
|
|
const response = await fetch(this.dataUrl);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`Failed to load viewconfigs: ${response.status} ${response.statusText}`);
|
||
|
|
}
|
||
|
|
const rawData = await response.json();
|
||
|
|
const configs = rawData.map((config) => ({
|
||
|
|
...config,
|
||
|
|
syncStatus: config.syncStatus || "synced"
|
||
|
|
}));
|
||
|
|
return configs;
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load viewconfigs:", error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async sendCreate(_config) {
|
||
|
|
throw new Error("MockViewConfigRepository does not support sendCreate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendUpdate(_id, _updates) {
|
||
|
|
throw new Error("MockViewConfigRepository does not support sendUpdate. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
async sendDelete(_id) {
|
||
|
|
throw new Error("MockViewConfigRepository does not support sendDelete. Mock data is read-only.");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_MockViewConfigRepository, "MockViewConfigRepository");
|
||
|
|
var MockViewConfigRepository = _MockViewConfigRepository;
|
||
|
|
|
||
|
|
// src/v2/workers/DataSeeder.ts
|
||
|
|
var _DataSeeder = class _DataSeeder {
|
||
|
|
constructor(services, repositories) {
|
||
|
|
this.services = services;
|
||
|
|
this.repositories = repositories;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Seed all entity stores if they are empty
|
||
|
|
*/
|
||
|
|
async seedIfEmpty() {
|
||
|
|
console.log("[DataSeeder] Checking if database needs seeding...");
|
||
|
|
try {
|
||
|
|
for (const service of this.services) {
|
||
|
|
const repository = this.repositories.find((repo) => repo.entityType === service.entityType);
|
||
|
|
if (!repository) {
|
||
|
|
console.warn(`[DataSeeder] No repository found for entity type: ${service.entityType}, skipping`);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
await this.seedEntity(service.entityType, service, repository);
|
||
|
|
}
|
||
|
|
console.log("[DataSeeder] Seeding complete");
|
||
|
|
} catch (error) {
|
||
|
|
console.error("[DataSeeder] Seeding failed:", error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async seedEntity(entityType, service, repository) {
|
||
|
|
const existing = await service.getAll();
|
||
|
|
if (existing.length > 0) {
|
||
|
|
console.log(`[DataSeeder] ${entityType} store already has ${existing.length} items, skipping seed`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
console.log(`[DataSeeder] ${entityType} store is empty, fetching from repository...`);
|
||
|
|
const data = await repository.fetchAll();
|
||
|
|
console.log(`[DataSeeder] Fetched ${data.length} ${entityType} items, saving to IndexedDB...`);
|
||
|
|
for (const entity of data) {
|
||
|
|
await service.save(entity, true);
|
||
|
|
}
|
||
|
|
console.log(`[DataSeeder] ${entityType} seeding complete (${data.length} items saved)`);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_DataSeeder, "DataSeeder");
|
||
|
|
var DataSeeder = _DataSeeder;
|
||
|
|
|
||
|
|
// src/v2/utils/PositionUtils.ts
|
||
|
|
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 };
|
||
|
|
}
|
||
|
|
__name(calculateEventPosition, "calculateEventPosition");
|
||
|
|
function minutesToPixels(minutes, config) {
|
||
|
|
return minutes / 60 * config.hourHeight;
|
||
|
|
}
|
||
|
|
__name(minutesToPixels, "minutesToPixels");
|
||
|
|
function pixelsToMinutes(pixels, config) {
|
||
|
|
return pixels / config.hourHeight * 60;
|
||
|
|
}
|
||
|
|
__name(pixelsToMinutes, "pixelsToMinutes");
|
||
|
|
function snapToGrid(pixels, config) {
|
||
|
|
const snapPixels = minutesToPixels(config.snapInterval, config);
|
||
|
|
return Math.round(pixels / snapPixels) * snapPixels;
|
||
|
|
}
|
||
|
|
__name(snapToGrid, "snapToGrid");
|
||
|
|
|
||
|
|
// src/v2/features/event/EventLayoutEngine.ts
|
||
|
|
function eventsOverlap(a, b) {
|
||
|
|
return a.start < b.end && a.end > b.start;
|
||
|
|
}
|
||
|
|
__name(eventsOverlap, "eventsOverlap");
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
__name(eventsWithinThreshold, "eventsWithinThreshold");
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
__name(findOverlapGroups, "findOverlapGroups");
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
__name(findGridCandidates, "findGridCandidates");
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
__name(calculateStackLevels, "calculateStackLevels");
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
__name(allocateColumns, "allocateColumns");
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
__name(calculateColumnLayout, "calculateColumnLayout");
|
||
|
|
|
||
|
|
// src/v2/features/event/EventRenderer.ts
|
||
|
|
var _EventRenderer = class _EventRenderer {
|
||
|
|
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(container2, filter, filterTemplate) {
|
||
|
|
this.container = container2;
|
||
|
|
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 = container2.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 = `
|
||
|
|
<swp-event-time>${this.dateService.formatTimeRange(event.start, event.end)}</swp-event-time>
|
||
|
|
<swp-event-title>${this.escapeHtml(event.title)}</swp-event-title>
|
||
|
|
${event.description ? `<swp-event-description>${this.escapeHtml(event.description)}</swp-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;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_EventRenderer, "EventRenderer");
|
||
|
|
var EventRenderer = _EventRenderer;
|
||
|
|
|
||
|
|
// src/v2/features/schedule/ScheduleRenderer.ts
|
||
|
|
var _ScheduleRenderer = class _ScheduleRenderer {
|
||
|
|
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(container2, filter) {
|
||
|
|
const dates = filter["date"] || [];
|
||
|
|
const resourceIds = filter["resource"] || [];
|
||
|
|
if (dates.length === 0)
|
||
|
|
return;
|
||
|
|
const dayColumns = container2.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;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ScheduleRenderer, "ScheduleRenderer");
|
||
|
|
var ScheduleRenderer = _ScheduleRenderer;
|
||
|
|
|
||
|
|
// src/v2/features/headerdrawer/HeaderDrawerRenderer.ts
|
||
|
|
var _HeaderDrawerRenderer = class _HeaderDrawerRenderer {
|
||
|
|
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(container2, filter, filterTemplate) {
|
||
|
|
this.filterTemplate = filterTemplate;
|
||
|
|
const drawer = container2.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();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_HeaderDrawerRenderer, "HeaderDrawerRenderer");
|
||
|
|
var HeaderDrawerRenderer = _HeaderDrawerRenderer;
|
||
|
|
|
||
|
|
// src/v2/storage/schedules/ScheduleOverrideStore.ts
|
||
|
|
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 });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ScheduleOverrideStore, "ScheduleOverrideStore");
|
||
|
|
var ScheduleOverrideStore = _ScheduleOverrideStore;
|
||
|
|
ScheduleOverrideStore.STORE_NAME = "scheduleOverrides";
|
||
|
|
|
||
|
|
// src/v2/storage/schedules/ScheduleOverrideService.ts
|
||
|
|
var _ScheduleOverrideService = class _ScheduleOverrideService {
|
||
|
|
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}`));
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ScheduleOverrideService, "ScheduleOverrideService");
|
||
|
|
var ScheduleOverrideService = _ScheduleOverrideService;
|
||
|
|
|
||
|
|
// src/v2/storage/schedules/ResourceScheduleService.ts
|
||
|
|
var _ResourceScheduleService = class _ResourceScheduleService {
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ResourceScheduleService, "ResourceScheduleService");
|
||
|
|
var ResourceScheduleService = _ResourceScheduleService;
|
||
|
|
|
||
|
|
// src/v2/types/SwpEvent.ts
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_SwpEvent, "SwpEvent");
|
||
|
|
var SwpEvent = _SwpEvent;
|
||
|
|
|
||
|
|
// src/v2/managers/DragDropManager.ts
|
||
|
|
var _DragDropManager = class _DragDropManager {
|
||
|
|
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(container2) {
|
||
|
|
this.container = container2;
|
||
|
|
container2.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;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_DragDropManager, "DragDropManager");
|
||
|
|
var DragDropManager = _DragDropManager;
|
||
|
|
|
||
|
|
// src/v2/managers/EdgeScrollManager.ts
|
||
|
|
var _EdgeScrollManager = class _EdgeScrollManager {
|
||
|
|
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, {});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_EdgeScrollManager, "EdgeScrollManager");
|
||
|
|
var EdgeScrollManager = _EdgeScrollManager;
|
||
|
|
|
||
|
|
// src/v2/managers/ResizeManager.ts
|
||
|
|
var _ResizeManager = class _ResizeManager {
|
||
|
|
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 container2 = element.closest("swp-event-group") ?? element;
|
||
|
|
const prevZIndex = container2.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
|
||
|
|
};
|
||
|
|
container2.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 container2 = this.resizeState.element.closest("swp-event-group") ?? this.resizeState.element;
|
||
|
|
container2.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(container2) {
|
||
|
|
this.container = container2;
|
||
|
|
container2.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;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_ResizeManager, "ResizeManager");
|
||
|
|
var ResizeManager = _ResizeManager;
|
||
|
|
|
||
|
|
// src/v2/managers/EventPersistenceManager.ts
|
||
|
|
var _EventPersistenceManager = class _EventPersistenceManager {
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
__name(_EventPersistenceManager, "EventPersistenceManager");
|
||
|
|
var EventPersistenceManager = _EventPersistenceManager;
|
||
|
|
|
||
|
|
// src/v2/V2CompositionRoot.ts
|
||
|
|
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 createV2Container() {
|
||
|
|
const container2 = new Container();
|
||
|
|
const builder = container2.builder();
|
||
|
|
builder.registerInstance(defaultTimeFormatConfig).as("ITimeFormatConfig");
|
||
|
|
builder.registerInstance(defaultGridConfig).as("IGridConfig");
|
||
|
|
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")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(EventStore).as("IStore");
|
||
|
|
builder.registerType(ResourceStore).as("IStore");
|
||
|
|
builder.registerType(BookingStore).as("IStore");
|
||
|
|
builder.registerType(CustomerStore).as("IStore");
|
||
|
|
builder.registerType(TeamStore).as("IStore");
|
||
|
|
builder.registerType(DepartmentStore).as("IStore");
|
||
|
|
builder.registerType(ScheduleOverrideStore).as("IStore");
|
||
|
|
builder.registerType(AuditStore).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(BookingService).as("IEntityService").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("IndexedDBContext"),
|
||
|
|
(c) => c.resolveType("IEventBus")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(BookingService).as("IEntityService").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("IndexedDBContext"),
|
||
|
|
(c) => c.resolveType("IEventBus")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(BookingService).as("BookingService").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("IndexedDBContext"),
|
||
|
|
(c) => c.resolveType("IEventBus")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(CustomerService).as("IEntityService").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("IndexedDBContext"),
|
||
|
|
(c) => c.resolveType("IEventBus")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(CustomerService).as("IEntityService").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("IndexedDBContext"),
|
||
|
|
(c) => c.resolveType("IEventBus")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(CustomerService).as("CustomerService").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("IndexedDBContext"),
|
||
|
|
(c) => c.resolveType("IEventBus")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(TeamService).as("IEntityService").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("IndexedDBContext"),
|
||
|
|
(c) => c.resolveType("IEventBus")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(TeamService).as("IEntityService").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("IndexedDBContext"),
|
||
|
|
(c) => c.resolveType("IEventBus")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(TeamService).as("TeamService").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("IndexedDBContext"),
|
||
|
|
(c) => c.resolveType("IEventBus")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(DepartmentService).as("IEntityService").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("IndexedDBContext"),
|
||
|
|
(c) => c.resolveType("IEventBus")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(DepartmentService).as("IEntityService").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("IndexedDBContext"),
|
||
|
|
(c) => c.resolveType("IEventBus")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(DepartmentService).as("DepartmentService").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(MockEventRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockEventRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockResourceRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockResourceRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockBookingRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockBookingRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockCustomerRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockCustomerRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockAuditRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockAuditRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockTeamRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockTeamRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockDepartmentRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockDepartmentRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockSettingsRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockSettingsRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockViewConfigRepository).as("IApiRepository");
|
||
|
|
builder.registerType(MockViewConfigRepository).as("IApiRepository");
|
||
|
|
builder.registerType(AuditService).as("AuditService").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("IndexedDBContext"),
|
||
|
|
(c) => c.resolveType("IEventBus")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(DataSeeder).as("DataSeeder").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveTypeAll("IEntityService"),
|
||
|
|
(c) => c.resolveTypeAll("IApiRepository")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
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")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
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(DateRenderer).as("IRenderer").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("DateService")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(ResourceRenderer).as("IRenderer").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("ResourceService")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(TeamRenderer).as("IRenderer").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("TeamService")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(DepartmentRenderer).as("IRenderer").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("DepartmentService")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(MockTeamStore).as("IGroupingStore");
|
||
|
|
builder.registerType(MockResourceStore).as("IGroupingStore");
|
||
|
|
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(TimeAxisRenderer).as("TimeAxisRenderer");
|
||
|
|
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(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")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
builder.registerType(DemoApp).as("DemoApp").autoWire({
|
||
|
|
mapResolvers: [
|
||
|
|
(c) => c.resolveType("IndexedDBContext"),
|
||
|
|
(c) => c.resolveType("DataSeeder"),
|
||
|
|
(c) => c.resolveType("AuditService"),
|
||
|
|
(c) => c.resolveType("CalendarApp"),
|
||
|
|
(c) => c.resolveType("DateService"),
|
||
|
|
(c) => c.resolveType("ResourceService"),
|
||
|
|
(c) => c.resolveType("IEventBus")
|
||
|
|
]
|
||
|
|
});
|
||
|
|
return builder.build();
|
||
|
|
}
|
||
|
|
__name(createV2Container, "createV2Container");
|
||
|
|
|
||
|
|
// src/v2/demo/index.ts
|
||
|
|
var container = createV2Container();
|
||
|
|
container.resolveType("DemoApp").init().catch(console.error);
|
||
|
|
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vLi4vbm9kZV9tb2R1bGVzL2RheWpzL2RheWpzLm1pbi5qcyIsICIuLi8uLi9ub2RlX21vZHVsZXMvZGF5anMvcGx1Z2luL3V0Yy5qcyIsICIuLi8uLi9ub2RlX21vZHVsZXMvZGF5anMvcGx1Z2luL3RpbWV6b25lLmpzIiwgIi4uLy4uL25vZGVfbW9kdWxlcy9kYXlqcy9wbHVnaW4vaXNvV2Vlay5qcyIsICIuLi8uLi9ub2RlX21vZHVsZXMvQG5vdmFkaS9jb3JlL2Rpc3QvdG9rZW4uanMiLCAiLi4vLi4vbm9kZV9tb2R1bGVzL0Bub3ZhZGkvY29yZS9kaXN0L2Vycm9ycy5qcyIsICIuLi8uLi9ub2RlX21vZHVsZXMvQG5vdmFkaS9jb3JlL2Rpc3QvYXV0b3dpcmUuanMiLCAiLi4vLi4vbm9kZV9tb2R1bGVzL0Bub3ZhZGkvY29yZS9kaXN0L2J1aWxkZXIuanMiLCAiLi4vLi4vbm9kZV9tb2R1bGVzL0Bub3ZhZGkvY29yZS9kaXN0L2NvbnRhaW5lci5qcyIsICIuLi8uLi9zcmMvdjIvZmVhdHVyZXMvZGF0ZS9EYXRlUmVuZGVyZXIudHMiLCAiLi4vLi4vc3JjL3YyL2NvcmUvRGF0ZVNlcnZpY2UudHMiLCAiLi4vLi4vc3JjL3YyL2NvcmUvQmFzZUdyb3VwaW5nUmVuZGVyZXIudHMiLCAiLi4vLi4vc3JjL3YyL2ZlYXR1cmVzL3Jlc291cmNlL1Jlc291cmNlUmVuZGVyZXIudHMiLCAiLi4vLi4vc3JjL3YyL2ZlYXR1cmVzL3RlYW0vVGVhbVJlbmRlcmVyLnRzIiwgIi4uLy4uL3NyYy92Mi9mZWF0dXJlcy9kZXBhcnRtZW50L0RlcGFydG1lbnRSZW5kZXJlci50cyIsICIuLi8uLi9zcmMvdjIvY29yZS9SZW5kZXJCdWlsZGVyLnRzIiwgIi4uLy4uL3NyYy92Mi9jb3JlL0ZpbHRlclRlbXBsYXRlLnRzIiwgIi4uLy4uL3NyYy92Mi9jb3JlL0NhbGVuZGFyT3JjaGVzdHJhdG9yLnRzIiwgIi4uLy4uL3NyYy92Mi9jb3JlL05hdmlnYXRpb25BbmltYXRvci50cyIsICIuLi8uLi9zcmMvdjIvY29yZS9DYWxlbmRhckV2ZW50cy50cyIsICIuLi8uLi9zcmMvdjIvY29yZS9DYWxlbmRhckFwcC50cyIsICIuLi8uLi9zcmMvdjIvZmVhdHVyZXMvdGltZWF4aXMvVGltZUF4aXNSZW5kZXJlci50cyIsICIuLi8uLi9zcmMvdjIvY29yZS9TY3JvbGxNYW5hZ2VyLnRzIiwgIi4uLy4uL3NyYy92Mi9jb3JlL0hlYWRlckRyYXdlck1hbmFnZXIudHMiLCAiLi4vLi4vc3JjL3YyL2RlbW8vTW9ja1N0b3Jlcy50cyIsICIuLi8uLi9zcmMvdjIvZGVtby9EZW1vQXBwLnRzIiwgIi4uLy4uL3NyYy92Mi9jb3JlL0V2ZW50QnVzLnRzIiwgIi4uLy4uL3NyYy92Mi9zdG9yYWdlL0luZGV4ZWREQkNvbnRleHQudHMiLCAiLi4vLi4vc3JjL3YyL3N0b3JhZ2UvZXZlbnRzL0V2ZW50U3RvcmUudHMiLCAiLi4vLi4vc3JjL3YyL3N0b3JhZ2UvZXZlbnRzL0V2ZW50U2VyaWFsaXphdGlvbi50cyIsICIuLi8uLi9zcmMvdjIvc3RvcmFnZS9TeW5jUGx1Z2luLnRzIiwgIi4uLy4uL3NyYy92Mi9jb25zdGFudHMvQ29yZUV2ZW50cy50cyIsICIuLi8uLi9ub2RlX21vZHVsZXMvanNvbi1kaWZmLXRzL3NyYy9oZWxwZXJzLnRzIiwgIi4uLy4uL25vZGVfbW9kdWxlcy9qc29uLWRpZmYtdHMvc3JjL2pzb25EaWZmLnRzIiwgIi4uLy4uL25vZGVfbW9kdWxlcy9qc29uLWRpZmYtdHMvc3JjL2pzb25Db21wYXJlLnRzIiwgIi4uLy4uL3NyYy92Mi9zdG9yYWdlL0Jhc2VFbnRpdHlTZXJ2aWNlLnRzIiwgIi4uLy4uL3NyYy92Mi9zdG9yYWdlL2V2ZW50cy9FdmVudFNlcnZpY2UudHMiLCAiLi4vLi4vc3JjL3YyL3N0b3JhZ2UvcmVzb3VyY2VzL1Jlc291cmNlU3RvcmUudHMiLCAiLi4vLi4vc3JjL3YyL3N0b3JhZ2UvcmVzb3VyY2VzL1Jlc291cmNlU2VydmljZS50cyIsICIuLi8uLi9zcmMvdjIvc3RvcmFnZS9ib29raW5ncy9Cb29raW5nU3RvcmUudHMiLCAiLi4vLi4vc3JjL3YyL3N0b3JhZ2UvYm9va2luZ3MvQm9va2luZ1NlcnZpY2UudHMiLCAiLi4vLi4vc3JjL3YyL3N0b3JhZ2UvY3VzdG9tZXJzL0N1c3RvbWVyU3RvcmUudHMiLCAiLi4vLi4vc3JjL3YyL3N0b3JhZ2UvY3VzdG9tZXJzL0N1c3RvbWVyU2VydmljZS50cyIsICIuLi8uLi9zcmMvdjIvc3RvcmFnZS90ZWFtcy9UZWFtU3RvcmUudHMiLCAiLi4vLi4vc3JjL3YyL3N0b3JhZ2UvdGVhbXMvVGVhbVNlcnZpY2UudHMiLCAiLi4vLi4vc3JjL3YyL3N0b3JhZ2UvZGVwYXJ0bWVudHMvRGVwYXJ0bWVudFN0b3JlLnRzIiwgIi4uLy4uL3NyYy92Mi9zdG9yYWdlL2RlcGFydG1lbnRzL0RlcGFydG1lbnRTZXJ2aWNlLnRzIiwgIi4uLy4uL3NyYy92Mi9zdG9yYWdlL3NldHRpbmdzL1NldHRpbmdzU3RvcmUudHMiLCAiLi4vLi4vc3JjL3YyL3R5cGVzL1NldHRpbmdzVHlwZXMudHMiLCAiLi4vLi4vc3JjL3YyL3N0b3JhZ2Uvc2V0dGluZ3MvU2V0dGluZ3NTZXJ2aWNlLnRzIiwgIi4uLy4uL3NyYy92Mi9zdG9yYWdlL3ZpZXdjb25maWdzL1ZpZXdDb25maWdTdG9yZS50cyIsICIuLi8uLi9zcmMvdjIvc3RvcmFnZS92aWV3Y29uZmlncy9WaWV3Q29uZmlnU2VydmljZS50cyIsICIuLi8uLi9zcmMvdjIvc3RvcmFnZS9hdWRpdC9BdWRpdFN0b3JlLnRzIiwgIi4uLy4uL3NyYy92Mi9zdG9yYWdlL2F1ZGl0L0F1ZGl0U2VydmljZS50cyIsICIuLi8uLi9zcmMvdjIvcmVwb3NpdG9yaWVzL01vY2tFdmVudFJlcG9zaXRvcnkudHMiLCAiLi4vLi4vc3JjL3YyL3JlcG9zaXRvcmllcy9Nb2NrUmVzb3VyY2VSZXBvc2l0b3J5LnRzIiwgIi4uLy4uL3NyYy92Mi9yZXBvc2l0b3JpZXMvTW9ja0Jvb2tpbmdSZXBvc2l0b3J5LnRzIiwgIi4uLy4uL3NyYy92Mi9yZXBvc2l0b3JpZXMvTW9ja0N1c3RvbWVyUmVwb3NpdG9yeS50cyIsICIuLi8uLi9zcmMvdjIvcmVwb3NpdG9yaWVzL01vY2tBdWRpdFJlcG9zaXRvcnkudHMiLCAiLi4vLi4vc3JjL3YyL3JlcG9zaXRvcmllcy9Nb2NrVGVhbVJlcG9zaXRvcnkudHMiLCAiLi4vLi4vc3JjL3YyL3JlcG9zaXRvcmllcy9Nb2NrRGVwYXJ0bWVudFJlcG9zaXRvcnkudHMiLCAiLi4vLi4vc3JjL3YyL3JlcG9zaXRvcmllcy9Nb2NrU2V0dGluZ3NSZXBvc2l0b3J5LnRzIiwgIi4uLy4uL3NyYy92Mi9yZXBvc2l0b3JpZXMvTW9ja1ZpZXdDb25maWdSZXBvc2l0b3J5LnRzIiwgIi4uL
|