E-Mail/Kontakte: zwei Tabellen, Kaltliste und übrige Leads
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
d8b1ed0979
commit
12cc297a41
2 files changed
+39
-30
No files matched your search
+37
-30
@@ -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) {
|
||||
<p style="margin:0">${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 <a href="${dokLink("webinar/mail-strecke/README.md", "?abschnitt=die-kalten-mails")}">Planung</a>.</p>
|
||||
</section>
|
||||
|
||||
<div id="leads-tabellen"><div class="leer">Tabellen laden …</div></div>
|
||||
|
||||
<h2 class="abschnitt">Auswertung</h2>
|
||||
<div class="kacheln">
|
||||
${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) => `<div class="zeile unterlage"><span><b>${esc(l.datei)}</b><span class="klein" style="display:block">${zahl(l.kontakte)} Kontakte · Spalten: ${esc(l.spalten.join(", "))}</span></span></div>`).join("")}
|
||||
<p class="klein" style="margin:10px 0 0">Die Dateien liegen in Dejans Downloads unter <code>Mailjet-Import/</code> und im Hub in <code>.docs/leads/</code>, nicht im Repo. Import in Mailjet: Contacts → Import contacts, Spalte <code>email</code> ist Pflicht, <code>vorname</code> der Eigenschaft <code>vorname</code> zuordnen.</p>
|
||||
</section>
|
||||
`;
|
||||
|
||||
<section class="block" id="leads-liste"><h2>Liste</h2><div class="leer">Lädt …</div></section>`;
|
||||
|
||||
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 = `<h2>Liste</h2><p class="klein" style="margin:0">Die Liste mit Namen und Adressen gibt es nur lokal auf Dejans Rechner. Hier stehen nur die Zahlen oben.</p>`;
|
||||
block.innerHTML = `<section class="block"><h2>Leads</h2><p style="margin:0">Die Tabellen mit Namen und Adressen erscheinen nur im <b>lokalen Dashboard</b> auf Dejans Rechner: im Hub-Ordner <code>./serve.sh</code> starten und dort <b>E-Mail → Kontakte</b> öffnen. In der Freigabe für Kollegen stehen bewusst nur die Zahlen.</p></section>`;
|
||||
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 `<section class="block"><h2>${esc(titel)} <span class="neben">${s ? `${zahl(treffer.length)} von ` : ""}${zahl(liste.length)} Leads · ${esc(unter)}</span></h2>
|
||||
${treffer.length ? `<div class="leads-tabelle"><table class="daten"><thead><tr><th>#</th>${spalten.map((k) => `<th>${esc(k)}</th>`).join("")}</tr></thead>
|
||||
<tbody>${treffer.map((z, i) => `<tr><td class="klein">${i + 1}</td>${spalten.map((k) => `<td>${esc(z[k])}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`
|
||||
: `<div class="leer">Keine Treffer.</div>`}
|
||||
</section>`;
|
||||
};
|
||||
|
||||
block.innerHTML = `<h2>Liste <span class="neben">nur lokal sichtbar</span></h2>
|
||||
<div class="schalterleiste" style="margin-bottom:10px">
|
||||
<div class="schalter">${leads.listen.map((l, i) => `<button class="${i === zustand.liste ? "an" : ""}" data-liste="${i}">${esc(l.datei.replace(/\.csv$/, "").replace("Leads Vertrieb ", ""))}</button>`).join("")}</div>
|
||||
<label class="auswahl">Herkunft <select id="leads-herkunft">${herkuenfte.map((h) => `<option ${h === zustand.herkunft ? "selected" : ""}>${esc(h)}</option>`).join("")}</select></label>
|
||||
<input id="leads-suche" class="leads-suche" type="search" placeholder="Suchen: Name, Betrieb, Ort …" value="${esc(zustand.suche)}">
|
||||
</div>
|
||||
<p class="klein" style="margin:0 0 8px">${zahl(treffer.length)} Treffer${treffer.length > zeigen.length ? `, die ersten ${zeigen.length} stehen hier` : ""}.</p>
|
||||
<div class="leads-tabelle"><table class="daten"><thead><tr>${spalten.map((k) => `<th>${esc(k)}</th>`).join("")}</tr></thead>
|
||||
<tbody>${zeigen.map((z) => `<tr>${spalten.map((k) => `<td>${esc(z[k])}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`;
|
||||
block.innerHTML = `
|
||||
<div class="schalterleiste"><input id="leads-suche" class="leads-suche" type="search" placeholder="In beiden Tabellen suchen: Name, Betrieb, Ort, Mail …" value="${esc(zustand.suche)}"></div>
|
||||
${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);
|
||||
};
|
||||
}
|
||||
@@ -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; } }
|
||||
|
||||
|
||||
Reference in new issue
Block a user