From 12cc297a4116ed5d133d35a89bc9217f10d7f89b Mon Sep 17 00:00:00 2001 From: DeeCee03 Date: Thu, 12 Nov 2026 22:40:04 +0100 Subject: [PATCH] =?UTF-8?q?E-Mail/Kontakte:=20zwei=20Tabellen,=20Kaltliste?= =?UTF-8?q?=20und=20=C3=BCbrige=20Leads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alle Leads statt der ersten 200, eine Suche für beide Tabellen, Kopfzeile bleibt beim Scrollen stehen. Tabellen weiter nur lokal. Co-Authored-By: Claude Opus 5.5 --- Dashboard/seiten/email.js | 67 +++++++++++++++++++++------------------ Dashboard/stil.css | 2 ++ 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/Dashboard/seiten/email.js b/Dashboard/seiten/email.js index 94450aa..7676536 100644 --- a/Dashboard/seiten/email.js +++ b/Dashboard/seiten/email.js @@ -14,8 +14,8 @@ const LISTEN = "/.docs/leads/"; const zahl = (n) => n.toLocaleString("de-DE"); let leads = null; // daten/leads.json -const tabellen = new Map(); // Dateiname → Zeilen der CSV (nur lokal) -const zustand = { liste: 0, suche: "", herkunft: "alle" }; +const tabellen_ = new Map(); // Dateiname → Zeilen der CSV (nur lokal) +const zustand = { suche: "" }; export async function zeichnen(main) { const reiter = route().rest[0] === "kontakte" ? "kontakte" : "mediakit"; @@ -72,6 +72,9 @@ async function kontakte(ziel) {

${zahl(kalt)} von ${zahl(alle.kontakte)} Kontakten stammen aus der Kaltliste (Google-Maps-Scrape). Ohne Einwilligung verbietet Mailjet den Import, und Mails wären nach § 7 UWG abmahnbar. Importieren erst nach der Entscheidung in Aufgabe 1.1, siehe Planung.

+
Tabellen laden …
+ +

Auswertung

${kachel(alle.kontakte, "Kontakte mit Mailadresse", alle.datei)} ${kachel(kalt, "davon aus der Kaltliste", `${Math.round((kalt / alle.kontakte) * 100)} %`)} @@ -88,10 +91,9 @@ async function kontakte(ziel) { ${leads.listen.map((l) => `
${esc(l.datei)}${zahl(l.kontakte)} Kontakte · Spalten: ${esc(l.spalten.join(", "))}
`).join("")}

Die Dateien liegen in Dejans Downloads unter Mailjet-Import/ und im Hub in .docs/leads/, nicht im Repo. Import in Mailjet: Contacts → Import contacts, Spalte email ist Pflicht, vorname der Eigenschaft vorname zuordnen.

+`; -

Liste

Lädt …
`; - - await liste(ziel.querySelector("#leads-liste")); + await tabellen(ziel.querySelector("#leads-tabellen")); } function kachel(wert, text, klein) { @@ -140,44 +142,49 @@ function csvLesen(text) { return rest.map((z) => Object.fromEntries(kopf.map((k, i) => [k, z[i] ?? ""]))); } -async function liste(block) { - const datei = leads.listen[zustand.liste]?.datei || leads.listen[0].datei; - if (!tabellen.has(datei)) { +// Zwei Tabellen aus der Datei mit allen Leads: Kaltliste und alle übrigen. +// Eine Suche oben gilt für beide. +const SPALTEN = ["email", "vorname", "nachname", "betrieb", "ort", "bundesland", "groesse", "erp", "status"]; + +async function tabellen(block) { + const datei = (leads.listen.find((l) => /alle/i.test(l.datei)) || leads.listen[0]).datei; + if (!tabellen_.has(datei)) { try { const r = await fetch(LISTEN + encodeURIComponent(datei), { cache: "no-store" }); if (!r.ok) throw new Error(r.status); - tabellen.set(datei, csvLesen(await r.text())); + tabellen_.set(datei, csvLesen(await r.text())); } catch { - block.innerHTML = `

Liste

Die Liste mit Namen und Adressen gibt es nur lokal auf Dejans Rechner. Hier stehen nur die Zahlen oben.

`; + block.innerHTML = `

Leads

Die Tabellen mit Namen und Adressen erscheinen nur im lokalen Dashboard auf Dejans Rechner: im Hub-Ordner ./serve.sh starten und dort E-Mail → Kontakte öffnen. In der Freigabe für Kollegen stehen bewusst nur die Zahlen.

`; return; } } - const zeilen = tabellen.get(datei); - const herkuenfte = ["alle", ...new Set(zeilen.map((z) => z.herkunft))]; + const zeilen = tabellen_.get(datei); const s = zustand.suche.toLowerCase(); - const treffer = zeilen.filter((z) => (zustand.herkunft === "alle" || z.herkunft === zustand.herkunft) - && (!s || Object.values(z).some((w) => w.toLowerCase().includes(s)))); - const zeigen = treffer.slice(0, 200); - const spalten = ["email", "vorname", "nachname", "betrieb", "ort", "bundesland", "groesse", "herkunft", "status"]; + const passt = (z) => !s || Object.values(z).some((w) => w.toLowerCase().includes(s)); + const kalt = zeilen.filter((z) => z.herkunft === "Kaltliste"); + const rest = zeilen.filter((z) => z.herkunft !== "Kaltliste"); + const tabelle = (titel, unter, liste, spalten) => { + const treffer = liste.filter(passt); + return `

${esc(titel)} ${s ? `${zahl(treffer.length)} von ` : ""}${zahl(liste.length)} Leads · ${esc(unter)}

+ ${treffer.length ? `
${spalten.map((k) => ``).join("")} + ${treffer.map((z, i) => `${spalten.map((k) => ``).join("")}`).join("")}
#${esc(k)}
${i + 1}${esc(z[k])}
` + : `
Keine Treffer.
`} +
`; + }; - block.innerHTML = `

Liste nur lokal sichtbar

-
-
${leads.listen.map((l, i) => ``).join("")}
- - -
-

${zahl(treffer.length)} Treffer${treffer.length > zeigen.length ? `, die ersten ${zeigen.length} stehen hier` : ""}.

-
${spalten.map((k) => ``).join("")} - ${zeigen.map((z) => `${spalten.map((k) => ``).join("")}`).join("")}
${esc(k)}
${esc(z[k])}
`; + block.innerHTML = ` +
+ ${tabelle("Kaltliste", "Google-Maps-Scrape, ohne Einwilligung", kalt, SPALTEN)} + ${tabelle("Übrige Leads", "Verband, Partner und andere Herkunft", rest, [...SPALTEN.slice(0, 5), "herkunft", ...SPALTEN.slice(5)])}`; - block.querySelectorAll("[data-liste]").forEach((b) => { - b.onclick = () => { zustand.liste = Number(b.dataset.liste); zustand.herkunft = "alle"; liste(block); }; - }); - block.querySelector("#leads-herkunft").onchange = (e) => { zustand.herkunft = e.target.value; liste(block); }; const suche = block.querySelector("#leads-suche"); suche.oninput = () => { zustand.suche = suche.value; clearTimeout(suche._t); - suche._t = setTimeout(async () => { await liste(block); const neu = block.querySelector("#leads-suche"); neu.focus(); neu.setSelectionRange(neu.value.length, neu.value.length); }, 200); + suche._t = setTimeout(async () => { + await tabellen(block); + const neu = block.querySelector("#leads-suche"); + neu.focus(); neu.setSelectionRange(neu.value.length, neu.value.length); + }, 250); }; } diff --git a/Dashboard/stil.css b/Dashboard/stil.css index 33bfcaa..7ebd551 100644 --- a/Dashboard/stil.css +++ b/Dashboard/stil.css @@ -332,6 +332,8 @@ h2.abschnitt .klein { font-weight: 500; font-size: 14px; margin-left: 10px; } .leads-suche { flex: 1 1 220px; border: 1px solid var(--rand); border-radius: 4px; padding: 7px 10px; font: inherit; font-size: 14px; background: var(--flaeche); } .leads-tabelle { overflow-x: auto; max-height: 70vh; overflow-y: auto; } .leads-tabelle table.daten td { font-size: 13.5px; white-space: nowrap; } +.leads-tabelle thead th { position: sticky; top: 0; z-index: 1; background: var(--flaeche); } +.leads-tabelle table.daten { min-width: 100%; } @media (max-width: 1100px) { .kacheln { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 700px) { .bl-zeile { grid-template-columns: minmax(0, 110px) minmax(0, 1fr) 96px; } }