Files
HiveGrid-Marketing/Dashboard/seiten/roadmap.js
T
dejanandClaude Opus 5.5 686ee36b4f HiveGrid Marketing: Hub mit Strategie, Redaktionsplan, Dashboard, Produktionsvorgaben und Webinar
Stand 16.10.2026. Enthält das Marketing-Dashboard, die Daten in daten/,
die Produktionsvorgaben, Reels und das Webinar mit Skript, Sprechzettel
und Präsentation (HTML, PowerPoint, PDF).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:09:07 +02:00

115 lines
6.4 KiB
JavaScript

// Roadmap: Zeitstrahl, Arbeitspakete, Status je Schritt.
import { S, statusSetzen } from "../app.js";
import {
heute, plus, tag, ttmm, kurzDatum, MON_KURZ,
esc, mdInline, ohneMd, zustaendig, istOffen, SCHRITT_STATUS, dokLink,
} from "../hilfen.js";
const SPEICHER = "hg-roadmap";
const zustand = { person: "alle", status: "alle" };
try { Object.assign(zustand, JSON.parse(localStorage.getItem(SPEICHER) || "{}")); } catch { /* egal */ }
const merken = () => { try { localStorage.setItem(SPEICHER, JSON.stringify(zustand)); } catch { /* egal */ } };
const ROADMAP_MD = "Brainstorming Kickoff/Strategie/Roadmap.md";
function passt(s) {
if (zustand.person !== "alle" && !zustaendig(s, zustand.person)) return false;
if (zustand.status === "offen" && !istOffen(s)) return false;
if (zustand.status === "erledigt" && s.status !== "erledigt") return false;
if (zustand.status === "später" && s.status !== "später") return false;
return true;
}
function zeitstrahl(h) {
const eintraege = S.roadmap.zeitstrahl;
const von = eintraege[0].von.slice(0, 8) + "01";
const ende = eintraege.at(-1).bis;
const bis = plus(`${ende.slice(0, 7)}-01`, 32).slice(0, 8) + "01";
const gesamt = tag(bis) - tag(von);
const pos = (d) => Math.max(0, Math.min(100, ((tag(d) - tag(von)) / gesamt) * 100));
const monate = [];
for (let d = von; d < bis; d = plus(`${d.slice(0, 7)}-01`, 32).slice(0, 8) + "01") {
const m = Number(d.slice(5, 7)) - 1;
monate.push(`<span style="left:${pos(d)}%">${MON_KURZ[m]}${m === 0 ? " " + d.slice(0, 4) : ""}</span>`);
}
const zeilen = eintraege.map((z) => {
const a = pos(z.von);
const e = pos(plus(z.bis, 1));
const klasse = z.offen ? "offen" : h > z.bis ? "vorbei" : h >= z.von ? "jetzt" : "";
const text = ohneMd(z.text);
// Text rechts neben den Balken, außer der Balken reicht schon weit nach rechts.
const textStil = e < 60 ? `left:${e}%;max-width:${100 - e}%` : `right:${100 - a}%;max-width:${a}%;text-align:right;padding-left:0;padding-right:8px`;
return `<div class="zs-zeile"><div class="wann">${esc(ohneMd(z.wann))}</div>
<div class="zs-bahn"><div class="zs-balken ${klasse}" style="left:${a}%;width:${Math.max(e - a, 0.6)}%" title="${esc(text)}"></div>
<div class="zs-text" style="${textStil};overflow:hidden;text-overflow:ellipsis" title="${esc(text)}">${esc(text)}</div></div></div>`;
}).join("");
const heuteLinie = h >= von && h < bis
? `<div class="zs-heute-linie" style="left:calc(170px + (100% - 170px) * ${pos(h) / 100})" title="heute"></div>` : "";
return `<section class="block"><h2>Zeitstrahl</h2><div class="zs-rahmen"><div class="zs-skala">${monate.join("")}</div>${zeilen}${heuteLinie}</div></section>`;
}
const ABSCHNITT = { A: "A.", B1: "B1.", C: "C.", D: "D.", E: "E.", F: "F.", G: "G.", H: "H." };
function paket(p, h) {
const schritte = p.schritte.filter(passt);
if (!schritte.length) return "";
const zaehlen = p.schritte.filter((s) => s.status !== "entfällt");
const fertig = zaehlen.filter((s) => s.status === "erledigt").length;
const anteil = zaehlen.length ? Math.round((fertig / zaehlen.length) * 100) : 0;
return `<section class="block paket">
<h2><span><span class="pid">${esc(p.id)}</span>${esc(p.titel)}</span>
<a class="neben" href="${dokLink(ROADMAP_MD, `?abschnitt=${encodeURIComponent(ABSCHNITT[p.id] || p.id)}`)}">im Dokument</a></h2>
<div class="paket-unter"><span>${fertig} von ${zaehlen.length} erledigt</span><span class="balken"><span style="width:${anteil}%"></span></span></div>
${schritte.map((s) => {
const ueber = istOffen(s) && s.faellig && s.faellig < h && !s.wiederkehrend;
return `<div class="schritt ${esc(s.status)}">
<input type="checkbox" data-haken="${esc(s.id)}" ${s.status === "erledigt" ? "checked" : ""} title="erledigt">
<div class="sid">${esc(s.id)}</div>
<div><div class="kurz">${esc(s.kurz)}</div>
<details><summary>Details${s.abhaengig ? `, braucht ${esc(s.abhaengig)}` : ""}</summary><div class="lang">${mdInline(s.text)}${s.voraussetzung ? ` <span class="muted">Voraussetzung: ${esc(s.voraussetzung)}.</span>` : ""}</div></details></div>
<div class="wer">${esc(s.wer || "")}</div>
<div class="wann ${ueber ? "ueberfaellig" : ""}">${s.faellig ? kurzDatum(s.faellig) : esc(s.wann || "")}${s.wiederkehrend ? `<span class="klein" style="display:block">wiederkehrend</span>` : ""}</div>
<select data-status="${esc(s.id)}" aria-label="Status">${SCHRITT_STATUS.map((st) => `<option ${st === s.status ? "selected" : ""}>${st}</option>`).join("")}</select>
</div>`;
}).join("")}
</section>`;
}
export function zeichnen(main) {
const h = heute();
const r = S.roadmap;
const personen = ["alle", ...r.personen.map((p) => p.name)];
const schalter = (gruppe, wert, text) =>
`<button class="${zustand[gruppe] === wert ? "an" : ""}" data-gruppe="${gruppe}" data-wert="${esc(wert)}">${esc(text)}</button>`;
const alle = S.schritte;
const offenZahl = alle.filter(istOffen).length;
const fertigZahl = alle.filter((s) => s.status === "erledigt").length;
const pakete = r.pakete.map((p) => paket(p, h)).join("");
main.innerHTML = `
<header class="seitenkopf">
<div><h1>Roadmap</h1>
<div class="unter">${offenZahl} offen, ${fertigZahl} erledigt. ${esc(r.status)} vom ${ttmm(r.stand)}${r.stand.slice(0, 4)}.</div></div>
<a class="knopf" href="${dokLink(ROADMAP_MD)}">Roadmap.md lesen</a>
</header>
${zeitstrahl(h)}
<div class="schalterleiste">
<div class="schalter"><span class="lbl">Wer</span>${personen.map((p) => schalter("person", p, p === "alle" ? "Alle" : p)).join("")}</div>
<div class="schalter"><span class="lbl">Status</span>${[["alle", "Alle"], ["offen", "offen"], ["erledigt", "erledigt"], ["später", "später"]].map(([w, t]) => schalter("status", w, t)).join("")}</div>
</div>
${pakete || `<div class="leer">Keine Schritte mit diesem Filter.</div>`}
`;
main.querySelectorAll("[data-gruppe]").forEach((b) => {
b.onclick = () => { zustand[b.dataset.gruppe] = b.dataset.wert; merken(); zeichnen(main); };
});
main.querySelectorAll("[data-haken]").forEach((box) => {
box.onchange = () => statusSetzen("roadmap", box.dataset.haken, box.checked ? "erledigt" : "offen");
});
main.querySelectorAll("select[data-status]").forEach((sel) => {
sel.onchange = () => statusSetzen("roadmap", sel.dataset.status, sel.value);
});
}