{"id":14,"date":"2026-09-30T08:40:44","date_gmt":"2026-09-30T06:40:44","guid":{"rendered":"http:\/\/92.5.124.145\/?page_id=14"},"modified":"2026-09-30T09:57:36","modified_gmt":"2026-09-30T07:57:36","slug":"oenskeseddel-admin","status":"publish","type":"page","link":"https:\/\/katsuidev.dk\/index.php\/oenskeseddel-admin\/","title":{"rendered":"\u00d8nskeseddel admin"},"content":{"rendered":"\n<style>\n  :root {\n    --bg-base: #1e1e2e;\n    --bg-mantle: #181825;\n    --bg-surface: #313244;\n    --bg-surface-hover: #45475a;\n    --text-main: #cdd6f4;\n    --accent-blue: #89b4fa;\n    --accent-green: #a6e3a1;\n    --accent-red: #f38ba8;\n    --border-color: #45475a;\n  }\n\n  .admin-wrapper {\n    background-color: var(--bg-base);\n    color: var(--text-main);\n    padding: 20px;\n    max-width: 900px;\n    margin: 0 auto;\n    border-radius: 12px;\n    font-family: 'Segoe UI', system-ui, sans-serif;\n  }\n\n  .top-btns {\n    display: flex;\n    gap: 10px;\n    flex-wrap: wrap;\n    margin-bottom: 20px;\n  }\n\n  button {\n    cursor: pointer;\n    border: none;\n    border-radius: 6px;\n    padding: 8px 14px;\n    font-weight: bold;\n    font-size: 0.85rem;\n  }\n\n  .btn-blue { background-color: var(--accent-blue); color: #11111b; }\n  .btn-green { background-color: var(--accent-green); color: #11111b; }\n  .btn-red { background-color: var(--accent-red); color: #11111b; }\n  .btn-cancel { background-color: var(--bg-surface-hover); color: var(--text-main); }\n\n  .store-card {\n    border: 1px solid var(--border-color);\n    border-radius: 8px;\n    margin-bottom: 15px;\n    background-color: var(--bg-base);\n    padding: 15px;\n  }\n\n  .store-header-admin {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    border-bottom: 1px solid var(--border-color);\n    padding-bottom: 10px;\n    margin-bottom: 10px;\n  }\n\n  .item-row {\n    background-color: var(--bg-mantle);\n    border: 1px solid var(--border-color);\n    border-radius: 8px;\n    padding: 10px;\n    margin-bottom: 10px;\n    display: flex;\n    align-items: center;\n    gap: 12px;\n  }\n\n  .item-img {\n    width: 60px;\n    height: 60px;\n    border-radius: 6px;\n    object-fit: cover;\n    background-color: var(--bg-surface);\n    cursor: pointer;\n    flex-shrink: 0;\n  }\n\n  .item-info { flex-grow: 1; }\n  .item-info h4 { margin: 0 0 2px 0; font-size: 0.95rem; color: var(--text-main); }\n  .item-info p { margin: 0; font-size: 0.85rem; opacity: 0.8; }\n\n  \/* Tvinger modal overalt via body-teleportation *\/\n  .modal-overlay {\n    position: fixed !important;\n    top: 0 !important;\n    left: 0 !important;\n    width: 100vw !important;\n    height: 100vh !important;\n    background: rgba(0, 0, 0, 0.8) !important;\n    display: none;\n    justify-content: center !important;\n    align-items: center !important;\n    z-index: 999999 !important;\n  }\n  .modal-overlay.active { display: flex !important; }\n\n  .modal-card {\n    background-color: var(--bg-mantle);\n    border: 2px solid var(--accent-blue);\n    border-radius: 12px;\n    padding: 20px;\n    width: 90%;\n    max-width: 500px;\n    box-shadow: 0 10px 30px rgba(0,0,0,0.8);\n    box-sizing: border-box;\n  }\n\n  .form-group { margin-bottom: 12px; }\n  .form-group label { display: block; font-size: 0.85rem; margin-bottom: 4px; }\n  .form-group input {\n    width: 100%;\n    padding: 8px;\n    background-color: var(--bg-surface);\n    border: 1px solid var(--border-color);\n    color: var(--text-main);\n    border-radius: 6px;\n    box-sizing: border-box;\n  }\n\n  .modal-buttons { display: flex; gap: 10px; margin-top: 15px; }\n  .modal-buttons button { flex: 1; }\n\n  #preview-img {\n    max-width: 100%;\n    max-height: 70vh;\n    object-fit: contain;\n    border-radius: 8px;\n    margin-top: 10px;\n  }\n<\/style>\n\n<div class=\"admin-wrapper\">\n  <h2 style=\"color: var(--accent-blue); margin-bottom: 15px;\">\ud83d\udee0\ufe0f Administrer \u00d8nskeseddel<\/h2>\n  \n  <div class=\"top-btns\">\n    <button class=\"btn-green\" onclick=\"saveDataToServer()\">\ud83d\udcbe Gem til Server<\/button>\n    <button class=\"btn-cancel\" onclick=\"document.getElementById('import-file').click()\">\ud83d\udcc2 \u00c5bn JSON Fil<\/button>\n    <button class=\"btn-blue\" onclick=\"openStoreModal()\">+ Tilf\u00f8j Butik<\/button>\n    <input type=\"file\" id=\"import-file\" accept=\".json\" style=\"display: none;\" onchange=\"importFromFile(event)\" \/>\n  <\/div>\n\n  <div id=\"stores-list\">Indl\u00e6ser&#8230;<\/div>\n<\/div>\n\n<!-- Modal til Edit\/Add Store -->\n<div class=\"modal-overlay\" id=\"store-modal\">\n  <div class=\"modal-card\">\n    <h3 id=\"store-modal-title\" style=\"color: var(--accent-blue); margin-bottom:10px;\">Tilf\u00f8j Butik<\/h3>\n    <input type=\"hidden\" id=\"store-id\" \/>\n    <div class=\"form-group\">\n      <label>Butiksnavn:<\/label>\n      <input type=\"text\" id=\"store-name\" \/>\n    <\/div>\n    <div class=\"form-group\">\n      <label>Hjemmeside (URL):<\/label>\n      <input type=\"text\" id=\"store-site\" \/>\n    <\/div>\n    <div class=\"form-group\">\n      <label>Valuta:<\/label>\n      <input type=\"text\" id=\"store-curr\" value=\"DKK\" \/>\n    <\/div>\n    <div class=\"form-group\">\n      <label>Fragtpris:<\/label>\n      <input type=\"number\" step=\"0.01\" id=\"store-ship\" \/>\n    <\/div>\n    <div class=\"form-group\">\n      <label>Gr\u00e6nse for fri fragt (valgfri):<\/label>\n      <input type=\"number\" step=\"0.01\" id=\"store-thresh\" \/>\n    <\/div>\n    <div class=\"modal-buttons\">\n      <button class=\"btn-blue\" onclick=\"saveStore()\">Gem Butik<\/button>\n      <button class=\"btn-red\" id=\"store-del-btn\" style=\"display:none;\" onclick=\"deleteStore()\">Slet<\/button>\n      <button class=\"btn-cancel\" onclick=\"closeModals()\">Annuller<\/button>\n    <\/div>\n  <\/div>\n<\/div>\n\n<!-- Modal til Edit\/Add Item -->\n<div class=\"modal-overlay\" id=\"item-modal\">\n  <div class=\"modal-card\">\n    <h3 id=\"item-modal-title\" style=\"color: var(--accent-blue); margin-bottom:10px;\">Tilf\u00f8j \u00d8nske<\/h3>\n    <input type=\"hidden\" id=\"item-store-id\" \/>\n    <input type=\"hidden\" id=\"item-idx\" \/>\n    <div class=\"form-group\">\n      <label>Navn p\u00e5 \u00f8nske:<\/label>\n      <input type=\"text\" id=\"item-name\" \/>\n    <\/div>\n    <div class=\"form-group\">\n      <label>Detaljer (f.eks. str.\/farve):<\/label>\n      <input type=\"text\" id=\"item-details\" \/>\n    <\/div>\n    <div class=\"form-group\">\n      <label>Pris:<\/label>\n      <input type=\"number\" step=\"0.01\" id=\"item-price\" \/>\n    <\/div>\n    <div class=\"form-group\">\n      <label>Direkte link til varen:<\/label>\n      <input type=\"text\" id=\"item-link\" \/>\n    <\/div>\n    <div class=\"form-group\">\n      <label>V\u00e6lg Billede:<\/label>\n      <input type=\"file\" id=\"item-img-file\" accept=\"image\/*\" \/>\n    <\/div>\n    <div class=\"modal-buttons\">\n      <button class=\"btn-blue\" onclick=\"saveItem()\">Gem \u00d8nske<\/button>\n      <button class=\"btn-red\" id=\"item-del-btn\" style=\"display:none;\" onclick=\"deleteItem()\">Slet<\/button>\n      <button class=\"btn-cancel\" onclick=\"closeModals()\">Annuller<\/button>\n    <\/div>\n  <\/div>\n<\/div>\n\n<!-- Modal til Billed-preview i Admin -->\n<div class=\"modal-overlay\" id=\"img-modal\" onclick=\"closeModals()\">\n  <div class=\"modal-card\" style=\"text-align: center;\" onclick=\"event.stopPropagation()\">\n    <h3 id=\"img-title\" style=\"color: var(--accent-blue);\">Billede<\/h3>\n    <img decoding=\"async\" id=\"preview-img\" src=\"\" alt=\"Preview\" \/>\n    <div style=\"margin-top: 15px;\">\n      <button class=\"btn-cancel\" onclick=\"closeModals()\">Luk<\/button>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n  let storesData = [];\n\n  \/\/ Flytter modalerne direkte ud under <body> s\u00e5 WordPress-temaer ikke l\u00e5ser dem inde\n  document.addEventListener(\"DOMContentLoaded\", function() {\n    ['store-modal', 'item-modal', 'img-modal'].forEach(id => {\n      const el = document.getElementById(id);\n      if (el) document.body.appendChild(el);\n    });\n  });\n\n  async function loadDataFromServer() {\n    try {\n      const res = await fetch('\/wp-json\/onskeseddel\/v1\/data');\n      storesData = await res.json() || [];\n      render();\n    } catch(e) {\n      storesData = [];\n      render();\n    }\n  }\n\n  async function saveDataToServer() {\n    const res = await fetch('\/wp-json\/onskeseddel\/v1\/save', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application\/json' },\n      body: JSON.stringify(storesData)\n    });\n    if (res.ok) {\n      alert('\u00d8nskesedlen er gemt p\u00e5 serveren!');\n    } else {\n      alert('Kunne ikke gemme p\u00e5 serveren.');\n    }\n  }\n\n  function render() {\n    const container = document.getElementById('stores-list');\n    container.innerHTML = '';\n\n    if (storesData.length === 0) {\n      container.innerHTML = '<p style=\"text-align: center; opacity: 0.6; margin-top: 20px;\">Ingen butikker endnu. Klik p\u00e5 \"+ Tilf\u00f8j Butik\" eller \"\ud83d\udcc2 \u00c5bn JSON Fil\".<\/p>';\n      return;\n    }\n\n    storesData.forEach((store, sIdx) => {\n      const card = document.createElement('div');\n      card.className = 'store-card';\n\n      let itemsHTML = (store.shopping_list || []).map((item, iIdx) => `\n        <div class=\"item-row\">\n          <img decoding=\"async\" class=\"item-img\" src=\"${item.image_data || ''}\" onerror=\"this.src='data:image\/svg+xml;utf8,<svg xmlns=\\'http:\/\/www.w3.org\/2000\/svg\\' width=\\'60\\' height=\\'60\\'><rect width=\\'60\\' height=\\'60\\' fill=\\'%23313244\\'\/><\/svg>'\" onclick=\"showImage('${item.image_data || ''}', '${item.name}')\" \/>\n          <div class=\"item-info\">\n            <h4>${item.name}<\/h4>\n            <p>\u2022 ${item.details || ''}<\/p>\n            <p>Pris: ${(Number(item.price)||0).toFixed(2)} ${store.currency || 'DKK'}<\/p>\n          <\/div>\n          <button class=\"btn-blue\" onclick=\"openItemModal(${sIdx}, ${iIdx})\">Rediger<\/button>\n        <\/div>\n      `).join('');\n\n      card.innerHTML = `\n        <div class=\"store-header-admin\" style=\"cursor: pointer;\" onclick=\"toggleStoreAdmin(${sIdx})\">\n          <strong id=\"store-admin-title-${sIdx}\" style=\"color: var(--accent-blue);\">\u25b6 ${store.name} (${(store.shopping_list || []).length} \u00f8nsker)<\/strong>\n          <div onclick=\"event.stopPropagation()\">\n            <button class=\"btn-blue\" onclick=\"openStoreModal(${sIdx})\">Rediger Butik<\/button>\n            <button class=\"btn-blue\" onclick=\"openItemModal(${sIdx})\">+ Tilf\u00f8j \u00d8nske<\/button>\n          <\/div>\n        <\/div>\n        <div class=\"store-content\" id=\"store-admin-content-${sIdx}\" style=\"display: none; padding-top: 10px;\">\n          ${itemsHTML}\n        <\/div>\n      `;\n      container.appendChild(card);\n    });\n  }\n\n  function toggleStoreAdmin(idx) {\n    const content = document.getElementById(`store-admin-content-${idx}`);\n    const title = document.getElementById(`store-admin-title-${idx}`);\n    const isHidden = content.style.display === 'none';\n    \n    content.style.display = isHidden ? 'block' : 'none';\n    const storeName = storesData[idx].name;\n    const count = (storesData[idx].shopping_list || []).length;\n    title.innerText = `${isHidden ? '\u25bc' : '\u25b6'} ${storeName} (${count} \u00f8nsker)`;\n  }\n\n  function showImage(src, title) {\n    if (!src) return;\n    document.getElementById('img-title').innerText = title;\n    document.getElementById('preview-img').src = src;\n    document.getElementById('img-modal').classList.add('active');\n  }\n\n  function closeModals() {\n    document.querySelectorAll('.modal-overlay').forEach(m => m.classList.remove('active'));\n  }\n\n  function importFromFile(event) {\n    const file = event.target.files[0];\n    if (!file) return;\n\n    const reader = new FileReader();\n    reader.onload = function(e) {\n      try {\n        const importedData = JSON.parse(e.target.result);\n        if (Array.isArray(importedData)) {\n          storesData = importedData;\n          render();\n          alert(\"JSON indl\u00e6st! Klik p\u00e5 '\ud83d\udcbe Gem til Server' for at opdatere permanent.\");\n        }\n      } catch (err) {\n        alert(\"Fejl ved l\u00e6sning af filen.\");\n      }\n    };\n    reader.readAsText(file);\n  }\n\n  \/* Store Operations *\/\n  function openStoreModal(sIdx = null) {\n    document.getElementById('store-id').value = sIdx !== null ? sIdx : '';\n    document.getElementById('store-modal-title').innerText = sIdx !== null ? 'Rediger Butik' : 'Tilf\u00f8j Butik';\n    document.getElementById('store-del-btn').style.display = sIdx !== null ? 'block' : 'none';\n\n    if (sIdx !== null) {\n      const s = storesData[sIdx];\n      document.getElementById('store-name').value = s.name;\n      document.getElementById('store-site').value = s.website;\n      document.getElementById('store-curr').value = s.currency || 'DKK';\n      document.getElementById('store-ship').value = s.flat_shipping ?? s.standard_shipping ?? '';\n      document.getElementById('store-thresh').value = s.free_shipping_threshold ?? '';\n    } else {\n      document.getElementById('store-name').value = '';\n      document.getElementById('store-site').value = '';\n      document.getElementById('store-curr').value = 'DKK';\n      document.getElementById('store-ship').value = '';\n      document.getElementById('store-thresh').value = '';\n    }\n    document.getElementById('store-modal').classList.add('active');\n  }\n\n  function saveStore() {\n    const sIdx = document.getElementById('store-id').value;\n    const name = document.getElementById('store-name').value.trim() || 'Ny Butik';\n    const website = document.getElementById('store-site').value.trim() || 'https:\/\/google.com';\n    const currency = document.getElementById('store-curr').value.trim().toUpperCase() || 'DKK';\n    const ship = parseFloat(document.getElementById('store-ship').value) || 0;\n    const thresh = parseFloat(document.getElementById('store-thresh').value);\n\n    const storeObj = { name, website, currency, shopping_list: sIdx !== '' ? storesData[sIdx].shopping_list : [] };\n    if (!isNaN(thresh)) {\n      storeObj.free_shipping_threshold = thresh;\n      storeObj.standard_shipping = ship;\n    } else {\n      storeObj.flat_shipping = ship;\n    }\n\n    if (sIdx !== '') {\n      storesData[sIdx] = storeObj;\n    } else {\n      storesData.push(storeObj);\n    }\n    render();\n    closeModals();\n  }\n\n  function deleteStore() {\n    const sIdx = document.getElementById('store-id').value;\n    if (sIdx !== '' && confirm('Slet butik?')) {\n      storesData.splice(sIdx, 1);\n      render();\n    }\n    closeModals();\n  }\n\n  \/* Billed-komprimering *\/\n  function compressImage(file, callback) {\n    const maxWidth = 800;\n    const maxHeight = 800;\n    const reader = new FileReader();\n\n    reader.onload = function(e) {\n      const img = new Image();\n      img.onload = function() {\n        let width = img.width;\n        let height = img.height;\n\n        if (width > height) {\n          if (width > maxWidth) {\n            height = Math.round((height * maxWidth) \/ width);\n            width = maxWidth;\n          }\n        } else {\n          if (height > maxHeight) {\n            width = Math.round((width * maxHeight) \/ height);\n            height = maxHeight;\n          }\n        }\n\n        const canvas = document.createElement('canvas');\n        canvas.width = width;\n        canvas.height = height;\n        const ctx = canvas.getContext('2d');\n        ctx.drawImage(img, 0, 0, width, height);\n\n        const dataUrl = canvas.toDataURL('image\/jpeg', 0.7);\n        callback(dataUrl);\n      };\n      img.src = e.target.result;\n    };\n    reader.readAsDataURL(file);\n  }\n\n  \/* Item Operations *\/\n  function openItemModal(sIdx, iIdx = null) {\n    document.getElementById('item-store-id').value = sIdx;\n    document.getElementById('item-idx').value = iIdx !== null ? iIdx : '';\n    document.getElementById('item-modal-title').innerText = iIdx !== null ? 'Rediger \u00d8nske' : 'Tilf\u00f8j \u00d8nske';\n    document.getElementById('item-del-btn').style.display = iIdx !== null ? 'block' : 'none';\n\n    if (iIdx !== null) {\n      const item = storesData[sIdx].shopping_list[iIdx];\n      document.getElementById('item-name').value = item.name;\n      document.getElementById('item-details').value = item.details || '';\n      document.getElementById('item-price').value = item.price;\n      document.getElementById('item-link').value = item.link || '';\n    } else {\n      document.getElementById('item-name').value = '';\n      document.getElementById('item-details').value = '';\n      document.getElementById('item-price').value = '';\n      document.getElementById('item-link').value = '';\n    }\n    document.getElementById('item-img-file').value = '';\n    document.getElementById('item-modal').classList.add('active');\n  }\n\n  function saveItem() {\n    const sIdx = document.getElementById('item-store-id').value;\n    const iIdx = document.getElementById('item-idx').value;\n    const fileInput = document.getElementById('item-img-file');\n\n    const name = document.getElementById('item-name').value.trim() || '\u00d8nske';\n    const details = document.getElementById('item-details').value.trim();\n    const price = parseFloat(document.getElementById('item-price').value) || 0;\n    const link = document.getElementById('item-link').value.trim();\n\n    let existingImgData = (iIdx !== '' && storesData[sIdx].shopping_list[iIdx]) ? storesData[sIdx].shopping_list[iIdx].image_data : '';\n\n    const processAndSave = (imgData) => {\n      const itemObj = { name, details, price, link, image_data: imgData };\n      if (!storesData[sIdx].shopping_list) storesData[sIdx].shopping_list = [];\n      \n      if (iIdx !== '') {\n        storesData[sIdx].shopping_list[iIdx] = itemObj;\n      } else {\n        storesData[sIdx].shopping_list.push(itemObj);\n      }\n      render();\n      closeModals();\n    };\n\n    if (fileInput.files && fileInput.files[0]) {\n      compressImage(fileInput.files[0], function(compressedDataUrl) {\n        processAndSave(compressedDataUrl);\n      });\n    } else {\n      processAndSave(existingImgData);\n    }\n  }\n\n  function deleteItem() {\n    const sIdx = document.getElementById('item-store-id').value;\n    const iIdx = document.getElementById('item-idx').value;\n    if (sIdx !== '' && iIdx !== '' && confirm('Slet \u00f8nske?')) {\n      storesData[sIdx].shopping_list.splice(iIdx, 1);\n      render();\n    }\n    closeModals();\n  }\n\n  loadDataFromServer();\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>\ud83d\udee0\ufe0f Administrer \u00d8nskeseddel \ud83d\udcbe Gem til Server \ud83d\udcc2 \u00c5bn JSON Fil + Tilf\u00f8j Butik Indl\u00e6ser&#8230; Tilf\u00f8j Butik Butiksnavn: Hjemmeside (URL): Valuta: Fragtpris: Gr\u00e6nse for fri fragt (valgfri): Gem Butik Slet Annuller Tilf\u00f8j \u00d8nske Navn p\u00e5 \u00f8nske: Detaljer (f.eks. str.\/farve): Pris: Direkte link til varen: V\u00e6lg Billede: Gem \u00d8nske Slet Annuller Billede Luk<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-14","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/katsuidev.dk\/index.php\/wp-json\/wp\/v2\/pages\/14","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/katsuidev.dk\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/katsuidev.dk\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/katsuidev.dk\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/katsuidev.dk\/index.php\/wp-json\/wp\/v2\/comments?post=14"}],"version-history":[{"count":7,"href":"https:\/\/katsuidev.dk\/index.php\/wp-json\/wp\/v2\/pages\/14\/revisions"}],"predecessor-version":[{"id":74,"href":"https:\/\/katsuidev.dk\/index.php\/wp-json\/wp\/v2\/pages\/14\/revisions\/74"}],"wp:attachment":[{"href":"https:\/\/katsuidev.dk\/index.php\/wp-json\/wp\/v2\/media?parent=14"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}