Files
HiveGrid-Marketing/Dashboard/seiten/formate.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

140 lines
8.3 KiB
JavaScript

// Formate: Übersicht aus der Formatbibliothek, mit Nutzung im Redaktionsplan.
import { S, schublade } from "../app.js";
import { heute, kurzDatum, esc, md, mdInline, ohneMd, codes, dokLink, pille, FUNNEL_NAME } from "../hilfen.js";
const QUELLE = "Ressourcen/Formatbibliothek.md";
let cache = { zeit: null, daten: null };
// Liest die Formatbibliothek: die Übersichtstabelle und die Abschnitte je Format.
export async function formatbibliothek() {
const zeit = S.stand?.dokumente;
if (cache.daten && cache.zeit === zeit) return cache.daten;
const text = await fetch("/" + QUELLE.split("/").map(encodeURIComponent).join("/"), { cache: "no-store" }).then((r) => r.text());
const abschnitte = {};
const teile = text.split(/^## /m);
const vorspann = teile.shift();
for (const t of teile) {
const kopf = t.slice(0, t.indexOf("\n"));
const rumpf = t.slice(t.indexOf("\n") + 1);
const m = kopf.match(/^(F\d{1,2}b?)\s/);
abschnitte[m ? m[1] : kopf.trim()] = { titel: kopf.trim(), rumpf };
}
const uebersicht = [];
const tabelle = (abschnitte["Übersicht"]?.rumpf || "").split("\n").filter((z) => /^\|\s*F\d/.test(z));
for (const z of tabelle) {
const [code, name, herkunft, aufwand, konto, laenge, start] = z.split("|").slice(1, -1).map((x) => x.trim());
uebersicht.push({ code, name, herkunft, aufwand, konto, laenge, start });
}
cache = { zeit, daten: { vorspann, abschnitte, uebersicht } };
return cache.daten;
}
function nutzung(code) {
return S.slots.filter((s) => codes(s.format).includes(code) && s.status !== "ausgefallen");
}
// Wie das Format für Noah heißt, welche Funnel-Stufe es hat und wie das Thema darin vorkommt.
const planFormat = (code) => S.plan.formate.find((f) => f.kuerzel === code) || S.plan.linkedin.find((l) => l.kuerzel === code);
const noahName = (code) => planFormat(code)?.noah_name || "";
const funnelZelle = (code) => (planFormat(code)?.funnel ? `<span class="fu fu-${planFormat(code).funnel}">${FUNNEL_NAME[planFormat(code).funnel]}</span>` : "");
function imPlan(code, h) {
const im = nutzung(code);
const naechst = im.find((s) => s.datum >= h);
if (!im.length) return `<span class="muted">nicht im Plan</span>`;
return `${im.length} Mal${naechst ? `<span class="klein" style="display:block;white-space:nowrap">ab ${kurzDatum(naechst.datum)}</span>` : ""}`;
}
export async function zeichnen(main, r) {
const h = heute();
const fb = await formatbibliothek();
const liste = [...fb.uebersicht];
// CH steht nur im Redaktionsplan, nicht in der Formatbibliothek.
for (const f of S.plan.formate) {
if (!liste.some((x) => x.code === f.kuerzel)) {
liste.push({ code: f.kuerzel, name: f.name, aufwand: f.aufwand === "mittel" ? "2" : f.aufwand === "leicht" ? "1" : "3", konto: "HiveGrid", start: "laut Redaktionsplan" });
}
}
const allgemein = fb.abschnitte["Was für alle Reel-Formate gilt"];
const imPlanListe = liste.filter((f) => nutzung(f.code).length);
const rest = liste.filter((f) => !nutzung(f.code).length);
const aufwandText = (a) => ({ "1": "leicht", "2": "mittel", "3": "hoch", "1 bis 2": "leicht bis mittel" }[String(a)] || String(a));
const zeile = (f, mitPlan) => `<tr class="klickbar" data-format="${esc(f.code)}">
<td style="width:56px" class="code">${esc(f.code)}</td>
<td><b>${esc(f.name)}</b>${noahName(f.code) ? `<span class="klein" style="display:block">Für Noah: ${esc(noahName(f.code))}</span>` : ""}</td>
<td>${funnelZelle(f.code)}</td><td>${esc(aufwandText(f.aufwand))}</td><td>${esc(f.konto || "")}</td>
${mitPlan ? `<td style="white-space:nowrap">${imPlan(f.code, h)}</td>` : `<td>${esc(f.start || "")}</td>`}
</tr>`;
main.innerHTML = `
<header class="seitenkopf">
<div><h1>Formate</h1><div class="unter">Ein Format ist eine wiederholbare Bauform. Alle Formate tragen dasselbe Thema.</div>
<div class="thema">Thema: <b>${esc(S.plan.thema.name)}</b></div></div>
<a class="knopf" href="${dokLink(QUELLE)}">Formatbibliothek lesen</a>
</header>
<section class="block"><h2>Im Plan <span class="neben">${imPlanListe.length} Formate, zum Öffnen anklicken</span></h2>
<div class="tab"><table class="daten">
<thead><tr><th></th><th>Format</th><th>Funnel</th><th>Aufwand</th><th>Konto</th><th>Im Plan</th></tr></thead>
<tbody>${imPlanListe.map((f) => zeile(f, true)).join("")}</tbody></table></div>
</section>
<section class="block">
<details><summary>${rest.length} weitere Formate, noch nicht im Plan</summary>
<div class="tab" style="margin-top:10px"><table class="daten">
<thead><tr><th></th><th>Format</th><th>Funnel</th><th>Aufwand</th><th>Konto</th><th>Start</th></tr></thead>
<tbody>${rest.map((f) => zeile(f, false)).join("")}</tbody></table></div>
</details>
</section>
${S.plan.ideen?.length ? `<section class="block"><h2>Ideen-Pool zum Thema <span class="neben">nicht fest im Plan, für Ausfälle und nach T5</span></h2>
<div class="tab"><table class="daten">
<thead><tr><th>Idee</th><th>Funnel</th><th>Aufwand</th><th>Baut auf</th></tr></thead>
<tbody>${S.plan.ideen.map((i) => `<tr>
<td><b>${esc(i.name)}</b><span class="klein" style="display:block">${esc(i.text)}</span></td>
<td><span class="fu fu-${esc(i.funnel)}">${FUNNEL_NAME[i.funnel] || ""}</span></td>
<td>${esc(i.aufwand)}</td>
<td class="klein">${esc([i.aus === "neu" ? "neu" : i.aus, i.vorbild].filter(Boolean).join(", "))}</td></tr>`).join("")}</tbody></table></div>
</section>` : ""}
<div class="raster r-2">
<section class="block"><h2>LinkedIn-Postarten <span class="neben">Text und Bild</span></h2>
${S.plan.linkedin.map((l) => `<div class="zeile" style="grid-template-columns:40px 1fr auto">
<span class="code">${esc(l.kuerzel)}</span><span><b>${esc(l.name)}</b>
<details><summary>Was dahinter steckt</summary><span class="klein" style="display:block;margin-top:6px">${mdInline(l.text)}</span></details></span>
<span class="klein" style="white-space:nowrap">${nutzung(l.kuerzel).length} Mal</span></div>`).join("")}
</section>
<section class="block"><h2>Blickwinkel <span class="neben">auf das eine Thema</span></h2>
${S.plan.saeulen.map((s) => `<div class="zeile" style="grid-template-columns:40px 1fr auto">
<span class="code">${esc(s.kuerzel)}</span><span><b>${esc(s.name)}</b><span class="klein" style="display:block">${esc(s.text || "")}</span></span>
<span class="klein">${S.slots.filter((x) => x.saeule === s.kuerzel).length} Slots</span></div>`).join("")}
</section>
</div>
${allgemein ? `<section class="block"><details><summary>${esc(allgemein.titel)}</summary><div class="md" style="margin-top:10px">${md(allgemein.rumpf)}</div></details></section>` : ""}
<p class="klein">Woher die Formate kommen: <a href="${dokLink("Ressourcen/Reel-Analyse/analysen/2026-10-05-inspo-liste-dejan.md")}">Analyse der Inspo-Reels</a>.</p>
`;
main.querySelectorAll("[data-format]").forEach((tr) => {
tr.onclick = () => { location.hash = `#/formate/${encodeURIComponent(tr.dataset.format)}`; };
});
const code = r.rest[0];
if (!code) return;
const abschnitt = fb.abschnitte[code];
const eintrag = liste.find((f) => f.code === code);
const im = nutzung(code);
schublade(`
<div class="ueber">Format</div>
<h2>${esc(abschnitt ? abschnitt.titel : `${code} ${eintrag?.name || ""}`)}</h2>
<div class="unter">${eintrag ? [`Aufwand ${esc(eintrag.aufwand)}`, eintrag.konto && esc(eintrag.konto), eintrag.laenge && esc(eintrag.laenge)].filter(Boolean).join(", ") + "." : ""}</div>
<dl class="felder">
<dt>Im Plan</dt><dd>${im.length ? im.map((s) =>
`<a href="#/plan/${esc(s.id)}">${kurzDatum(s.datum)}, ${esc(ohneMd(s.format))}</a>${s.status !== "geplant" ? ` ${pille(s.status)}` : ""}`).join("<br>") : `<span class="muted">nicht im festen Plan</span>`}</dd>
${planFormat(code)?.funnel ? `<dt>Funnel</dt><dd>${funnelZelle(code)}</dd>` : ""}
${planFormat(code)?.thema ? `<dt>Im Thema</dt><dd>${esc(planFormat(code).thema)}</dd>` : ""}
${noahName(code) ? `<dt>Für Noah</dt><dd>${esc(noahName(code))}</dd>` : ""}
</dl>
<div class="md" style="margin-top:18px">${abschnitt ? md(abschnitt.rumpf) : `<p class="muted">In der Formatbibliothek gibt es dazu keinen eigenen Abschnitt.</p>`}</div>
`, { zurueck: "#/formate", breit: true });
}