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

160 lines
8.0 KiB
JavaScript

// Kleine Helfer für Datum, Text, Markdown und Kanäle.
// Alles mit Datum rechnet mit reinen Kalendertagen in UTC, damit die
// Sommerzeit keinen Tag verschiebt.
export const WT = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
export const WT_LANG = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
export const MONATE = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"];
export const MON_KURZ = ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"];
export const tag = (iso) => new Date(iso + "T00:00:00Z");
export const isoVon = (d) => d.toISOString().slice(0, 10);
export const plus = (iso, n) => { const d = tag(iso); d.setUTCDate(d.getUTCDate() + n); return isoVon(d); };
export const ttmm = (iso) => `${iso.slice(8, 10)}.${iso.slice(5, 7)}.`;
export const wt = (iso) => WT[tag(iso).getUTCDay()];
export const kurzDatum = (iso) => `${wt(iso)} ${ttmm(iso)}`;
export const langDatum = (iso) => {
const d = tag(iso);
return `${WT_LANG[d.getUTCDay()]}, ${d.getUTCDate()}. ${MONATE[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
};
export const tageZwischen = (a, b) => Math.round((tag(b) - tag(a)) / 86400000);
export const montag = (iso) => plus(iso, -((tag(iso).getUTCDay() + 6) % 7));
export function kw(iso) {
const d = tag(iso);
d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 6) % 7) + 3);
const erster = new Date(Date.UTC(d.getUTCFullYear(), 0, 4));
return 1 + Math.round(((d - erster) / 86400000 - 3 + ((erster.getUTCDay() + 6) % 7)) / 7);
}
// "12. bis 18.10." im selben Monat, sonst "26.10. bis 01.11."
export function spanne(von, bis) {
if (von.slice(0, 7) === bis.slice(0, 7)) return `${von.slice(8, 10)}. bis ${ttmm(bis)}`;
return `${ttmm(von)} bis ${ttmm(bis)}`;
}
// Heute, oder zum Ausprobieren ein festes Datum über ?heute=2026-10-21.
export function heute() {
const q = new URLSearchParams(location.search).get("heute");
if (q && /^\d{4}-\d{2}-\d{2}$/.test(q)) return q;
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
export function abstand(von, bis) {
const n = tageZwischen(von, bis);
if (n === 0) return "heute";
if (n === 1) return "morgen";
if (n === -1) return "gestern";
return n > 0 ? `in ${n} Tagen` : `vor ${-n} Tagen`;
}
// Zeitstempel vom Server (Sekunden) als "05.10., 20:41"
export function zeitstempel(sek) {
if (!sek) return "";
const d = new Date(sek * 1000);
const zz = (n) => String(n).padStart(2, "0");
return `${zz(d.getDate())}.${zz(d.getMonth() + 1)}., ${zz(d.getHours())}:${zz(d.getMinutes())}`;
}
// ---------------------------------------------------------------- Text
export const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
export const mdInline = (s) => window.marked.parseInline(String(s ?? ""));
export const md = (s) => window.marked.parse(String(s ?? ""), { gfm: true, breaks: true });
export const ohneMd = (s) => String(s ?? "").replace(/\*\*|`/g, "");
export function slug(text) {
return String(text).toLowerCase()
.replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss")
.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "abschnitt";
}
export const dokLink = (pfad, extra = "") =>
`#/dok/${pfad.split("/").map(encodeURIComponent).join("/")}${extra}`;
// ---------------------------------------------------------------- Formate und Kanäle
const FORMAT_CODE = /\b(F\d{1,2}b?|CH|L[1-4])\b/g;
export const codes = (s) => [...new Set(String(s ?? "").match(FORMAT_CODE) || [])];
export const KANAL_KURZ = {
"Instagram": "IG", "Facebook": "FB", "TikTok": "TT", "YouTube Shorts": "YT",
"LinkedIn": "LI", "Instagram Stories": "ST", "intern": "IN",
};
export const KANAL_FILTER = [
["alle", "Alle"], ["Instagram", "Instagram"], ["LinkedIn", "LinkedIn"], ["TikTok", "TikTok"],
["Facebook", "Facebook"], ["YouTube Shorts", "YouTube Shorts"], ["Instagram Stories", "Stories"], ["intern", "intern"],
];
// Kanäle als graue Kürzel, der volle Name steht im Tooltip.
export function kanalBadges(kanaele) {
return `<span class="kn" title="${esc(kanaele.join(", "))}">${kanaele.map((k) => KANAL_KURZ[k] || esc(k)).join(" ")}</span>`;
}
// Welche Art Beitrag ein Slot ist. Danach richtet sich die Farbe im Kalender.
export function art(slot) {
const k = slot.kanaele;
if (k.includes("intern")) return "intern";
if (k.includes("Instagram Stories")) return "story";
if (k.includes("LinkedIn")) return "linkedin";
if (k.includes("TikTok") || k.includes("YouTube Shorts")) return "reel";
return "carousel";
}
export const ART_NAME = { carousel: "Carousel", reel: "Reel", linkedin: "LinkedIn", story: "Story", intern: "intern" };
// Funnel-Stufe eines Slots: am Slot gesetzt, sonst die des ersten Formats.
export const FUNNEL_NAME = { top: "Top", middle: "Middle", bottom: "Bottom" };
export function funnel(s, plan) {
if (s.funnel) return s.funnel;
if (s.kanaele.includes("intern")) return null;
const code = codes(s.format)[0];
const f = plan.formate.find((x) => x.kuerzel === code) || plan.linkedin.find((x) => x.kuerzel === code);
return f?.funnel || null;
}
export function funnelMix(slots, plan) {
const z = { top: 0, middle: 0, bottom: 0 };
for (const s of slots) { const f = funnel(s, plan); if (f) z[f] += 1; }
const n = z.top + z.middle + z.bottom;
const pz = (k) => (n ? Math.round((z[k] / n) * 100) : 0);
return { n, top: pz("top"), middle: pz("middle"), bottom: pz("bottom") };
}
// Ein Balken mit den drei Anteilen und der Zielspanne für Top.
export function funnelBalken(m, ziel) {
return `<span class="fbalken" title="Top ${m.top} %, Middle ${m.middle} %, Bottom ${m.bottom} %">
<span class="fb-top" style="width:${m.top}%"></span><span class="fb-middle" style="width:${m.middle}%"></span><span class="fb-bottom" style="width:${m.bottom}%"></span>
${ziel ? `<span class="fb-ziel" style="left:${ziel[0]}%;width:${ziel[1] - ziel[0]}%"></span>` : ""}
</span>`;
}
export const SLOT_STATUS = ["geplant", "fertig", "erschienen", "ausgefallen"];
export const SCHRITT_STATUS = ["offen", "läuft", "erledigt", "später", "entfällt"];
// Status als Text mit einem kleinen Quadrat davor (Farbe und Füllung im Stylesheet).
export const pille = (status, text) => `<span class="st st-${esc(status)}">${esc(text ?? status)}</span>`;
// "Dejan, auf Anfrage von Noah" ist eine Aufgabe von Dejan, nicht von Noah.
export const zustaendig = (schritt, name) =>
new RegExp(`\\b${name}\\b`).test((schritt.wer || "").replace(/auf Anfrage von \S+/g, ""));
export const istOffen = (s) => s.status === "offen" || s.status === "läuft";
// Kleine Liniengrafik ohne Bibliothek: eine Linie, der letzte Wert markiert.
export function verlaufSvg(werte, breite = 320, hoehe = 70) {
if (werte.length < 2) return "";
const xs = werte.map((w) => tag(w.datum).getTime());
const ys = werte.map((w) => w.wert);
const [x0, x1] = [Math.min(...xs), Math.max(...xs)];
const y1 = Math.max(...ys) * 1.1;
const px = (x) => 3 + ((x - x0) / (x1 - x0 || 1)) * (breite - 6);
const py = (y) => hoehe - 3 - (y / (y1 || 1)) * (hoehe - 6);
const punkte = werte.map((w, i) => `${px(xs[i]).toFixed(1)},${py(ys[i]).toFixed(1)}`).join(" ");
const [lx, ly] = [px(xs.at(-1)), py(ys.at(-1))];
return `<svg class="verlauf" viewBox="0 0 ${breite} ${hoehe}" role="img" aria-label="Verlauf">
<line x1="0" y1="${hoehe - 0.5}" x2="${breite}" y2="${hoehe - 0.5}" stroke="#E3E3DE" stroke-width="1" vector-effect="non-scaling-stroke"/>
<polyline points="${punkte}" fill="none" stroke="#151515" stroke-width="1.5" stroke-linejoin="round" vector-effect="non-scaling-stroke"/>
${werte.map((w, i) => `<circle cx="${px(xs[i]).toFixed(1)}" cy="${py(ys[i]).toFixed(1)}" r="6" fill="transparent"><title>${ttmm(w.datum)}: ${w.wert}</title></circle>`).join("")}
<rect x="${(lx - 3).toFixed(1)}" y="${(ly - 3).toFixed(1)}" width="6" height="6" fill="#F1B729"/>
</svg>`;
}