{"id":13,"date":"2025-09-29T17:21:15","date_gmt":"2025-09-29T17:21:15","guid":{"rendered":"https:\/\/ernst.bureaukiem.nl\/?page_id=13"},"modified":"2025-09-29T20:29:02","modified_gmt":"2025-09-29T20:29:02","slug":"planning","status":"publish","type":"page","link":"https:\/\/ernst.bureaukiem.nl\/?page_id=13","title":{"rendered":"Planning"},"content":{"rendered":"\n<div id=\"app\" data-app=\"planning\" data-sheet-id=\"1-FuHTXu5ibYGAtod5l5V0GMO95mN1jmeIoXnc1KuT9M\" data-gid=\"540802128\">\n  <!-- hier staat je bestaande planning HTML met #week, #dag, #t, #status-pill, etc. -->\n<\/div>\n\n\n\n\n<div class=\"wrap\">\n  <header>\n    <div>\n      <div class=\"title\">Vrijwilligersoverzicht<\/div>\n      <div class=\"subtitle\">Snel zien wie wanneer aanwezig is \u2013 filter op weeknummer en dag<\/div>\n    <\/div>\n    <div class=\"toolbar\">\n      <button id=\"themeToggle\" class=\"btn\" title=\"Wissel thema\">\ud83c\udf17 Licht \/ Donker<\/button>\n      <span class=\"pill\" id=\"status-pill\">Laden\u2026<\/span>\n    <\/div>\n  <\/header>\n\n  <section class=\"panel\" id=\"filters\">\n    <div class=\"group\">\n      <label for=\"week\">Week<\/label>\n      <select id=\"week\"><option value=\"\">Alle weken<\/option><\/select>\n    <\/div>\n    <div class=\"group\">\n      <label for=\"dag\">Dag<\/label>\n      <select id=\"dag\">\n        <option value=\"\">Alle dagen<\/option>\n        <option>Donderdag<\/option>\n        <option>Vrijdag<\/option>\n        <option>Zaterdag<\/option>\n      <\/select>\n    <\/div>\n    <div class=\"group\" style=\"margin-left:auto\">\n      <input id=\"zoek\" type=\"search\" placeholder=\"Zoek op naam of tekst\" \/>\n    <\/div>\n  <\/section>\n\n  <div class=\"table-wrap\">\n    <table id=\"t\">\n      <thead><tr>\n        <th style=\"width:140px\">Datum<\/th>\n        <th style=\"width:80px\">Week<\/th>\n        <th style=\"width:120px\">Dag<\/th>\n        <th style=\"width:140px\">Bezetting<\/th>\n        <th>Aanwezig<\/th>\n      <\/tr><\/thead>\n      <tbody><\/tbody>\n    <\/table>\n  <\/div>\n\n  <section class=\"panel\" style=\"margin-top:14px\">\n    <div class=\"hint\" style=\"margin-bottom:6px\">Berichtgenerator (gebaseerd op de huidige <b>week<\/b>-filter)<\/div>\n    <div class=\"msg-row\">\n      <div class=\"msg-left\" style=\"flex:1 1 420px; min-width:300px\">\n        <textarea id=\"bericht\" placeholder=\"Klik op \u2018Genereer bericht\u2019\u2026\"><\/textarea>\n        <div class=\"hint\" style=\"margin-top:6px\">Tip: voeg hier je eigen extra zin(nen) toe (bijv. oproep voor open plekken).<\/div>\n      <\/div>\n      <div class=\"actions\">\n        <button id=\"genBtn\" class=\"btn primary\">Genereer bericht<\/button>\n        <button id=\"copyBtn\" class=\"btn\">Kopieer<\/button>\n      <\/div>\n    <\/div>\n  <\/section>\n\n  <div class=\"footer\">\n    <div class=\"count\" id=\"count\">0 rijen<\/div>\n    <div class=\"badge\">Bron: Google Sheet (<a class=\"link\" target=\"_blank\" rel=\"noreferrer noopener\" id=\"sheet-link\">open<\/a>)<\/div>\n  <\/div>\n<\/div>\n\n\n\n<script>\n  \/\/ ====== CONFIG \u2013 gebruik jouw sheetId en gid ======\n  const SHEET_ID = '1-FuHTXu5ibYGAtod5l5V0GMO95mN1jmeIoXnc1KuT9M';\n  const GID = '540802128';\n  \/\/ ================================================\n\n  const sheetURL = `https:\/\/docs.google.com\/spreadsheets\/d\/${SHEET_ID}\/edit#gid=${GID}`;\n  document.getElementById('sheet-link').href = sheetURL;\n\n  const gvizURL = `https:\/\/docs.google.com\/spreadsheets\/d\/${SHEET_ID}\/gviz\/tq?gid=${GID}&tqx=out:json`;\n\n  const weekSelect = document.getElementById('week');\n  const dagSelect = document.getElementById('dag');\n  const zoek = document.getElementById('zoek');\n  const tbody = document.querySelector('#t tbody');\n  const count = document.getElementById('count');\n  const statusPill = document.getElementById('status-pill');\n  const berichtTA = document.getElementById('bericht');\n  const genBtn = document.getElementById('genBtn');\n  const copyBtn = document.getElementById('copyBtn');\n\n  \/\/ Theme toggle\n  const rootHtml = document.documentElement;\n  const themeToggle = document.getElementById('themeToggle');\n  const savedTheme = localStorage.getItem('theme-pref');\n  if(savedTheme){ rootHtml.setAttribute('data-theme', savedTheme); }\n  themeToggle.addEventListener('click', ()=>{\n    const next = rootHtml.getAttribute('data-theme') === 'light' ? 'dark' : 'light';\n    rootHtml.setAttribute('data-theme', next);\n    localStorage.setItem('theme-pref', next);\n  });\n\n  \/\/ Parse dd-mm-yyyy \u2192 Date (local)\n  function parseNLDate(str){\n    if(!str) return null;\n    if(\/^Date\\(\/.test(str)){\n      const m = str.match(\/Date\\((\\d+),(\\d+),(\\d+)\\)\/);\n      if(m) return new Date(Number(m[1]), Number(m[2]), Number(m[3]));\n    }\n    const p = String(str).trim().split(\/[-\/]\/);\n    if(p.length !== 3) return new Date(str);\n    const [dd,mm,yyyy] = p.map(Number);\n    return new Date(yyyy, mm-1, dd);\n  }\n\n  function isoWeek(d){\n    const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));\n    const dayNum = (date.getUTCDay() + 6) % 7;\n    date.setUTCDate(date.getUTCDate() - dayNum + 3);\n    const firstThursday = new Date(Date.UTC(date.getUTCFullYear(),0,4));\n    const diff = (date - firstThursday) \/ 86400000;\n    return 1 + Math.floor(diff \/ 7);\n  }\n\n  function classifyShift(shift){\n    const s = String(shift).toLowerCase();\n    const isMorning = \/(smorgens|s\\'morgens|s\u2019\\morgens|ochtend)\/.test(s);\n    const isAfternoon = \/(smiddags|middag)\/.test(s);\n    const isFull = \/(gehele\\s*dag|hele\\s*dag|hele dag)\/.test(s);\n    return {isMorning, isAfternoon, isFull};\n  }\n\n  function occupancyBadge(m, a){\n    let cls = 'low';\n    if(m===0 && a===0) cls = 'none';\n    else if(m>=2 && a>=2) cls = 'ok';\n    else cls = 'low';\n    const title = `Smorgens: ${m} \u2022 Smiddags: ${a}`;\n    const label = cls==='ok' ? 'Volledige bezetting' : (cls==='low' ? 'Beperkte bezetting' : 'Geen bezetting');\n    return `<span class=\"status ${cls}\" title=\"${title}\"><span class=\"dot\"><\/span>${label}<\/span>`\n  }\n\n  function joinNames(parts){ return parts.join(', '); }\n\n  let rows = [];\n  let weeks = new Set();\n\n  function render(){\n    const w = weekSelect.value;\n    const d = dagSelect.value.toLowerCase();\n    const q = zoek.value.trim().toLowerCase();\n\n    const filtered = rows.filter(r=>{\n      const okW = !w || String(r.week) === w;\n      const okD = !d || (r.dag && r.dag.toLowerCase() === d);\n      const text = (r.dag + ' ' + r.aanwezigText).toLowerCase();\n      const okQ = !q || text.includes(q);\n      return okW && okD && okQ;\n    });\n\n    tbody.innerHTML = filtered.map(r=>{\n      const chips = r.aanwezig.length\n        ? r.aanwezig.map(x=>`<span class=\"chip\"><b>${x.naam}<\/b> \u2014 ${x.shift}<\/span>`).join('')\n        : '<span class=\"empty\">Niemand ingevuld<\/span>';\n      return `<tr>\n        <td>${r.datumStr}<\/td>\n        <td>${r.week}<\/td>\n        <td>${r.dag || ''}<\/td>\n        <td>${occupancyBadge(r.morningCount||0, r.afternoonCount||0)}<\/td>\n        <td>${chips}<\/td>\n      <\/tr>`\n    }).join('');\n\n    count.textContent = `${filtered.length} rijen`;\n  }\n\n  function populateWeekFilter(){\n    const sorted = Array.from(weeks).sort((a,b)=>a-b);\n    for(const w of sorted){\n      const opt = document.createElement('option');\n      opt.value = String(w);\n      opt.textContent = `Week ${w}`;\n      weekSelect.appendChild(opt);\n    }\n  }\n\n  function generateMessage(){\n    const selectedWeek = weekSelect.value;\n    if(!selectedWeek){\n      alert('Kies eerst een weeknummer om het bericht te maken.');\n      return;\n    }\n    const order = ['Donderdag','Vrijdag','Zaterdag'];\n    const inWeek = rows.filter(r=>String(r.week)===String(selectedWeek));\n\n    const byDay = {};\n    for(const d of order) byDay[d]=[];\n    for(const r of inWeek){\n      const list = r.aanwezig.map(x=>`${x.naam} (${x.shift})`);\n      const key = r.dag || r.datumStr;\n      if(!byDay[key]) byDay[key]=[];\n      if(list.length) byDay[key].push(joinNames(list));\n    }\n\n    const lines = [\"Voor deze week staan de volgende personen op de planning:\"];\n    for(const d of order){\n      if(byDay[d] && byDay[d].length){\n        lines.push(`${d}: ${joinNames(byDay[d])}`);\n      } else {\n        lines.push(`${d}: Voor deze dag hebben we nog geen bezetting. Zijn er mensen beschikbaar deze dag?`);\n      }\n    }\n\n    const current = berichtTA.value.trim();\n    const msg = lines.join('\\n') + (current? `\\n\\n${current}`: '');\n    berichtTA.value = msg;\n  }\n\n  function jsonpGviz(url){\n    return new Promise((resolve, reject)=>{\n      const prev = window.google?.visualization?.Query?.setResponse;\n      window.google = window.google || {};\n      google.visualization = google.visualization || {};\n      google.visualization.Query = google.visualization.Query || {};\n      google.visualization.Query.setResponse = (payload)=>{\n        cleanup();\n        resolve(payload);\n      };\n      const s = document.createElement('script');\n      s.src = url + `&cachebust=${Date.now()}`;\n      s.async = true;\n      s.onerror = ()=>{ cleanup(); reject(new Error('Script kon niet laden (controleer sheet-permissies en internetverbinding)')); };\n      document.head.appendChild(s);\n      function cleanup(){\n        if(s && s.parentNode) s.parentNode.removeChild(s);\n        if(prev){ google.visualization.Query.setResponse = prev; }\n      }\n    });\n  }\n\n  async function load(){\n    try{\n      const json = await jsonpGviz(gvizURL);\n      const cols = json.table?.cols || [];\n      const dataRows = json.table?.rows || [];\n      if(!Array.isArray(dataRows)) throw new Error('Onverwacht formaat: rows ontbreekt');\n\n      rows = [];\n      weeks.clear();\n\n      for(const r of dataRows){\n        const c = r.c || [];\n        const rawDate = (c[0] && (c[0].f ?? c[0].v)) || '';\n        if(!rawDate || \/Datum\/i.test(String(rawDate))) continue;\n        const dt = parseNLDate(rawDate);\n        if(!(dt instanceof Date) || isNaN(dt)) continue;\n\n        const datumStr = `${String(dt.getDate()).padStart(2,'0')}-${String(dt.getMonth()+1).padStart(2,'0')}-${dt.getFullYear()}`;\n        const week = isoWeek(dt);\n        weeks.add(week);\n\n        const dagVal = (c[3] && (c[3].f ?? c[3].v)) || '';\n        const dag = String(dagVal || '').trim();\n\n        const aanwezig = [];\n        let aanwezigText = '';\n        let morning = 0, afternoon = 0;\n        for(let i=4;i<cols.length;i++){\n          const colLabel = cols[i]?.label || `Kolom ${i+1}`;\n          const val = c[i] &#038;&#038; (c[i].f ?? c[i].v);\n          const shift = (val==null? '' : String(val).trim());\n          if(shift){\n            const naam = colLabel;\n            aanwezig.push({naam, shift});\n            aanwezigText += `${naam} ${shift} `;\n            const cls = classifyShift(shift);\n            if(cls.isFull){ morning += 1; afternoon += 1; }\n            else {\n              if(cls.isMorning) morning += 1;\n              if(cls.isAfternoon) afternoon += 1;\n            }\n          }\n        }\n\n        rows.push({datum:dt, datumStr, week, dag, aanwezig, aanwezigText:aanwezigText.trim(), morningCount:morning, afternoonCount:afternoon});\n      }\n\n      rows.sort((a,b)=>a.datum-b.datum);\n      weekSelect.innerHTML = '<option value=\"\">Alle weken<\/option>';\n      populateWeekFilter();\n      statusPill.textContent = `\u2713 ${rows.length} datums`;\n      render();\n    }catch(err){\n      console.error('[LOAD] Fout:', err);\n      statusPill.textContent = 'Kon data niet laden';\n      statusPill.style.background = 'var(--none-bg)';\n      statusPill.style.color = 'var(--none-fg)';\n      statusPill.title = 'Tip: zet de Google Sheet op \"Iedereen met de link \u2013 Bekijken\".';\n    }\n  }\n\n  weekSelect.addEventListener('change', render);\n  dagSelect.addEventListener('change', render);\n  zoek.addEventListener('input', render);\n  genBtn.addEventListener('click', generateMessage);\n  copyBtn.addEventListener('click', async ()=>{\n    try{\n      await navigator.clipboard.writeText(berichtTA.value);\n      copyBtn.textContent='Gekopieerd!';\n      setTimeout(()=>copyBtn.textContent='Kopieer', 1300);\n    }catch(e){ alert('Kopi\u00ebren mislukt.'); }\n  });\n\n  (function runTests(){\n    try{\n      console.assert('\\n'.length===1, '\"\\\\n\" length must be 1');\n      const j = ['a','b'].join('\\n');\n      console.assert(j === 'a\\nb', 'join with \"\\\\n\" should give a\\\\nb');\n      const d = parseNLDate('02-10-2025');\n      console.assert(d instanceof Date && !isNaN(d), 'parseNLDate werkt voor 02-10-2025');\n      const gd = parseNLDate('Date(2025,9,2)');\n      console.assert(gd instanceof Date && gd.getMonth()===9 && gd.getDate()===2, 'parseNLDate werkt voor gviz Date(...)');\n      const w1 = isoWeek(new Date(2025,0,1));\n      const w2 = isoWeek(new Date(2025,11,31));\n      console.assert(w1>=1 && w1<=53 &#038;&#038; w2>=1 && w2<=53, 'isoWeek binnen bereik 1..53');\n      console.assert(occupancyBadge(0,0).includes('status none'), '0\/0 => rood');\n      console.assert(occupancyBadge(2,2).includes('status ok'), '2\/2 => groen');\n      console.assert(occupancyBadge(2,0).includes('status low'), '2\/0 => oranje');\n      console.assert(occupancyBadge(1,1).includes('status low'), '1\/1 => oranje');\n      console.assert(occupancyBadge(1,0).includes('status low'), '1\/0 => oranje');\n      console.assert(joinNames(['A','B'])==='A, B', 'joinNames gebruikt comma, geen pipe');\n      (function(){ const days = ['Donderdag','Vrijdag','Zaterdag']; console.assert(days.length===3 && days[0]==='Donderdag' && days[2]==='Zaterdag', 'berichtgenerator gebruikt do\/vr\/za'); })();\n      console.log('[Self-test] OK');\n    }catch(e){ console.warn('[Self-test] Mislukt', e); }\n  })();\n\n  load();\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>Vrijwilligersoverzicht Snel zien wie wanneer aanwezig is \u2013 filter op weeknummer en dag \ud83c\udf17 Licht \/ Donker Laden\u2026 Week Alle weken Dag Alle dagenDonderdagVrijdagZaterdag Datum Week Dag Bezetting Aanwezig Berichtgenerator (gebaseerd op de huidige week-filter) Tip: voeg hier je eigen extra zin(nen) toe (bijv. oproep voor open plekken). Genereer bericht Kopieer 0 rijen Bron: Google [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"page-no-title","meta":{"footnotes":""},"class_list":["post-13","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/ernst.bureaukiem.nl\/index.php?rest_route=\/wp\/v2\/pages\/13","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ernst.bureaukiem.nl\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/ernst.bureaukiem.nl\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/ernst.bureaukiem.nl\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ernst.bureaukiem.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=13"}],"version-history":[{"count":6,"href":"https:\/\/ernst.bureaukiem.nl\/index.php?rest_route=\/wp\/v2\/pages\/13\/revisions"}],"predecessor-version":[{"id":35,"href":"https:\/\/ernst.bureaukiem.nl\/index.php?rest_route=\/wp\/v2\/pages\/13\/revisions\/35"}],"wp:attachment":[{"href":"https:\/\/ernst.bureaukiem.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=13"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}