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>
173 lines
9.7 KiB
JavaScript
173 lines
9.7 KiB
JavaScript
// Übersicht: was diese Woche ansteht, was als Nächstes kommt, was offen ist.
|
|
|
|
import { S, statusSetzen } from "../app.js";
|
|
import {
|
|
heute, plus, montag, kw, ttmm, wt, kurzDatum, tageZwischen, abstand,
|
|
esc, mdInline, ohneMd, kanalBadges, art, pille, zustaendig, istOffen, verlaufSvg,
|
|
WT_LANG, MONATE, tag,
|
|
} from "../hilfen.js";
|
|
|
|
export function aktuellePhase(h) {
|
|
const phasen = S.plan.phasen;
|
|
const p = phasen.find((x) => h >= x.von && h <= x.bis);
|
|
if (p) return `Phase ${p.nr}, ${p.name}`;
|
|
if (h < phasen[0].von) return "Vorbereitung";
|
|
const pause = S.plan.pausen.find((x) => h >= x.von && h <= x.bis);
|
|
if (pause) return pause.titel;
|
|
return "nach dem Plan";
|
|
}
|
|
|
|
const istPost = (s) => !s.kanaele.includes("intern");
|
|
|
|
function slotZeile(s) {
|
|
return `<a class="e ${art(s)} ${s.wichtig ? "wichtig" : ""} st-${esc(s.status)}" href="#/plan/${esc(s.id)}">
|
|
<span class="f">${esc(ohneMd(s.format))}</span>
|
|
<span class="u">${s.status !== "geplant" ? `<span class="s">${esc(s.status)}</span>` : ""}</span>
|
|
</a>`;
|
|
}
|
|
|
|
function aufgabe(s) {
|
|
return `<label class="aufgabe" title="${esc(ohneMd(s.text))}">
|
|
<input type="checkbox" data-schritt="${esc(s.id)}" ${s.status === "erledigt" ? "checked" : ""}>
|
|
<span class="id">${esc(s.id)}</span>
|
|
<span class="was">${esc(s.kurz)}</span>
|
|
<span class="wer">${esc(s.wer)}</span>
|
|
</label>`;
|
|
}
|
|
|
|
// Viele Schritte an einem Tag: die ersten vier zeigen, den Rest zum Aufklappen.
|
|
function aufgaben(schritte) {
|
|
if (schritte.length <= 5) return schritte.map(aufgabe).join("");
|
|
return schritte.slice(0, 4).map(aufgabe).join("") +
|
|
`<details><summary>${schritte.length - 4} weitere Schritte</summary>${schritte.slice(4).map(aufgabe).join("")}</details>`;
|
|
}
|
|
|
|
export function zeichnen(main) {
|
|
const h = heute();
|
|
const d = tag(h);
|
|
const posts = S.slots.filter(istPost);
|
|
|
|
// Zähler: vor dem ersten Beitrag zählt der Start, danach der nächste Beitrag.
|
|
const erschienen = posts.some((s) => s.status === "erschienen");
|
|
const naechster = posts.find((s) => s.datum >= h && s.status !== "erschienen" && s.status !== "ausgefallen");
|
|
let zaehler = `<div class="zaehler"><span>Kein Beitrag mehr im Plan.</span></div>`;
|
|
if (naechster) {
|
|
const n = tageZwischen(h, naechster.datum);
|
|
const erst = !erschienen && naechster === posts[0];
|
|
const was = `<a href="#/plan/${esc(naechster.id)}">${kurzDatum(naechster.datum)}, ${esc(ohneMd(naechster.format))}</a>`;
|
|
zaehler = n === 0
|
|
? `<div class="zaehler"><b>Heute</b><span>${erst ? "erscheint der erste Beitrag" : "erscheint"}:<br>${was}</span></div>`
|
|
: `<div class="zaehler"><b>${n}</b><span>${n === 1 ? "Tag" : "Tage"} bis zum ${erst ? "ersten" : "nächsten"} Beitrag<br>${was}</span></div>`;
|
|
}
|
|
|
|
// Überfälliges
|
|
const ueberfaellig = S.schritte.filter((s) => istOffen(s) && s.faellig && s.faellig < h && !s.wiederkehrend);
|
|
const unmarkiert = posts.filter((s) => s.datum < h && (s.status === "geplant" || s.status === "fertig"));
|
|
let warnung = "";
|
|
if (ueberfaellig.length || unmarkiert.length) {
|
|
warnung = `<section class="block warnung" style="margin-bottom:34px"><h2>Offen geblieben</h2>
|
|
${ueberfaellig.length ? `<div class="zeile" style="grid-template-columns:150px 1fr"><span>Roadmap, über dem Termin</span><span>${ueberfaellig.map((s) => `${esc(s.id)} ${esc(s.kurz)}`).join(", ")}</span></div>` : ""}
|
|
${unmarkiert.length ? `<div class="zeile" style="grid-template-columns:150px 1fr"><span>Beiträge ohne Status</span><span>${unmarkiert.map((s) => `<a href="#/plan/${esc(s.id)}">${kurzDatum(s.datum)}</a>`).join(", ")}</span></div>` : ""}
|
|
</section>`;
|
|
}
|
|
|
|
// Diese Woche, Tag für Tag
|
|
const m = montag(h);
|
|
const tage = [];
|
|
for (let i = 0; i < 7; i++) {
|
|
const t = plus(m, i);
|
|
const slots = S.slots.filter((s) => s.datum === t || (s.bis && s.datum < t && s.bis >= t));
|
|
const schritte = S.schritte.filter((s) => s.faellig === t && s.status !== "später" && s.status !== "entfällt");
|
|
if (!slots.length && !schritte.length && t !== h) continue;
|
|
tage.push(`<div class="zeile tagzeile ${t === h ? "heute" : ""} ${t < h ? "vorbei" : ""}">
|
|
<div class="datum">${wt(t)} ${ttmm(t)}<span class="klein">${t === h ? "heute" : abstand(h, t)}</span></div>
|
|
<div>${slots.map(slotZeile).join("")}${aufgaben(schritte)}${!slots.length && !schritte.length ? `<span class="muted">Nichts geplant.</span>` : ""}</div>
|
|
</div>`);
|
|
}
|
|
|
|
// Als Nächstes
|
|
const kommende = posts.filter((s) => s.datum >= h && s.status !== "ausgefallen" && s.status !== "erschienen").slice(0, 7);
|
|
const naechste = kommende.map((s) => `<a class="zeile naechst ${s.wichtig ? "wichtig" : ""}" href="#/plan/${esc(s.id)}">
|
|
<span class="datum">${kurzDatum(s.datum)}</span>
|
|
<span><span class="art ${art(s)}"></span><b>${esc(ohneMd(s.format))}</b>${s.notiz ? `<span class="klein notiz">${mdInline(s.notiz)}</span>` : ""}</span>
|
|
${kanalBadges(s.kanaele)}
|
|
</a>`).join("");
|
|
|
|
// Entscheidungen
|
|
const tests = S.plan.tests.map((t) => ({ ...t, naechst: t.termine.find((x) => x >= h) || null }))
|
|
.filter((t) => t.naechst || !t.termine.length)
|
|
.sort((a, b) => (a.naechst || "9999").localeCompare(b.naechst || "9999"));
|
|
const testsHtml = tests.map((t) => `<div class="zeile" style="grid-template-columns:76px 1fr">
|
|
<span class="datum">${t.naechst ? kurzDatum(t.naechst) : "offen"}<span class="klein">${t.naechst ? abstand(h, t.naechst) : esc(t.wann || "")}</span></span>
|
|
<span><b>${t.id.startsWith("T") ? esc(t.id) + " " : ""}${esc(t.titel)}</b><span class="klein" style="display:block">${esc(t.frage)}</span>
|
|
${t.ergebnis ? `<span class="klein" style="display:block">Ergebnis: ${mdInline(t.ergebnis)}</span>` : ""}</span>
|
|
</div>`).join("");
|
|
|
|
// Kanäle
|
|
const kanaeleHtml = S.kanaele.kanaele.map((k) => `<div class="zeile" style="grid-template-columns:1fr auto">
|
|
<span>${k.url ? `<a href="${esc(k.url)}" target="_blank" rel="noopener"><b>${esc(k.name)}</b></a>` : `<b>${esc(k.name)}</b>`}
|
|
<span class="klein" style="display:block">${esc(k.notiz)}${k.schritt && k.status !== "aktiv" ? `, Schritt ${esc(k.schritt)}` : ""}${k.metricool ? ", in Metricool" : ""}</span></span>
|
|
${pille(k.status)}
|
|
</div>`).join("");
|
|
|
|
// Instagram
|
|
const verlauf = S.kanaele.follower_instagram;
|
|
const letzter = verlauf.at(-1);
|
|
const ns = S.kanaele.nordstern;
|
|
const instaHtml = letzter ? `<div style="display:flex;align-items:baseline;gap:12px;margin-top:4px">
|
|
<span class="gross">${letzter.wert}</span><span class="klein">Follower, ${letzter.wert - verlauf[0].wert} mehr als am ${ttmm(verlauf[0].datum)}</span></div>
|
|
${verlaufSvg(verlauf)}
|
|
<div class="klein">Stand ${ttmm(letzter.datum)}, aus Metricool.</div>
|
|
<div class="klein" style="margin-top:12px;padding-top:10px;border-top:1px solid var(--linie-2)"><b style="color:var(--tinte)">Nordstern:</b> ${esc(ns.name)}. ${ns.ziel ? `Ziel ${esc(ns.ziel)} pro Monat.` : esc(ns.hinweis)}</div>` : `<div class="leer">Noch keine Zahlen.</div>`;
|
|
|
|
// Roadmap je Paket
|
|
const roadmapHtml = S.roadmap.pakete.map((p) => {
|
|
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 `<a class="zeile" href="#/roadmap" style="grid-template-columns:28px 1fr 64px;text-decoration:none">
|
|
<span class="id">${esc(p.id)}</span>
|
|
<span><span style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(p.titel)}</span><span class="balken" style="display:block;margin-top:5px"><span style="width:${anteil}%"></span></span></span>
|
|
<span class="klein" style="text-align:right">${fertig} von ${zaehlen.length}</span>
|
|
</a>`;
|
|
}).join("");
|
|
|
|
// Wer ist dran
|
|
const personenHtml = S.roadmap.personen.map((p) => {
|
|
const eigene = S.schritte.filter((s) => istOffen(s) && zustaendig(s, p.name));
|
|
const n = eigene.filter((s) => s.faellig).sort((a, b) => a.faellig.localeCompare(b.faellig))[0];
|
|
return `<div class="zeile" style="grid-template-columns:70px 1fr 28px">
|
|
<b>${esc(p.name)}</b>
|
|
<span class="klein">${n ? `${esc(n.id)} ${esc(n.kurz)}, ${kurzDatum(n.faellig)}` : esc(p.zusatz || "nichts mit Termin")}</span>
|
|
<span style="text-align:right;font-weight:700">${eigene.length}</span>
|
|
</div>`;
|
|
}).join("");
|
|
|
|
main.innerHTML = `
|
|
<header class="seitenkopf">
|
|
<div><div class="ueber">KW ${kw(h)}, ${esc(aktuellePhase(h))}</div>
|
|
<h1>${WT_LANG[d.getUTCDay()]}, ${d.getUTCDate()}. ${MONATE[d.getUTCMonth()]}</h1>
|
|
<div class="thema">Thema: <b>${esc(S.plan.thema.name)}</b></div></div>
|
|
${zaehler}
|
|
</header>
|
|
${warnung}
|
|
<div class="raster r-2-1">
|
|
<section class="block"><h2>Diese Woche <span class="neben">KW ${kw(h)}, ${ttmm(m)} bis ${ttmm(plus(m, 6))}</span></h2>${tage.join("")}</section>
|
|
<section class="block"><h2>Als Nächstes <a class="neben" href="#/plan">Redaktionsplan</a></h2>${naechste || `<div class="leer">Nichts mehr geplant.</div>`}</section>
|
|
</div>
|
|
<div class="raster r-3">
|
|
<section class="block"><h2>Entscheidungen</h2>${testsHtml || `<div class="leer">Keine offenen.</div>`}</section>
|
|
<section class="block"><h2>Kanäle</h2>${kanaeleHtml}</section>
|
|
<section class="block"><h2>Instagram <span class="neben">hivegrid.de</span></h2>${instaHtml}</section>
|
|
</div>
|
|
<div class="raster r-2">
|
|
<section class="block"><h2>Roadmap <a class="neben" href="#/roadmap">alle Schritte</a></h2>${roadmapHtml}</section>
|
|
<section class="block"><h2>Wer ist dran <span class="neben">offene Schritte</span></h2>${personenHtml}</section>
|
|
</div>
|
|
`;
|
|
|
|
main.querySelectorAll("[data-schritt]").forEach((box) => {
|
|
box.onchange = () => statusSetzen("roadmap", box.dataset.schritt, box.checked ? "erledigt" : "offen");
|
|
});
|
|
}
|