{"id":2378,"date":"2026-05-13T12:00:23","date_gmt":"2026-05-13T12:00:23","guid":{"rendered":"https:\/\/dpgsv.de\/?page_id=2378"},"modified":"2026-05-13T12:00:35","modified_gmt":"2026-05-13T12:00:35","slug":"kontakt-datenbank","status":"publish","type":"page","link":"https:\/\/dpgsv.de\/pl\/kontakt-datenbank\/","title":{"rendered":"Kontakt-Datenbank"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"2378\" class=\"elementor elementor-2378\">\n\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-6554ec8 elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"6554ec8\" data-element_type=\"section\" data-e-type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-40d1aff\" data-id=\"40d1aff\" data-element_type=\"column\" data-e-type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-3b7e2f5 elementor-widget elementor-widget-html\" data-id=\"3b7e2f5\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!doctype html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \/>\n  <title>Contact List Manager<\/title>\n  <script src=\"https:\/\/cdn.jsdelivr.net\/npm\/xlsx@0.18.5\/dist\/xlsx.full.min.js\"><\/script>\n  <style>\n    :root {\n      --bg: #f6f7fb;\n      --card: #ffffff;\n      --text: #1f2937;\n      --muted: #6b7280;\n      --border: #e5e7eb;\n      --primary: #2563eb;\n      --primary-dark: #1d4ed8;\n      --tag: #eef2ff;\n      --tag-text: #3730a3;\n      --green: #ecfdf5;\n      --yellow: #fffbeb;\n      --red: #fef2f2;\n    }\n\n    * { box-sizing: border-box; }\n\n    body {\n      margin: 0;\n      font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n      background: var(--bg);\n      color: var(--text);\n    }\n\n    header {\n      background: linear-gradient(135deg, #1e3a8a, #2563eb);\n      color: white;\n      padding: 28px 32px;\n    }\n\n    header h1 {\n      margin: 0 0 6px;\n      font-size: 28px;\n    }\n\n    header p {\n      margin: 0;\n      opacity: .9;\n    }\n\n    main {\n      padding: 24px 32px 40px;\n      max-width: 1500px;\n      margin: 0 auto;\n    }\n\n    .panel {\n      background: var(--card);\n      border: 1px solid var(--border);\n      border-radius: 18px;\n      padding: 18px;\n      box-shadow: 0 10px 24px rgba(15, 23, 42, .06);\n      margin-bottom: 18px;\n    }\n\n    .toolbar {\n      display: grid;\n      grid-template-columns: 1.2fr repeat(4, minmax(150px, .5fr));\n      gap: 12px;\n      align-items: end;\n    }\n\n    label {\n      display: block;\n      font-size: 12px;\n      font-weight: 700;\n      color: var(--muted);\n      margin-bottom: 6px;\n      text-transform: uppercase;\n      letter-spacing: .04em;\n    }\n\n    input, select, textarea, button {\n      font: inherit;\n    }\n\n    input, select, textarea {\n      width: 100%;\n      border: 1px solid var(--border);\n      border-radius: 12px;\n      padding: 10px 12px;\n      background: white;\n      color: var(--text);\n      outline: none;\n    }\n\n    input:focus, select:focus, textarea:focus {\n      border-color: var(--primary);\n      box-shadow: 0 0 0 3px rgba(37, 99, 235, .12);\n    }\n\n    button {\n      border: 0;\n      border-radius: 12px;\n      padding: 10px 14px;\n      background: var(--primary);\n      color: white;\n      cursor: pointer;\n      font-weight: 700;\n    }\n\n    button:hover { background: var(--primary-dark); }\n    button.secondary { background: #111827; }\n    button.light { background: #eef2ff; color: #1e3a8a; }\n    button.danger { background: #dc2626; }\n\n    .actions {\n      display: flex;\n      gap: 10px;\n      flex-wrap: wrap;\n      margin-top: 14px;\n    }\n\n    .stats {\n      display: flex;\n      gap: 12px;\n      flex-wrap: wrap;\n      margin-top: 10px;\n      color: var(--muted);\n      font-size: 14px;\n    }\n\n    .stat {\n      background: #f9fafb;\n      border: 1px solid var(--border);\n      border-radius: 999px;\n      padding: 7px 12px;\n    }\n\n    .table-wrap {\n      overflow: auto;\n      max-height: 68vh;\n      border-radius: 18px;\n      border: 1px solid var(--border);\n      background: white;\n    }\n\n    table {\n      width: 100%;\n      border-collapse: separate;\n      border-spacing: 0;\n      min-width: 1200px;\n    }\n\n    th {\n      position: sticky;\n      top: 0;\n      z-index: 2;\n      background: #f8fafc;\n      color: #334155;\n      text-align: left;\n      font-size: 12px;\n      text-transform: uppercase;\n      letter-spacing: .04em;\n      border-bottom: 1px solid var(--border);\n      padding: 12px;\n    }\n\n    td {\n      border-bottom: 1px solid var(--border);\n      padding: 10px 12px;\n      vertical-align: top;\n      font-size: 14px;\n    }\n\n    tr:hover td { background: #f9fafb; }\n\n    .tag {\n      display: inline-flex;\n      align-items: center;\n      background: var(--tag);\n      color: var(--tag-text);\n      border-radius: 999px;\n      padding: 4px 8px;\n      font-size: 12px;\n      font-weight: 700;\n      margin: 2px;\n    }\n\n    .notes {\n      min-width: 220px;\n      min-height: 42px;\n      resize: vertical;\n    }\n\n    .editable {\n      min-width: 140px;\n    }\n\n    .empty {\n      padding: 42px;\n      text-align: center;\n      color: var(--muted);\n    }\n\n    .share-box {\n      display: none;\n      margin-top: 12px;\n    }\n\n    .share-box textarea {\n      height: 90px;\n      font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n      font-size: 12px;\n    }\n\n    @media (max-width: 900px) {\n      main, header { padding-left: 16px; padding-right: 16px; }\n      .toolbar { grid-template-columns: 1fr; }\n    }\n  <\/style>\n<\/head>\n<body>\n  <header>\n    <h1>Contact List Manager<\/h1>\n    <p>Upload Excel\/CSV contacts, filter by region\/type\/tags, add notes, share, and export.<\/p>\n  <\/header>\n\n  <main>\n    <section class=\"panel\">\n      <div class=\"toolbar\">\n        <div>\n          <label for=\"fileInput\">Upload Excel or CSV<\/label>\n          <input id=\"fileInput\" type=\"file\" accept=\".xlsx,.xls,.csv\" \/>\n        <\/div>\n        <div>\n          <label for=\"searchInput\">Search<\/label>\n          <input id=\"searchInput\" placeholder=\"Name, email, organisation...\" \/>\n        <\/div>\n        <div>\n          <label for=\"typeFilter\">Type<\/label>\n          <select id=\"typeFilter\"><option value=\"\">All types<\/option><\/select>\n        <\/div>\n        <div>\n          <label for=\"regionFilter\">Region<\/label>\n          <select id=\"regionFilter\"><option value=\"\">All regions<\/option><\/select>\n        <\/div>\n        <div>\n          <label for=\"tagFilter\">Tag<\/label>\n          <select id=\"tagFilter\"><option value=\"\">All tags<\/option><\/select>\n        <\/div>\n      <\/div>\n\n      <div class=\"actions\">\n        <button onclick=\"downloadExcel()\">Download filtered Excel<\/button>\n        <button class=\"light\" onclick=\"downloadCSV()\">Download filtered CSV<\/button>\n        <button class=\"secondary\" onclick=\"generateShareLink()\">Create share link<\/button>\n        <button class=\"danger\" onclick=\"clearLocalData()\">Clear saved local data<\/button>\n      <\/div>\n\n      <div class=\"stats\">\n        <span class=\"stat\" id=\"totalCount\">0 contacts loaded<\/span>\n        <span class=\"stat\" id=\"filteredCount\">0 shown<\/span>\n        <span class=\"stat\">Notes\/tags are saved in this browser<\/span>\n      <\/div>\n\n      <div class=\"share-box\" id=\"shareBox\">\n        <label>Share link \/ data package<\/label>\n        <textarea id=\"shareText\" readonly><\/textarea>\n        <p style=\"color: var(--muted); font-size: 13px; margin-bottom: 0;\">For large lists, use Download Excel instead. Browser links can get too long.<\/p>\n      <\/div>\n    <\/section>\n\n    <section class=\"panel table-wrap\" id=\"tableContainer\">\n      <div class=\"empty\">Upload an Excel or CSV file to begin.<\/div>\n    <\/section>\n  <\/main>\n\n  <script>\n    const STORAGE_KEY = \"contact-manager-data-v1\";\n    let contacts = [];\n    let filteredContacts = [];\n    let headers = [];\n\n    const likelyColumns = {\n      organisation: [\"organisation\", \"institution\", \"company\"],\n      type: [\"type of organisation\", \"type\", \"organisation type\"],\n      region: [\"region\", \"area\", \"county\"],\n      email: [\"email\", \"e-mail\", \"mail\"],\n      name: [\"name\", \"first name\"],\n      surname: [\"surname\", \"last name\"],\n      phone: [\"phone\", \"telephone\", \"tel\"]\n    };\n\n    function normalize(value) {\n      return String(value || \"\").trim().toLowerCase();\n    }\n\n    function findColumn(type) {\n      const candidates = likelyColumns[type];\n      return headers.find(h => candidates.some(c => normalize(h).includes(c))) || \"\";\n    }\n\n    function uniqueValues(column) {\n      return [...new Set(contacts.map(c => c[column]).filter(Boolean).map(v => String(v).trim()))].sort();\n    }\n\n    function saveLocal() {\n      localStorage.setItem(STORAGE_KEY, JSON.stringify({ contacts, headers }));\n    }\n\n    function loadLocal() {\n      const raw = localStorage.getItem(STORAGE_KEY);\n      if (!raw) return;\n      try {\n        const parsed = JSON.parse(raw);\n        contacts = parsed.contacts || [];\n        headers = parsed.headers || [];\n        refreshUI();\n      } catch (e) {\n        console.warn(\"Could not load saved data\", e);\n      }\n    }\n\n    document.getElementById(\"fileInput\").addEventListener(\"change\", async (event) => {\n      const file = event.target.files[0];\n      if (!file) return;\n\n      const data = await file.arrayBuffer();\n      const workbook = XLSX.read(data, { type: \"array\" });\n      const firstSheet = workbook.Sheets[workbook.SheetNames[0]];\n      const rows = XLSX.utils.sheet_to_json(firstSheet, { defval: \"\" });\n\n      contacts = rows.map((row, index) => ({\n        id: crypto.randomUUID ? crypto.randomUUID() : String(Date.now() + index),\n        ...row,\n        Tags: row.Tags || \"\",\n        Notes: row.Notes || \"\"\n      }));\n\n      headers = Object.keys(rows[0] || {});\n      if (!headers.includes(\"Tags\")) headers.push(\"Tags\");\n      if (!headers.includes(\"Notes\")) headers.push(\"Notes\");\n\n      saveLocal();\n      refreshUI();\n    });\n\n    [\"searchInput\", \"typeFilter\", \"regionFilter\", \"tagFilter\"].forEach(id => {\n      document.getElementById(id).addEventListener(\"input\", applyFilters);\n    });\n\n    function refreshUI() {\n      populateFilters();\n      applyFilters();\n    }\n\n    function populateFilters() {\n      const typeCol = findColumn(\"type\");\n      const regionCol = findColumn(\"region\");\n\n      populateSelect(\"typeFilter\", typeCol ? uniqueValues(typeCol) : [], \"All types\");\n      populateSelect(\"regionFilter\", regionCol ? uniqueValues(regionCol) : [], \"All regions\");\n\n      const tags = [...new Set(contacts.flatMap(c => String(c.Tags || \"\").split(\",\").map(t => t.trim()).filter(Boolean)))].sort();\n      populateSelect(\"tagFilter\", tags, \"All tags\");\n    }\n\n    function populateSelect(id, values, placeholder) {\n      const select = document.getElementById(id);\n      const current = select.value;\n      select.innerHTML = `<option value=\"\">${placeholder}<\/option>` + values.map(v => `<option value=\"${escapeHtml(v)}\">${escapeHtml(v)}<\/option>`).join(\"\");\n      select.value = values.includes(current) ? current : \"\";\n    }\n\n    function applyFilters() {\n      const search = normalize(document.getElementById(\"searchInput\").value);\n      const selectedType = document.getElementById(\"typeFilter\").value;\n      const selectedRegion = document.getElementById(\"regionFilter\").value;\n      const selectedTag = document.getElementById(\"tagFilter\").value;\n      const typeCol = findColumn(\"type\");\n      const regionCol = findColumn(\"region\");\n\n      filteredContacts = contacts.filter(contact => {\n        const textMatch = !search || Object.values(contact).some(v => normalize(v).includes(search));\n        const typeMatch = !selectedType || String(contact[typeCol] || \"\") === selectedType;\n        const regionMatch = !selectedRegion || String(contact[regionCol] || \"\") === selectedRegion;\n        const tagMatch = !selectedTag || String(contact.Tags || \"\").split(\",\").map(t => t.trim()).includes(selectedTag);\n        return textMatch && typeMatch && regionMatch && tagMatch;\n      });\n\n      renderTable();\n      document.getElementById(\"totalCount\").textContent = `${contacts.length} contacts loaded`;\n      document.getElementById(\"filteredCount\").textContent = `${filteredContacts.length} shown`;\n    }\n\n    function renderTable() {\n      const container = document.getElementById(\"tableContainer\");\n      if (!contacts.length) {\n        container.innerHTML = `<div class=\"empty\">Upload an Excel or CSV file to begin.<\/div>`;\n        return;\n      }\n\n      const visibleHeaders = headers.filter(h => h !== \"id\");\n      container.innerHTML = `\n        <table>\n          <thead>\n            <tr>${visibleHeaders.map(h => `<th>${escapeHtml(h)}<\/th>`).join(\"\")}<\/tr>\n          <\/thead>\n          <tbody>\n            ${filteredContacts.map(contact => `\n              <tr>\n                ${visibleHeaders.map(header => renderCell(contact, header)).join(\"\")}\n              <\/tr>\n            `).join(\"\")}\n          <\/tbody>\n        <\/table>\n      `;\n    }\n\n    function renderCell(contact, header) {\n      const value = contact[header] || \"\";\n      if (header === \"Notes\") {\n        return `<td><textarea class=\"notes\" onchange=\"updateContact('${contact.id}', 'Notes', this.value)\">${escapeHtml(value)}<\/textarea><\/td>`;\n      }\n      if (header === \"Tags\") {\n        return `<td>\n          <input class=\"editable\" placeholder=\"e.g. priority, called\" value=\"${escapeHtml(value)}\" onchange=\"updateContact('${contact.id}', 'Tags', this.value)\" \/>\n          <div>${String(value).split(\",\").map(t => t.trim()).filter(Boolean).map(t => `<span class=\"tag\">${escapeHtml(t)}<\/span>`).join(\"\")}<\/div>\n        <\/td>`;\n      }\n      return `<td contenteditable=\"true\" onblur=\"updateContact('${contact.id}', '${escapeAttr(header)}', this.textContent)\">${escapeHtml(value)}<\/td>`;\n    }\n\n    function updateContact(id, field, value) {\n      const contact = contacts.find(c => c.id === id);\n      if (!contact) return;\n      contact[field] = value.trim();\n      saveLocal();\n      populateFilters();\n    }\n\n    function exportRows() {\n      return filteredContacts.map(({ id, ...rest }) => rest);\n    }\n\n    function downloadExcel() {\n      const worksheet = XLSX.utils.json_to_sheet(exportRows());\n      const workbook = XLSX.utils.book_new();\n      XLSX.utils.book_append_sheet(workbook, worksheet, \"Contacts\");\n      XLSX.writeFile(workbook, \"filtered_contacts.xlsx\");\n    }\n\n    function downloadCSV() {\n      const worksheet = XLSX.utils.json_to_sheet(exportRows());\n      const csv = XLSX.utils.sheet_to_csv(worksheet);\n      const blob = new Blob([csv], { type: \"text\/csv;charset=utf-8;\" });\n      const url = URL.createObjectURL(blob);\n      const a = document.createElement(\"a\");\n      a.href = url;\n      a.download = \"filtered_contacts.csv\";\n      a.click();\n      URL.revokeObjectURL(url);\n    }\n\n    function generateShareLink() {\n      const shareBox = document.getElementById(\"shareBox\");\n      const shareText = document.getElementById(\"shareText\");\n      const data = btoa(unescape(encodeURIComponent(JSON.stringify({ contacts: filteredContacts, headers }))));\n      const url = `${location.origin}${location.pathname}#data=${data}`;\n      shareText.value = url.length < 2000 ? url : \"List is too large for a reliable link. Please use Download filtered Excel instead.\";\n      shareBox.style.display = \"block\";\n    }\n\n    function loadFromHash() {\n      if (!location.hash.startsWith(\"#data=\")) return false;\n      try {\n        const data = location.hash.replace(\"#data=\", \"\");\n        const parsed = JSON.parse(decodeURIComponent(escape(atob(data))));\n        contacts = parsed.contacts || [];\n        headers = parsed.headers || [];\n        saveLocal();\n        refreshUI();\n        return true;\n      } catch (e) {\n        console.warn(\"Invalid shared data\", e);\n        return false;\n      }\n    }\n\n    function clearLocalData() {\n      if (!confirm(\"Clear all locally saved contacts, notes, and tags from this browser?\")) return;\n      localStorage.removeItem(STORAGE_KEY);\n      contacts = [];\n      filteredContacts = [];\n      headers = [];\n      refreshUI();\n    }\n\n    function escapeHtml(value) {\n      return String(value ?? \"\").replace(\/[&<>'\"]\/g, char => ({\n        \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", \"'\": \"&#39;\", '\"': \"&quot;\"\n      }[char]));\n    }\n\n    function escapeAttr(value) {\n      return String(value ?? \"\").replace(\/[\\\\'\\\"]\/g, \"\");\n    }\n\n    if (!loadFromHash()) loadLocal();\n  <\/script>\n<\/body>\n<\/html>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Contact List Manager Contact List Manager Upload Excel\/CSV contacts, filter by region\/type\/tags, add notes, share, and export. Upload Excel or CSV Search Type All types Region All regions Tag All tags Download filtered Excel Download filtered CSV Create share link<\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_canvas","meta":{"footnotes":""},"class_list":["post-2378","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/dpgsv.de\/pl\/wp-json\/wp\/v2\/pages\/2378","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dpgsv.de\/pl\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/dpgsv.de\/pl\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/dpgsv.de\/pl\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dpgsv.de\/pl\/wp-json\/wp\/v2\/comments?post=2378"}],"version-history":[{"count":4,"href":"https:\/\/dpgsv.de\/pl\/wp-json\/wp\/v2\/pages\/2378\/revisions"}],"predecessor-version":[{"id":2382,"href":"https:\/\/dpgsv.de\/pl\/wp-json\/wp\/v2\/pages\/2378\/revisions\/2382"}],"wp:attachment":[{"href":"https:\/\/dpgsv.de\/pl\/wp-json\/wp\/v2\/media?parent=2378"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}