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>
414 lines
24 KiB
JavaScript
414 lines
24 KiB
JavaScript
// Redaktionsplan als Kalender oder Liste, mit Filtern und einer Schublade je Slot.
|
|
|
|
import { S, schublade, statusSetzen, meldung } from "../app.js";
|
|
import {
|
|
heute, plus, montag, kw, ttmm, wt, kurzDatum, langDatum, spanne, abstand,
|
|
esc, mdInline, ohneMd, codes, kanalBadges, art, ART_NAME, KANAL_FILTER, SLOT_STATUS, pille,
|
|
funnel, funnelMix, funnelBalken, FUNNEL_NAME,
|
|
} from "../hilfen.js";
|
|
|
|
const SPEICHER = "hg-plan-2";
|
|
const zustand = { ansicht: "kalender", kanal: "alle", saeule: "alle", funnel: "alle", roadmap: false };
|
|
try { Object.assign(zustand, JSON.parse(localStorage.getItem(SPEICHER) || "{}")); } catch { /* egal */ }
|
|
const merken = () => { try { localStorage.setItem(SPEICHER, JSON.stringify(zustand)); } catch { /* egal */ } };
|
|
|
|
function sichtbar(s) {
|
|
if (zustand.kanal !== "alle" && !s.kanaele.includes(zustand.kanal)) return false;
|
|
if (zustand.saeule !== "alle" && s.saeule !== zustand.saeule) return false;
|
|
if (zustand.funnel !== "alle" && funnel(s, S.plan) !== zustand.funnel) return false;
|
|
return true;
|
|
}
|
|
const amTag = (s, d) => s.datum === d || (s.bis && s.datum <= d && s.bis >= d);
|
|
|
|
function eintrag(s) {
|
|
return `<button class="e ${art(s)} ${s.wichtig ? "wichtig" : ""} st-${esc(s.status)}" data-slot="${esc(s.id)}"
|
|
title="${esc(ohneMd(s.format))}${s.notiz ? ": " + esc(ohneMd(s.notiz)) : ""}">
|
|
<span class="f">${esc(ohneMd(s.format))}</span>
|
|
<span class="u">${funnel(s, S.plan) ? `<span class="fu fu-${funnel(s, S.plan)}">${FUNNEL_NAME[funnel(s, S.plan)]}</span>` : "<span></span>"}${s.status !== "geplant" ? `<span class="s">${esc(s.status)}</span>` : ""}</span>
|
|
</button>`;
|
|
}
|
|
|
|
function roadmapEintraege(d) {
|
|
if (!zustand.roadmap) return "";
|
|
const faellig = S.schritte.filter((s) => s.faellig === d && s.status !== "später" && s.status !== "entfällt");
|
|
if (!faellig.length) return "";
|
|
if (faellig.length > 2) {
|
|
const fertig = faellig.filter((s) => s.status === "erledigt").length;
|
|
return `<a class="e roadmap" href="#/roadmap" title="${esc(faellig.map((s) => s.id + " " + s.kurz).join("\n"))}">
|
|
${faellig.length} Roadmap-Schritte fällig, ${fertig} erledigt</a>`;
|
|
}
|
|
return faellig.map((s) => `<a class="e roadmap" href="#/roadmap" title="${esc(ohneMd(s.text))}">
|
|
${esc(s.id)} ${esc(s.kurz)}${s.status === "erledigt" ? ", erledigt" : ""}</a>`).join("");
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Kalender
|
|
|
|
function kalender(h) {
|
|
const phasen = S.plan.phasen;
|
|
const erster = phasen[0].von;
|
|
const startM = montag(plus(erster, -7));
|
|
const letzte = S.plan.pausen.length ? S.plan.pausen.at(-1).bis : phasen.at(-1).bis;
|
|
const spaetester = S.slots.length ? S.slots.at(-1).datum : letzte;
|
|
const endeM = montag(spaetester > letzte ? spaetester : letzte);
|
|
const vorbereitung = S.roadmap.zeitstrahl.find((z) => z.von <= startM && z.bis >= startM);
|
|
|
|
let html = `<div class="kal-kopf"><div></div>${["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"].map((t) => `<div>${t}</div>`).join("")}</div>`;
|
|
let block = null;
|
|
for (let m = startM; m <= endeM; m = plus(m, 7)) {
|
|
const so = plus(m, 6);
|
|
const phase = phasen.find((p) => m >= p.von && m <= p.bis);
|
|
const pause = S.plan.pausen.find((p) => m >= montag(p.von) && m <= p.bis);
|
|
const neu = phase ? `p${phase.nr}` : pause ? "pause" : m < erster ? "vor" : "nach";
|
|
if (neu !== block) {
|
|
block = neu;
|
|
if (phase) html += `<div class="kal-phase"><b>Phase ${phase.nr}, ${esc(phase.name)}</b><span class="klein">${spanne(phase.von, phase.bis)}</span><span class="klein phase-zusatz">${esc(phase.text)}</span></div>`;
|
|
else if (pause) html += `<div class="kal-phase"><b>${esc(pause.titel)}</b><span class="klein">${spanne(pause.von, pause.bis)}</span><span class="klein phase-zusatz">${esc(pause.text)}</span></div>`;
|
|
else if (neu === "vor") html += `<div class="kal-phase"><b>Vorbereitung</b><span class="klein">${vorbereitung ? esc(ohneMd(vorbereitung.text).replace(/^Vorbereitung:\s*/, "")) : "Kanäle anlegen"}</span></div>`;
|
|
else html += `<div class="kal-phase"><b>Nach dem Plan</b></div>`;
|
|
}
|
|
const info = S.plan.wochen.find((w) => w.kw === kw(m)) || {};
|
|
const diese = h >= m && h <= so;
|
|
if (pause) {
|
|
html += `<div class="kal-woche ${diese ? "diese" : ""}"><div class="kal-kw"><div class="nr">KW ${kw(m)}</div><div class="klein">${spanne(m, so)}</div></div>
|
|
<div class="kal-tag" style="grid-column:2 / -1"><span class="muted">${esc(pause.titel)}</span>${tageMitSlots(m)}</div></div>`;
|
|
continue;
|
|
}
|
|
html += `<div class="kal-woche ${diese ? "diese" : ""}"><div class="kal-kw"><div class="nr">KW ${kw(m)}</div>
|
|
<div class="klein">${spanne(m, so)}</div>${info.titel ? `<div class="titel">${esc(info.titel)}</div>` : ""}</div>`;
|
|
for (let i = 0; i < 7; i++) {
|
|
const d = plus(m, i);
|
|
const slots = S.slots.filter((s) => amTag(s, d) && sichtbar(s));
|
|
html += `<div class="kal-tag ${i > 4 ? "we" : ""} ${d === h ? "heute" : ""} ${d < h ? "vorbei" : ""}">
|
|
<div class="dnr">${ttmm(d)}${d === h ? " heute" : ""}</div>${slots.map(eintrag).join("")}${roadmapEintraege(d)}</div>`;
|
|
}
|
|
html += `</div>`;
|
|
}
|
|
if (S.plan.ausblick) {
|
|
const a = S.plan.ausblick;
|
|
html += `<div class="kal-phase"><b>${esc(a.titel)}</b><span class="klein">${a.punkte.map((p) => esc(p)).join(" ")}</span></div>`;
|
|
}
|
|
return `<div class="kalender">${html}</div>`;
|
|
}
|
|
|
|
// In einer Pausenwoche stehen keine festen Slots. Falls doch einer dort landet, nicht verschlucken.
|
|
function tageMitSlots(m) {
|
|
const slots = S.slots.filter((s) => s.datum >= m && s.datum <= plus(m, 6) && sichtbar(s));
|
|
return slots.length ? `<div style="margin-top:8px;max-width:320px">${slots.map(eintrag).join("")}</div>` : "";
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Liste
|
|
|
|
function tabelle(slots, h) {
|
|
return `<div class="tab"><table class="daten">
|
|
<thead><tr><th>Datum</th><th>Format</th><th>Kanäle</th><th>Blickwinkel</th><th>Funnel</th><th>Notiz</th><th>Status</th></tr></thead>
|
|
<tbody>${slots.map((s) => `<tr class="klickbar ${s.datum < h ? "vorbei" : ""} ${amTag(s, h) ? "heute" : ""}" data-slot="${esc(s.id)}">
|
|
<td class="datum ${s.wichtig ? "wichtig" : ""}">${s.bis ? `${kurzDatum(s.datum)} bis ${kurzDatum(s.bis)}` : kurzDatum(s.datum)}</td>
|
|
<td><span class="art ${art(s)}"></span>${mdInline(s.format)}</td>
|
|
<td>${kanalBadges(s.kanaele)}</td>
|
|
<td>${esc(S.plan.saeulen.find((x) => x.kuerzel === s.saeule)?.name || s.saeule)}</td>
|
|
<td>${funnel(s, S.plan) ? `<span class="fu fu-${funnel(s, S.plan)}">${FUNNEL_NAME[funnel(s, S.plan)]}</span>` : ""}</td>
|
|
<td>${mdInline(s.notiz)}</td>
|
|
<td>${pille(s.status)}</td>
|
|
</tr>`).join("")}</tbody></table></div>`;
|
|
}
|
|
|
|
function wochen(slots, h) {
|
|
const nach = new Map();
|
|
for (const s of slots) {
|
|
const m = montag(s.datum);
|
|
if (!nach.has(m)) nach.set(m, []);
|
|
nach.get(m).push(s);
|
|
}
|
|
return [...nach.entries()].map(([m, liste]) => {
|
|
const info = S.plan.wochen.find((w) => w.kw === kw(m)) || {};
|
|
return `<div class="zwischentitel">KW ${kw(m)}${info.titel ? `, ${esc(info.titel)}` : ""}<span class="klein">${spanne(m, plus(m, 6))}</span></div>
|
|
${info.text ? `<p class="phase-text">${esc(info.text)}</p>` : ""}${tabelle(liste, h)}`;
|
|
}).join("");
|
|
}
|
|
|
|
function liste(h) {
|
|
const slots = S.slots.filter(sichtbar);
|
|
const vergeben = new Set();
|
|
let html = "";
|
|
S.plan.phasen.forEach((p) => {
|
|
const drin = slots.filter((s) => s.datum >= p.von && s.datum <= p.bis);
|
|
drin.forEach((s) => vergeben.add(s.id));
|
|
html += `<section class="block"><h2 class="phase-titel">Phase ${p.nr}, ${esc(p.name)}<span class="klein">${spanne(p.von, p.bis)}</span></h2>
|
|
<p class="phase-text">${esc(p.text)}</p>${wochen(drin, h) || `<div class="leer">Keine Slots mit diesem Filter.</div>`}</section>`;
|
|
});
|
|
const rest = slots.filter((s) => !vergeben.has(s.id));
|
|
if (rest.length) html += `<section class="block"><h2 class="phase-titel">Weitere Slots</h2>${wochen(rest, h)}</section>`;
|
|
for (const p of S.plan.pausen) html += `<section class="block"><h2 class="phase-titel">${esc(p.titel)}<span class="klein">${spanne(p.von, p.bis)}</span></h2><p class="phase-text">${esc(p.text)}</p></section>`;
|
|
if (S.plan.ausblick) {
|
|
const a = S.plan.ausblick;
|
|
html += `<section class="block"><h2 class="phase-titel">${esc(a.titel)}</h2>${a.punkte.map((p) => `<p class="phase-text" style="margin:0">${mdInline(p)}</p>`).join("")}</section>`;
|
|
}
|
|
return html;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Schublade
|
|
|
|
function naechsterWerktag(d) {
|
|
let x = plus(d, 1);
|
|
while (["Sa", "So"].includes(wt(x))) x = plus(x, 1);
|
|
return x;
|
|
}
|
|
|
|
function formatText(code) {
|
|
const f = S.plan.formate.find((x) => x.kuerzel === code);
|
|
const l = S.plan.linkedin.find((x) => x.kuerzel === code);
|
|
if (l) return `<p><b>${esc(code)} ${esc(l.name)}</b></p><p class="klein">${mdInline(l.text)}</p>`;
|
|
if (f) return `<p><b>${esc(code)}</b> ${esc(f.name)}</p><p class="klein">Aufwand ${esc(f.aufwand)}. <a href="#/formate/${encodeURIComponent(code)}">In der Formatbibliothek lesen</a></p>`;
|
|
return `<p><a href="#/formate/${encodeURIComponent(code)}">${esc(code)} in der Formatbibliothek lesen</a></p>`;
|
|
}
|
|
|
|
function slotSchublade(s, h) {
|
|
const phase = S.plan.phasen.find((p) => s.datum >= p.von && s.datum <= p.bis);
|
|
const info = S.plan.wochen.find((w) => w.kw === kw(s.datum)) || {};
|
|
const saeule = S.plan.saeulen.find((x) => x.kuerzel === s.saeule);
|
|
const reihe = S.slots.filter(sichtbar);
|
|
const i = reihe.findIndex((x) => x.id === s.id);
|
|
const vor = i > 0 ? reihe[i - 1] : null;
|
|
const nach = i >= 0 && i < reihe.length - 1 ? reihe[i + 1] : null;
|
|
|
|
// L1 nimmt das Carousel der Vorwoche. Die Notiz nennt das Datum, daraus wird ein Link.
|
|
let grundlage = "";
|
|
const vom = s.notiz.match(/Carousel vom (\d{2})\.(\d{2})\./);
|
|
if (vom) {
|
|
const quelle = S.slots.find((x) => x.datum.endsWith(`-${vom[2]}-${vom[1]}`) && art(x) === "carousel");
|
|
if (quelle) grundlage = `<dt>Grundlage</dt><dd><a href="#/plan/${esc(quelle.id)}">${kurzDatum(quelle.datum)}, ${esc(ohneMd(quelle.format))}</a> ${pille(quelle.status)}</dd>`;
|
|
}
|
|
// Umgekehrt: wird dieses Carousel später als L1 verwertet?
|
|
let verwertet = "";
|
|
if (art(s) === "carousel") {
|
|
const nutzer = S.slots.filter((x) => x.notiz.includes(`Carousel vom ${ttmm(s.datum)}`));
|
|
if (nutzer.length) verwertet = `<dt>Wiederverwertet</dt><dd>${nutzer.map((x) => `<a href="#/plan/${esc(x.id)}">${kurzDatum(x.datum)}, ${esc(ohneMd(x.format))}</a>`).join("<br>")}</dd>`;
|
|
}
|
|
const zeiten = [...new Set(s.kanaele.map((k) => S.plan.uhrzeiten?.[k] ? `${k === "LinkedIn" ? "LinkedIn" : "Instagram und Co."} um ${S.plan.uhrzeiten[k]}` : null).filter(Boolean))];
|
|
|
|
schublade(`
|
|
<div class="ueber">KW ${kw(s.datum)}${info.titel ? `, ${esc(info.titel)}` : ""}${phase ? `. Phase ${phase.nr}, ${esc(phase.name)}` : ""}</div>
|
|
<h2>${esc(ohneMd(s.format))}</h2>
|
|
<div class="unter">${langDatum(s.datum)}${s.bis ? ` bis ${langDatum(s.bis)}` : ""}, ${abstand(h, s.datum)}. ${ART_NAME[art(s)]}.</div>
|
|
<dl class="felder">
|
|
<dt>Status</dt><dd><div class="schalter">${SLOT_STATUS.map((st) => `<button data-status="${st}" class="${s.status === st ? "an" : ""}">${st}</button>`).join("")}</div></dd>
|
|
<dt>Kanäle</dt><dd>${esc(s.kanaele.join(", "))}${zeiten.length ? `<span class="klein">${esc(zeiten.join(", "))}</span>` : ""}</dd>
|
|
${codes(s.format).length ? `<dt>Format</dt><dd>${codes(s.format).map(formatText).join("")}</dd>` : ""}
|
|
${s.saeule ? `<dt>Blickwinkel</dt><dd>${saeule ? `<b>${esc(saeule.name)}</b><span class="klein">${esc(saeule.text)}</span>` : esc(s.saeule === "offen" ? "Meister oder Nerds, je nach Trend, immer im Thema" : s.saeule)}</dd>` : ""}
|
|
${funnel(s, S.plan) ? `<dt>Funnel</dt><dd><span class="fu fu-${funnel(s, S.plan)}">${FUNNEL_NAME[funnel(s, S.plan)]}</span><span class="klein">${esc(S.plan.funnel.stufen.find((x) => x.id === funnel(s, S.plan))?.text || "")}</span></dd>` : ""}
|
|
${s.notiz ? `<dt>Notiz</dt><dd>${mdInline(s.notiz)}</dd>` : ""}
|
|
${s.noah ? `<dt>Für Noah</dt><dd>${esc(s.noah)}</dd>` : ""}
|
|
${grundlage}${verwertet}
|
|
<dt>Kennung</dt><dd><span class="kennung">${esc(s.id)}</span><span class="klein">Zum Ändern Claude sagen, zum Beispiel „Schieb ${esc(s.id)} auf ${kurzDatum(naechsterWerktag(s.datum))}“</span></dd>
|
|
</dl>
|
|
<div class="blaettern">
|
|
${vor ? `<a href="#/plan/${esc(vor.id)}">Vorher: ${kurzDatum(vor.datum)}</a>` : "<span></span>"}
|
|
${nach ? `<a href="#/plan/${esc(nach.id)}">Danach: ${kurzDatum(nach.datum)}</a>` : ""}
|
|
</div>
|
|
`, { zurueck: "#/plan" }).querySelectorAll("[data-status]").forEach((b) => {
|
|
b.onclick = () => statusSetzen("redaktionsplan", s.id, b.dataset.status);
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Funnel
|
|
|
|
// Mix je Phase und gesamt, mit der Zielspanne für Top (Vorgabe Noah).
|
|
function funnelBlock() {
|
|
const p = S.plan;
|
|
const ziel = p.funnel.ziel_top;
|
|
// Noahs Vorgabe gilt für die Reels. Der Mix aller Beiträge steht klein daneben.
|
|
const posts = (von, bis) => S.slots.filter((s) => s.datum >= von && s.datum <= bis);
|
|
const reels = (liste) => liste.filter((s) => art(s) === "reel");
|
|
const zeilen = [
|
|
...p.phasen.map((ph) => [`Phase ${ph.nr}`, posts(ph.von, ph.bis)]),
|
|
["Gesamt", S.slots],
|
|
];
|
|
return `<section class="block funnel-block">
|
|
<h2>Funnel der Reels <span class="neben">${esc(p.funnel.ziel_text)}</span></h2>
|
|
${zeilen.map(([name, liste]) => {
|
|
const m = funnelMix(reels(liste), p);
|
|
const alle = funnelMix(liste, p);
|
|
return `<div class="fzeile ${m.top < ziel[0] || m.top > ziel[1] ? "daneben" : ""}">
|
|
<span class="fname">${esc(name)}</span>${funnelBalken(m, ziel)}
|
|
<span class="fwerte"><b>Top ${m.top} %</b>, Middle ${m.middle} %, Bottom ${m.bottom} %
|
|
<span class="klein" style="display:block">alle Beiträge: Top ${alle.top} %, Middle ${alle.middle} %, Bottom ${alle.bottom} %</span></span></div>`;
|
|
}).join("")}
|
|
<div class="legende" style="margin-top:8px">${p.funnel.stufen.map((f) => `<span><span class="fu fu-${f.id}">${esc(f.name)}</span> ${esc(f.kurz || "")}</span>`).join("")}</div>
|
|
</section>`;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Export
|
|
|
|
// Was Noah immer wieder will: der Plan für einen Zeitraum, ohne Codes,
|
|
// interne Termine und Säulen. Der Server baut die Datei und legt sie in
|
|
// den Downloads-Ordner.
|
|
const ausfuhr = { von: null, bis: null, format: "pdf", ideen: false, ergebnis: null, laeuft: false };
|
|
|
|
function vorlagen(h) {
|
|
const m = montag(h);
|
|
const posts = S.slots.filter((s) => !s.kanaele.includes("intern"));
|
|
return [
|
|
["Diese Woche", m, plus(m, 6)],
|
|
["Nächste Woche", plus(m, 7), plus(m, 13)],
|
|
["Nächste 2 Wochen", plus(m, 7), plus(m, 20)],
|
|
["Nächste 4 Wochen", plus(m, 7), plus(m, 34)],
|
|
["Ganzer Plan", posts[0]?.datum || m, S.plan.phasen.at(-1).bis],
|
|
];
|
|
}
|
|
|
|
function imZeitraum(von, bis) {
|
|
return S.slots.filter((s) => !s.kanaele.includes("intern") && s.status !== "ausgefallen" && (s.bis || s.datum) >= von && s.datum <= bis);
|
|
}
|
|
|
|
function exportSchublade(h) {
|
|
const v = vorlagen(h);
|
|
if (!ausfuhr.von) [, ausfuhr.von, ausfuhr.bis] = v[2];
|
|
const gueltig = ausfuhr.von && ausfuhr.bis && ausfuhr.von <= ausfuhr.bis;
|
|
const drin = gueltig ? imZeitraum(ausfuhr.von, ausfuhr.bis) : [];
|
|
const wochen = [...new Set(drin.map((s) => kw(s.datum)))];
|
|
const el = schublade(`
|
|
<div class="ueber">Redaktionsplan</div>
|
|
<h2>Exportieren</h2>
|
|
<div class="unter">Für Noah: welcher Content wann auf welchen Kanälen kommt. Ohne Codes, interne Termine und Säulen.</div>
|
|
<dl class="felder">
|
|
<dt>Zeitraum</dt><dd>
|
|
<div class="schalter">${v.map(([name, a, b], i) => `<button data-vorlage="${i}" class="${ausfuhr.von === a && ausfuhr.bis === b ? "an" : ""}">${esc(name)}</button>`).join("")}</div>
|
|
<div class="datumfelder">
|
|
<label>von <input type="date" id="ex-von" value="${esc(ausfuhr.von)}"></label>
|
|
<label>bis <input type="date" id="ex-bis" value="${esc(ausfuhr.bis)}"></label>
|
|
</div>
|
|
</dd>
|
|
<dt>Enthält</dt><dd>${!gueltig ? `<span style="color:var(--rot)">Das Ende liegt vor dem Anfang.</span>`
|
|
: drin.length ? `<b>${drin.length} ${drin.length === 1 ? "Beitrag" : "Beiträge"}</b><span class="klein">${wochen.length > 1 ? `KW ${wochen[0]} bis KW ${wochen.at(-1)}` : `KW ${wochen[0]}`}, ${kurzDatum(drin[0].datum)} bis ${kurzDatum(drin.at(-1).datum)}</span>`
|
|
: `<span class="muted">Kein Beitrag in diesem Zeitraum.</span>`}</dd>
|
|
<dt>Format</dt><dd><div class="schalter">${[["pdf", "PDF"], ["html", "HTML"], ["md", "Markdown"]].map(([w, t]) =>
|
|
`<button data-format="${w}" class="${ausfuhr.format === w ? "an" : ""}">${t}</button>`).join("")}</div></dd>
|
|
<dt>Dazu</dt><dd><label class="kreuz"><input type="checkbox" id="ex-ideen" ${ausfuhr.ideen ? "checked" : ""}> Weitere Ideen zum Thema anhängen</label>
|
|
<span class="klein">${S.plan.ideen?.length || 0} Ideen aus dem Pool, mit Funnel</span></dd>
|
|
</dl>
|
|
<div class="ausfuehren">
|
|
<button class="knopf voll" id="ex-los" ${!gueltig || ausfuhr.laeuft ? "disabled" : ""}>${ausfuhr.laeuft ? "Wird gebaut" : "Exportieren"}</button>
|
|
<span class="klein">Landet in deinem Downloads-Ordner.</span>
|
|
</div>
|
|
${ausfuhr.ergebnis ? `<div class="ergebnis">
|
|
<div><b>Gespeichert</b><span class="klein">${esc(ausfuhr.ergebnis.name)}</span></div>
|
|
<div class="ergebnis-knoepfe"><button class="knopf" data-oeffnen>Öffnen</button><button class="knopf" data-zeigen>Im Finder zeigen</button></div>
|
|
</div>` : ""}
|
|
`, { zurueck: "#/plan" });
|
|
|
|
const neu = () => { ausfuhr.ergebnis = null; exportSchublade(h); };
|
|
el.querySelectorAll("[data-vorlage]").forEach((b) => {
|
|
b.onclick = () => { [, ausfuhr.von, ausfuhr.bis] = v[Number(b.dataset.vorlage)]; neu(); };
|
|
});
|
|
el.querySelector("#ex-von").onchange = (e) => { ausfuhr.von = e.target.value; neu(); };
|
|
el.querySelector("#ex-bis").onchange = (e) => { ausfuhr.bis = e.target.value; neu(); };
|
|
el.querySelectorAll("[data-format]").forEach((b) => {
|
|
b.onclick = () => { ausfuhr.format = b.dataset.format; neu(); };
|
|
});
|
|
el.querySelector("#ex-ideen").onchange = (e) => { ausfuhr.ideen = e.target.checked; neu(); };
|
|
el.querySelector("#ex-los").onclick = async () => {
|
|
ausfuhr.laeuft = true;
|
|
ausfuhr.ergebnis = null;
|
|
exportSchublade(h);
|
|
try {
|
|
const r = await fetch("/api/export", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ von: ausfuhr.von, bis: ausfuhr.bis, format: ausfuhr.format, ideen: ausfuhr.ideen }),
|
|
});
|
|
const j = await r.json();
|
|
if (!r.ok || !j.ok) throw new Error(j.fehler || `Fehler ${r.status}`);
|
|
ausfuhr.ergebnis = j;
|
|
meldung("Export gespeichert");
|
|
} catch (e) {
|
|
meldung(`Export fehlgeschlagen: ${e.message}`, true);
|
|
}
|
|
ausfuhr.laeuft = false;
|
|
if (location.hash === "#/plan/export") exportSchublade(h);
|
|
};
|
|
const oeffnen = (zeigen) => fetch("/api/oeffnen", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ pfad: ausfuhr.ergebnis.pfad, zeigen }),
|
|
});
|
|
el.querySelector("[data-oeffnen]")?.addEventListener("click", () => oeffnen(false));
|
|
el.querySelector("[data-zeigen]")?.addEventListener("click", () => oeffnen(true));
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Seite
|
|
|
|
export function zeichnen(main, r, { erstesMal } = {}) {
|
|
const h = heute();
|
|
const p = S.plan;
|
|
const erster = S.slots.find((s) => !s.kanaele.includes("intern"));
|
|
const ende = p.phasen.at(-1).bis;
|
|
const anzahl = S.slots.filter((s) => !s.kanaele.includes("intern")).length;
|
|
|
|
|
|
main.innerHTML = `
|
|
<header class="seitenkopf">
|
|
<div><h1>Redaktionsplan</h1>
|
|
<div class="unter">${erster ? ttmm(erster.datum) : ""} bis ${ttmm(ende)}${ende.slice(0, 4)}, ${anzahl} Beiträge. ${esc(p.status)} vom ${ttmm(p.stand)}${p.stand.slice(0, 4)}.</div>
|
|
<div class="thema">Thema: <b>${esc(p.thema.name)}</b></div></div>
|
|
<div style="display:flex;gap:12px;align-items:center">
|
|
<div class="schalter">
|
|
<button data-ansicht="kalender" class="${zustand.ansicht === "kalender" ? "an" : ""}">Kalender</button>
|
|
<button data-ansicht="liste" class="${zustand.ansicht === "liste" ? "an" : ""}">Liste</button>
|
|
</div>
|
|
<a class="knopf voll" href="#/plan/export">Exportieren</a>
|
|
</div>
|
|
</header>
|
|
<div class="schalterleiste">
|
|
<label class="auswahl">Kanal <select data-wahl="kanal">${KANAL_FILTER.map(([w, t]) => `<option value="${esc(w)}" ${zustand.kanal === w ? "selected" : ""}>${esc(t)}</option>`).join("")}</select></label>
|
|
<label class="auswahl">Blickwinkel <select data-wahl="saeule"><option value="alle">Alle</option>${p.saeulen.map((s) => `<option value="${esc(s.kuerzel)}" ${zustand.saeule === s.kuerzel ? "selected" : ""}>${esc(s.name)}</option>`).join("")}</select></label>
|
|
<label class="auswahl">Funnel <select data-wahl="funnel"><option value="alle">Alle</option>${p.funnel.stufen.map((f) => `<option value="${f.id}" ${zustand.funnel === f.id ? "selected" : ""}>${esc(f.name)}</option>`).join("")}</select></label>
|
|
${zustand.ansicht === "kalender" ? `<label class="kreuz"><input type="checkbox" data-roadmap ${zustand.roadmap ? "checked" : ""}> Roadmap-Termine</label>` : ""}
|
|
<div class="legende" style="margin-left:auto">
|
|
${["carousel", "reel", "linkedin", "story"].map((a) => `<span><span class="art ${a}"></span>${ART_NAME[a]}</span>`).join("")}
|
|
</div>
|
|
</div>
|
|
${funnelBlock()}
|
|
${zustand.ansicht === "kalender" ? kalender(h) : liste(h)}
|
|
|
|
<section class="block" id="flex"><h2>Flex-Slots <span class="neben">hängen von anderen ab und erscheinen, wenn sie fertig sind</span></h2>
|
|
<div class="tab"><table class="daten"><thead><tr><th>Flex-Slot</th><th>Von wem</th><th>Wenn er kommt</th></tr></thead><tbody>
|
|
${p.flex.map((f) => `<tr><td><b>${esc(f.name)}</b>${f.erschienen?.length ? `<span class="klein" style="display:block">bisher ${f.erschienen.map(ttmm).join(", ")}</span>` : ""}</td>
|
|
<td>${esc(f.von)}</td><td>${mdInline(f.dann)}</td></tr>`).join("")}
|
|
</tbody></table></div></section>
|
|
|
|
<section class="block" id="tests"><h2>Entscheidungen und Tests</h2>
|
|
<div class="tab"><table class="daten"><thead><tr><th>Wann</th><th>Test</th><th>Frage</th><th>Grundlage</th><th>Ergebnis</th></tr></thead><tbody>
|
|
${p.tests.map((t) => `<tr class="${t.termine.length && t.termine.at(-1) < h ? "vorbei" : ""}">
|
|
<td class="datum">${t.termine.length ? t.termine.map((d, i) => (i ? ttmm(d) : kurzDatum(d))).join(", ") : esc(t.wann || "offen")}</td>
|
|
<td><b>${t.id.startsWith("T") ? esc(t.id) + " " : ""}${esc(t.titel)}</b></td>
|
|
<td>${esc(t.frage)}</td><td class="klein">${esc(t.grundlage)}</td><td>${t.ergebnis ? mdInline(t.ergebnis) : `<span class="muted">offen</span>`}</td></tr>`).join("")}
|
|
</tbody></table></div></section>
|
|
`;
|
|
|
|
main.querySelectorAll("[data-ansicht]").forEach((b) => {
|
|
b.onclick = () => { zustand.ansicht = b.dataset.ansicht; merken(); zeichnen(main, r); };
|
|
});
|
|
main.querySelectorAll("select[data-wahl]").forEach((sel) => {
|
|
sel.onchange = () => { zustand[sel.dataset.wahl] = sel.value; merken(); zeichnen(main, r); };
|
|
});
|
|
const rm = main.querySelector("[data-roadmap]");
|
|
if (rm) rm.onchange = () => { zustand.roadmap = rm.checked; merken(); zeichnen(main, r); };
|
|
main.querySelectorAll("[data-slot]").forEach((el) => {
|
|
el.onclick = () => { location.hash = `#/plan/${el.dataset.slot}`; };
|
|
});
|
|
|
|
if (r.rest[0] === "export") exportSchublade(h);
|
|
const slot = r.rest[0] && S.slots.find((s) => s.id === r.rest[0]);
|
|
if (slot) {
|
|
main.querySelectorAll(`[data-slot="${slot.id}"]`).forEach((el) => el.classList.add("ausgewaehlt"));
|
|
slotSchublade(slot, h);
|
|
}
|
|
|
|
// Beim ersten Öffnen zur laufenden Woche springen, wenn sie weiter unten liegt.
|
|
if (erstesMal) {
|
|
const teil = r.query.get("teil");
|
|
const ziel = teil ? main.querySelector(`#${CSS.escape(teil)}`) : !slot && zustand.ansicht === "kalender" ? main.querySelector(".kal-woche.diese") : null;
|
|
if (ziel && (teil || ziel.getBoundingClientRect().top > window.innerHeight * 0.6)) {
|
|
requestAnimationFrame(() => ziel.scrollIntoView({ block: teil ? "start" : "center" }));
|
|
}
|
|
}
|
|
}
|