{"id":39,"date":"2025-09-29T18:57:29","date_gmt":"2025-09-29T18:57:29","guid":{"rendered":"https:\/\/ernst.bureaukiem.nl\/?page_id=39"},"modified":"2025-09-29T19:39:15","modified_gmt":"2025-09-29T19:39:15","slug":"inzet","status":"publish","type":"page","link":"https:\/\/ernst.bureaukiem.nl\/?page_id=39","title":{"rendered":"Inzet"},"content":{"rendered":"\n<div id=\"app\"\n     data-app=\"future\"\n     data-sheet-id=\"1-FuHTXu5ibYGAtod5l5V0GMO95mN1jmeIoXnc1KuT9M\"\n     data-gid=\"540802128\">\n  <!-- jouw HTML -->\n<\/div>\n\n\n\n\n\n\n<!doctype html>\n<html lang=\"nl\" data-theme=\"dark\">\n<head>\n  <meta charset=\"utf-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \/>\n  <title>Inzetanalyse<\/title>\n  <style>\n    :root{ --radius:18px; --bg:#0b1020; --card:#121936; --muted:#8fa3c8; --text:#e8eefc; --accent:#5b8cff; --accent-2:#76e0a5; --card-line:#243466; --shadow-panel:0 10px 30px rgba(0,0,0,.08); --shadow-table:0 16px 40px rgba(10,20,60,.10) }\n    [data-theme=\"light\"]{ --bg:#f7f8fb; --card:#ffffff; --muted:#64748b; --text:#0f172a; --accent:#3b82f6; --accent-2:#16a34a; --card-line:#e6ebf4; --shadow-panel:0 2px 6px rgba(0,0,0,.04); --shadow-table:0 3px 8px rgba(0,0,0,.05) }\n    html,body{margin:0}\n    body{ background:var(--bg); font-family: ui-sans-serif, system-ui,-apple-system, Segoe UI, Roboto, Arial, \"Noto Sans\",\"Helvetica Neue\"; color:var(--text); transition: background-color .25s ease, color .25s ease }\n    .wrap{max-width:1200px; margin:40px auto; padding:0 20px}\n    header{display:flex; align-items:center; justify-content:space-between; gap:16px; margin-bottom:18px}\n    .title{font-weight:800; letter-spacing:.3px; font-size:clamp(22px,3vw,28px)}\n    .subtitle{color:var(--muted); font-size:14px}\n    .toolbar{display:flex; gap:10px; align-items:center}\n    .btn{cursor:pointer; border:1px solid var(--card-line); background:var(--card); color:var(--text); padding:8px 12px; border-radius:12px; font-weight:600}\n    .panel{display:flex; flex-wrap:wrap; gap:10px; background:var(--card); padding:14px; border:1px solid var(--card-line); border-radius:var(--radius); box-shadow:var(--shadow-panel)}\n    .panel .group{display:flex; gap:10px; align-items:center}\n    input[type=\"search\"],input[type=\"number\"],input[type=\"date\"]{ background:var(--card); color:var(--text); border:1px solid var(--card-line); border-radius:12px; padding:8px 10px; font-size:14px; outline:none; appearance:none }\n    .pill{border-radius:999px; padding:6px 10px; font-size:12px; color:#0b1a10; background:var(--accent-2); font-weight:700}\n    .muted{color:var(--muted); font-size:13px}\n    .section{margin-top:16px}\n    .section-title{display:flex; align-items:center; justify-content:space-between; margin:8px 2px 8px}\n    .card{background:var(--card); border:1px solid var(--card-line); border-radius:var(--radius); padding:10px 12px}\n    .list{display:flex; flex-wrap:wrap; gap:8px}\n    .chip{display:inline-block; padding:4px 8px; border-radius:999px; background:var(--card); border:1px solid var(--card-line); margin:2px 4px 2px 0; font-size:12px}\n    .danger{background:#4a1f24; color:#ffb3bf; border-color:#7a2e38}\n    .warn{background:#42300d; color:#ffd89a; border-color:#775b1e}\n    .ok{background:#1b3a2a; color:#a6f3c6; border-color:#2c6b4a}\n    .table-wrap{background:var(--card); border:1px solid var(--card-line); border-radius:var(--radius); overflow:auto; box-shadow:var(--shadow-table)}\n    table{width:100%; border-collapse:separate; border-spacing:0; min-width:680px}\n    thead th{position:sticky; top:0; background:var(--card); color:var(--text); text-align:left; font-weight:700; font-size:13px; padding:12px; border-bottom:1px solid var(--card-line)}\n    tbody td{padding:10px 12px; border-bottom:1px dashed color-mix(in oklab, var(--card-line) 70%, #000 30%); vertical-align:top; font-size:14px}\n    tbody tr:hover{background:color-mix(in oklab, var(--card) 90%, #000 10%)}\n  <\/style>\n<\/head>\n<body>\n  <div class=\"wrap\">\n    <header>\n      <div>\n        <div class=\"title\">Inzetanalyse<\/div>\n        <div class=\"subtitle\">Komende periode bovenaan, historie daaronder<\/div>\n      <\/div>\n      <div class=\"toolbar\">\n        <button id=\"themeToggle\" class=\"btn\">\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\"><label for=\"lookahead\">Komende (weken)<\/label><input id=\"lookahead\" type=\"number\" min=\"1\" max=\"52\" value=\"8\"><\/div>\n      <div class=\"group\"><label for=\"threshold\">Drempel \u2264<\/label><input id=\"threshold\" type=\"number\" min=\"0\" max=\"10\" value=\"2\"><\/div>\n      <div class=\"group\"><label for=\"from\">Historie vanaf<\/label><input id=\"from\" type=\"date\"><\/div>\n      <div class=\"group\"><label for=\"to\">t\/m<\/label><input id=\"to\" type=\"date\"><\/div>\n      <div class=\"group\"><input id=\"zoek\" type=\"search\" placeholder=\"Zoek op naam\u2026\"><\/div>\n      <div class=\"group\" style=\"margin-left:auto\"><a id=\"sheetLink\" class=\"muted\" target=\"_blank\" rel=\"noopener\">Open sheet<\/a><\/div>\n    <\/section>\n\n    <!-- 1) KOMENDE PERIODE (bovenaan) -->\n    <section class=\"section\" id=\"future\">\n      <div class=\"section-title\"><h3 style=\"margin:0\">Komende periode<\/h3><span class=\"muted\" id=\"future-count\">\u2013<\/span><\/div>\n      <div class=\"card\" style=\"margin-bottom:10px\">\n        <div class=\"muted\" style=\"margin-bottom:6px\">Weinig ingepland (\u2264 drempel)<\/div>\n        <div id=\"low-list\" class=\"list\"><\/div>\n      <\/div>\n      <div class=\"table-wrap\">\n        <table id=\"future-table\">\n          <thead><tr><th style=\"width:220px\">Naam<\/th><th style=\"width:120px\">Ingepland<\/th><th style=\"width:160px\">Eerstvolgende<\/th><th>Datums (binnen periode)<\/th><\/tr><\/thead>\n          <tbody><\/tbody>\n        <\/table>\n      <\/div>\n    <\/section>\n\n    <!-- 2) HISTORIE (onderaan) -->\n    <section class=\"section\" id=\"past\">\n      <div class=\"section-title\"><h3 style=\"margin:0\">Historie<\/h3><span class=\"muted\" id=\"past-count\">\u2013<\/span><\/div>\n      <div class=\"table-wrap\">\n        <table id=\"past-table\">\n          <thead><tr><th style=\"width:220px\">Naam<\/th><th style=\"width:100px\">Totaal<\/th><th>Datums<\/th><\/tr><\/thead>\n          <tbody><\/tbody>\n        <\/table>\n      <\/div>\n    <\/section>\n\n    <p class=\"muted\" id=\"row-info\" style=\"margin-top:10px\"><\/p>\n  <\/div>\n\n<script>\n\/\/ ============== CONFIG ==============\nconst SHEET_ID = '1-FuHTXu5ibYGAtod5l5V0GMO95mN1jmeIoXnc1KuT9M'\nconst GID      = '540802128'\n\/\/ Optioneel: CSV-fallback (Bestand \u2192 Publiceren op het web \u2192 Kies tabblad \u2192 CSV, kopieer URL)\n\/\/ const PUB_CSV_URL = 'https:\/\/docs.google.com\/spreadsheets\/d\/e\/...\/pub?gid=540802128&single=true&output=csv'\n\/\/ ====================================\n\nconst root = document.documentElement\nroot.setAttribute('data-theme', localStorage.getItem('theme-pref') || 'dark')\ndocument.getElementById('themeToggle').addEventListener('click', ()=>{\n  const next = root.getAttribute('data-theme') === 'light' ? 'dark' : 'light'\n  root.setAttribute('data-theme', next)\n  localStorage.setItem('theme-pref', next)\n})\n\nconst statusPill = document.getElementById('status-pill')\nconst lookaheadInput = document.getElementById('lookahead')\nconst thresholdInput = document.getElementById('threshold')\nconst fromInput = document.getElementById('from')\nconst toInput = document.getElementById('to')\nconst zoek = document.getElementById('zoek')\nconst futureTbody = document.querySelector('#future-table tbody')\nconst pastTbody = document.querySelector('#past-table tbody')\nconst lowList = document.getElementById('low-list')\nconst futureCountEl = document.getElementById('future-count')\nconst pastCountEl = document.getElementById('past-count')\nconst rowInfo = document.getElementById('row-info')\ndocument.getElementById('sheetLink').href = `https:\/\/docs.google.com\/spreadsheets\/d\/${SHEET_ID}\/edit#gid=${GID}`\n\nfunction pad2(n){ return String(n).padStart(2,'0') }\nfunction dstr(d){ return `${pad2(d.getDate())}-${pad2(d.getMonth()+1)}-${d.getFullYear()}` }\nfunction 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(+m[1], +m[2], +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}\nfunction jsonpGviz(sheetId, gid){\n  return new Promise((resolve, reject)=>{\n    const prev = window.google?.visualization?.Query?.setResponse\n    window.google = window.google || {}; google.visualization = google.visualization || {}; google.visualization.Query = google.visualization.Query || {}\n    google.visualization.Query.setResponse = (payload)=>{ cleanup(); resolve(payload) }\n    const s = document.createElement('script')\n    s.src = `https:\/\/docs.google.com\/spreadsheets\/d\/${sheetId}\/gviz\/tq?gid=${gid}&tqx=out:json&cache=${Date.now()}`\n    s.async = true\n    s.onerror = ()=>{ cleanup(); reject(new Error('JSONP geblokkeerd of sheet niet gedeeld')) }\n    document.head.appendChild(s)\n    function cleanup(){ if(s && s.parentNode) s.parentNode.removeChild(s); if(prev){ google.visualization.Query.setResponse = prev } }\n  })\n}\nasync function loadViaCSV(){\n  if (typeof PUB_CSV_URL === 'undefined') throw new Error('Geen CSV-URL ingesteld')\n  const res = await fetch(PUB_CSV_URL, { credentials:'omit' })\n  if(!res.ok) throw new Error('CSV fetch mislukt')\n  const text = await res.text()\n  const rows = text.split(\/\\r?\\n\/).filter(Boolean).map(line=>{\n    \/\/ simpele CSV split \u2013 voldoet zolang velden geen gequote komma\u2019s bevatten\n    return line.split(',')\n  })\n  const headers = rows.shift()\n  const col = Object.fromEntries(headers.map((h,i)=>[h.trim().toLowerCase(), i]))\n  function get(r, name){ const i = col[name.toLowerCase()] ?? -1; return i>=0 ? r[i] : '' }\n  \/\/ Verwacht: A=Datum, D=Dag, E.. = namen\n  const out = []\n  for(const r of rows){\n    const dt = parseNLDate(get(r,'datum'))\n    if(!(dt instanceof Date) || isNaN(dt)) continue\n    const dag = get(r,'dag') || ''\n    const present = []\n    for(let i=4;i<r.length;i++){\n      const val = (r[i]||'').trim()\n      if(val) present.push({naam: headers[i], shift: val})\n    }\n    out.push({date:dt, dag, present})\n  }\n  return out\n}\n\n\/\/ STATE\nconst state = { rows:[], per:{}, names:[], minDate:null, maxDate:null }\n\nasync function load(){\n  try{\n    statusPill.textContent = 'Laden\u2026'\n    \/\/ 1) Probeer JSONP (geeft meestal ook verborgen rijen mee, tenzij er een basisfilter actief is)\n    const json = await jsonpGviz(SHEET_ID, GID)\n    const cols = json.table?.cols || []\n    const dataRows = json.table?.rows || []\n    let rows = []\n    for(const r of dataRows){\n      const c = r.c || []\n      const rawDate = (c[0] &#038;&#038; (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      const dagVal = (c[3] &#038;&#038; (c[3].f ?? c[3].v)) || ''\n      const dag = String(dagVal||'').trim()\n      const present=[]\n      for(let i=4;i<cols.length;i++){\n        const naam = 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) present.push({naam, shift})\n      }\n      rows.push({date:dt, dag, present})\n    }\n    if(rows.length===0) throw new Error('Geen rijen via gviz (mogelijk filter actief)')\n    state.rows = rows.sort((a,b)=>a.date-b.date)\n  }catch(e){\n    console.warn('[Inzetanalyse] JSONP faalde of leeg:', e.message)\n    \/\/ 2) CSV-fallback \u2013 pakt ALLES, incl. verborgen rijen en negeert filter views\n    state.rows = await loadViaCSV()\n    state.rows.sort((a,b)=>a.date-b.date)\n  }\n\n    \/\/ Bouw indexen\n    const per = {}\n    const set = new Set()\n    for(const r of state.rows){\n      for(const p of r.present){\n        set.add(p.naam)\n        ;(per[p.naam] = per[p.naam] || []).push({date:r.date, shift:p.shift})\n      }\n    }\n    state.per = per\n    state.names = Array.from(set).sort((a,b)=>a.localeCompare(b,'nl'))\n    state.minDate = state.rows[0]?.date || null\n    state.maxDate = state.rows[state.rows.length-1]?.date || null\n\n    \/\/ Historie standaard: alles t\/m vandaag\n    const today = new Date(); today.setHours(0,0,0,0)\n    const from = state.minDate || today\n    fromInput.value = `${from.getFullYear()}-${pad2(from.getMonth()+1)}-${pad2(from.getDate())}`\n    toInput.value   = `${today.getFullYear()}-${pad2(today.getMonth()+1)}-${pad2(today.getDate())}`\n\n    statusPill.textContent = `\u2713 ${state.rows.length} dagen, ${state.names.length} personen`\n    rowInfo.textContent = `Bron: ${SHEET_ID} \u00b7 gid=${GID}`\n    render()\n}\n\nfunction render(){\n  const lookahead = Math.max(1, Number(lookaheadInput.value||8))\n  const threshold = Math.max(0, Number(thresholdInput.value||2))\n  const q = (zoek.value||'').trim().toLowerCase()\n  const names = state.names.filter(n => !q || n.toLowerCase().includes(q))\n\n  const today = new Date(); today.setHours(0,0,0,0)\n  const futureEnd = new Date(today); futureEnd.setDate(futureEnd.getDate() + lookahead*7)\n\n  \/\/ === KOMENDE\n  const futureRows=[]\n  const low=[]\n  for(const name of names){\n    const items = (state.per[name]||[]).filter(e => e.date >= today && e.date <= futureEnd).sort((a,b)=>a.date-b.date)\n    const count = items.length\n    const first = items[0]?.date ? dstr(items[0].date) : '\u2014'\n    const dates = items.map(e=>dstr(e.date))\n    if(count<=threshold) low.push({name,count})\n    futureRows.push({name,count,first,dates})\n  }\n  futureRows.sort((a,b)=> a.count - b.count || a.name.localeCompare(b.name,'nl'))\n  futureTbody.innerHTML = futureRows.map(r=>{\n    const badgeCls = r.count===0 ? 'chip danger' : (r.count<=threshold ? 'chip warn' : 'chip ok')\n    return `<tr><td>${r.name}<\/td><td><span class=\"${badgeCls}\">${r.count}<\/span><\/td><td>${r.first}<\/td><td>${r.dates.map(d=>`<span class=\"chip\">${d}<\/span>`).join('')}<\/td><\/tr>`\n  }).join('')\n  futureCountEl.textContent = `${futureRows.length} personen binnen ${lookahead} weken`\n  low.sort((a,b)=> a.count - b.count || a.name.localeCompare(b.name,'nl'))\n  lowList.innerHTML = low.map(p=>`<span class=\"chip ${p.count===0?'danger':'warn'}\">${p.name}: ${p.count}<\/span>`).join('')\n\n  \/\/ === HISTORIE\n  const from = fromInput.value ? new Date(fromInput.value) : state.minDate || today\n  const to   = toInput.value ? new Date(toInput.value) : today\n  const pastRows=[]\n  for(const name of names){\n    const items = (state.per[name]||[]).filter(e => e.date >= from && e.date <= to &#038;&#038; e.date <= today).sort((a,b)=>a.date-b.date)\n    pastRows.push({name, count: items.length, dates: items.map(e=>dstr(e.date))})\n  }\n  pastRows.sort((a,b)=> b.count - a.count || a.name.localeCompare(b.name,'nl'))\n  pastTbody.innerHTML = pastRows.map(r=>`<tr><td>${r.name}<\/td><td>${r.count}<\/td><td>${r.dates.map(d=>`<span class=\"chip\">${d}<\/span>`).join('')}<\/td><\/tr>`).join('')\n  pastCountEl.textContent = `${pastRows.length} personen (${dstr(from)} t\/m ${dstr(to)})`\n}\n\n\/\/ events\nlookaheadInput.addEventListener('input', render)\nthresholdInput.addEventListener('input', render)\nfromInput.addEventListener('change', render)\ntoInput.addEventListener('change', render)\nzoek.addEventListener('input', render)\n\nload()\n<\/script>\n<\/body>\n<\/html>\n\n","protected":false},"excerpt":{"rendered":"<p>Inzetanalyse Inzetanalyse Komende periode bovenaan, historie daaronder \ud83c\udf17 Licht \/ Donker Laden\u2026 Komende (weken) Drempel \u2264 Historie vanaf t\/m Open sheet Komende periode \u2013 Weinig ingepland (\u2264 drempel) Naam Ingepland Eerstvolgende Datums (binnen periode) Historie \u2013 Naam Totaal Datums<\/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-39","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/ernst.bureaukiem.nl\/index.php?rest_route=\/wp\/v2\/pages\/39","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=39"}],"version-history":[{"count":6,"href":"https:\/\/ernst.bureaukiem.nl\/index.php?rest_route=\/wp\/v2\/pages\/39\/revisions"}],"predecessor-version":[{"id":45,"href":"https:\/\/ernst.bureaukiem.nl\/index.php?rest_route=\/wp\/v2\/pages\/39\/revisions\/45"}],"wp:attachment":[{"href":"https:\/\/ernst.bureaukiem.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=39"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}