128 lines
7.2 KiB
JavaScript
128 lines
7.2 KiB
JavaScript
const hier = {chart: null, sig: "", closed: new Set(), ids: 0};
|
||||
|
|
const OFFICE_ICONS = {chair: "bi-award", line: "bi-person-badge", cluster: "bi-collection", work: "bi-briefcase", other: "bi-signpost-split"};
|
|||
|
|
const OFFICE_GROUPS = [["cluster", "Organisatoren", "Organisator"], ["work", "Präsidenten", "Präsident"], ["other", "Weitere Ämter", "Amt"]];
|
|||
|
|
|
|||
|
|
function hierarchyScreen() {
|
|||
|
|
hier.chart = h("section", {class: "card org-chart", "aria-labelledby": "org-title"});
|
|||
|
|
orderDom.box = h("section", {class: "card agent-order", id: "agent-order", "aria-labelledby": "agent-order-title"});
|
|||
|
|
hier.sig = "";
|
|||
|
|
return h("div", {class: "hierarchy-body"}, hier.chart, orderDom.box);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function hierarchySig() {
|
|||
|
|
const held = state.claims.map((claim) => [claim.office, claim.session, claim.agent, claim.work, claim.beat !== null && claim.beat >= state.now - ACTIVE_BEAT]);
|
|||
|
|
return JSON.stringify([held, state.agent_order, state.people]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function drawHierarchy(force) {
|
|||
|
|
if (!hier.chart || !hier.chart.isConnected) {
|
|||
|
|
hier.chart = null;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const sig = hierarchySig();
|
|||
|
|
if (!force && sig === hier.sig) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
hier.sig = sig;
|
|||
|
|
drawChart();
|
|||
|
|
drawAgentOrder();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function focusKey(box) {
|
|||
|
|
const at = document.activeElement;
|
|||
|
|
return at && box.contains(at) ? at.dataset.focus || null : null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function refocus(box, key) {
|
|||
|
|
const found = key === null ? null : [...box.querySelectorAll("[data-focus]")].find((element) => element.dataset.focus === key);
|
|||
|
|
if (found) {
|
|||
|
|
found.focus();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function officeBtn(name, label, hint, run, focus) {
|
|||
|
|
const button = iconBtn(name, label, "btn small ghost", run, hint);
|
|||
|
|
button.dataset.focus = focus;
|
|||
|
|
return button;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function holderOf(claim) {
|
|||
|
|
return claim.agent || (claim.session === "-" ? "" : claim.session);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function officeRow(claim, label, title) {
|
|||
|
|
const who = holderOf(claim);
|
|||
|
|
const active = claim.beat !== null && claim.beat >= state.now - ACTIVE_BEAT;
|
|||
|
|
return h("div", {class: "org-row", "data-office": claim.office},
|
|||
|
|
h("span", {class: "org-office", title: `${title} ${claim.office}`}, icon(OFFICE_ICONS[claim.kind]), h("strong", {}, label)),
|
|||
|
|
who ? h("button", {type: "button", class: "linkish org-holder", "data-focus": `agent:${claim.office}`, title: `${who} öffnen`, on: {click: () => openAgent(who)}}, face(who, false), who) : h("span", {class: "muted"}, "–"),
|
|||
|
|
active ? chip("bi-play-circle", "aktiv", "ok") : chip("bi-pause-circle", "ohne Herzschlag", "neutral"),
|
|||
|
|
claim.work && claim.work !== "-" ? h("span", {class: "muted org-work", title: claim.work}, short(claim.work)) : null,
|
|||
|
|
h("span", {class: "org-actions"},
|
|||
|
|
officeBtn("bi-arrow-left-right", "Übergeben", `Übergeben: ${claim.office} an eine andere Session, mit Passkey-Signatur`, () => officeSheet("takeover", claim), `takeover:${claim.office}`),
|
|||
|
|
officeBtn("bi-person-x", "Abberufen", `Abberufen: ${claim.office} frei machen, mit Passkey-Signatur`, () => officeSheet("recall", claim), `recall:${claim.office}`)));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function vacantRow(office, label) {
|
|||
|
|
return h("div", {class: "org-row vacant", "data-office": office},
|
|||
|
|
h("span", {class: "org-office"}, icon(OFFICE_ICONS.chair), h("strong", {}, label)),
|
|||
|
|
pill("unbesetzt", "warn"),
|
|||
|
|
h("span", {class: "org-actions"}, officeBtn("bi-person-plus", "Besetzen", `Besetzen: ${office} einer Session geben, mit Passkey-Signatur`, () => officeSheet("appoint", {office, session: "", work: ""}), `appoint:${office}`)));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function groupRow(glyph, label, count, note) {
|
|||
|
|
return h("div", {class: "org-row group"}, h("span", {class: "org-office"}, icon(glyph), h("strong", {}, label)), counter(count), h("span", {class: "muted"}, note));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function orgNode(id, label, row, children) {
|
|||
|
|
if (!children.length) {
|
|||
|
|
return h("li", {}, h("div", {class: "org-line"}, h("span", {class: "org-gap", "aria-hidden": "true"}), row));
|
|||
|
|
}
|
|||
|
|
const open = !hier.closed.has(id);
|
|||
|
|
hier.ids += 1;
|
|||
|
|
const list = h("ul", {class: "org-tree", id: `org-${hier.ids}`, hidden: !open}, children);
|
|||
|
|
const toggle = h("button", {type: "button", class: "btn small ghost icon-only org-toggle", "aria-expanded": String(open), "aria-controls": list.id, "data-focus": `toggle:${id}`, ...named(`${open ? "Zuklappen" : "Aufklappen"}: ${label}`)}, icon(open ? "bi-chevron-down" : "bi-chevron-right"));
|
|||
|
|
toggle.addEventListener("click", () => {
|
|||
|
|
const now = list.hidden;
|
|||
|
|
list.hidden = !now;
|
|||
|
|
toggle.setAttribute("aria-expanded", String(now));
|
|||
|
|
relabel(toggle, now ? "bi-chevron-down" : "bi-chevron-right", `${now ? "Zuklappen" : "Aufklappen"}: ${label}`);
|
|||
|
|
if (now) {
|
|||
|
|
hier.closed.delete(id);
|
|||
|
|
} else {
|
|||
|
|
hier.closed.add(id);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return h("li", {"data-node": id}, h("div", {class: "org-line"}, toggle, row), list);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function chartTree() {
|
|||
|
|
const claims = state.claims;
|
|||
|
|
const dimensions = new Map();
|
|||
|
|
for (const claim of claims.filter((entry) => entry.kind === "line")) {
|
|||
|
|
dimensions.set(claim.dimension, [...(dimensions.get(claim.dimension) || []), claim]);
|
|||
|
|
}
|
|||
|
|
const lines = [...dimensions].sort(([a], [b]) => a.localeCompare(b)).map(([dimension, held]) => orgNode(`dimension:${dimension}`, dimension,
|
|||
|
|
groupRow("bi-diagram-2", dimension, held.length, held.length === 1 ? "Linie" : "Linien"),
|
|||
|
|
held.map((claim) => orgNode(claim.office, claim.office, officeRow(claim, claim.line, "Repräsentant"), []))));
|
|||
|
|
const groups = OFFICE_GROUPS.map(([kind, label, title]) => [kind, label, title, claims.filter((entry) => entry.kind === kind)]).filter(([, , , held]) => held.length)
|
|||
|
|
.map(([kind, label, title, held]) => orgNode(`group:${kind}`, label, groupRow(OFFICE_ICONS[kind], label, held.length, kind === "other" ? "" : `${kind}/…`),
|
|||
|
|
held.map((claim) => orgNode(claim.office, claim.office, officeRow(claim, kind === "other" ? claim.office : claim.office.slice(kind.length + 1), title), []))));
|
|||
|
|
const chair = claims.find((entry) => entry.kind === "chair");
|
|||
|
|
const chairNode = orgNode("chair", "Chair", chair ? officeRow(chair, "Chair", "Amt") : vacantRow("chair", "Chair"), [...lines, ...groups]);
|
|||
|
|
const root = h("div", {class: "org-row org-root"}, h("span", {class: "org-office"}, icon("bi-person-circle"), h("strong", {}, HUMAN)), h("span", {class: "muted"}, "vergibt jedes Amt mit Passkey-Signatur"));
|
|||
|
|
return h("ul", {class: "org-tree root"}, orgNode("operator", HUMAN, root, [chairNode]));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function drawChart() {
|
|||
|
|
const key = focusKey(hier.chart);
|
|||
|
|
hier.ids = 0;
|
|||
|
|
const count = state.claims.length;
|
|||
|
|
hier.chart.replaceChildren(
|
|||
|
|
h("div", {class: "inbox-head"}, h("h2", {class: "group-title", id: "org-title"}, "Hierarchie", counter(count)),
|
|||
|
|
h("span", {class: "muted"}, "Formale Ämter aus dem Council: Operator → Chair → Dimensionen → Linien, dazu Organisatoren und Präsidenten"),
|
|||
|
|
h("span", {class: "spacer"}),
|
|||
|
|
h("button", {type: "button", class: "btn small soft", "data-focus": "appoint:new", ...named("Neues Amt", "Neues Amt: ein freies Amt einer Session geben, mit Passkey-Signatur"), on: {click: () => officeSheet("appoint", null)}}, icon("bi-person-plus"), "Neues Amt")),
|
|||
|
|
chartTree());
|
|||
|
|
refocus(hier.chart, key);
|
|||
|
|
}
|