W5H Command Center logo
W5H Command Center3e Peloton NWT
W5H COMMAND CENTER V16 3e PELOTON NWT
Zelfstandig • lokaal • militaire formulierbouwer
Gereed
Werken op je laptop: vul links in → bekijk rechts → sla op als bestand → druk af of kies PDF → deel de PDF via WhatsApp Web. Nog niet als bestand opgeslagen

Formulier invullen • originele W5H-indeling

Links vul je de onderdelen in. Rechts zie je direct de W5H in de herkenbare opmaak van het oorspronkelijke formulier. Bij een nieuwe W5H blijven de vaste standaardteksten staan. Controleer ze vóór uitgifte. Het logo wordt in de lokale back-up opgeslagen.

');w.document.close();w.focus();setTimeout(()=>w.print(),250)}function refreshSafety(){document.getElementById('v7safetySelect').innerHTML=safetyBlocks.map((x,i)=>'').join('')}function saveSafety(){const name=document.getElementById('v7safetyName').value.trim(),body=document.getElementById('v7safetyText').value.trim();if(!name||!body){alert('Vul naam en tekst in');return}safetyBlocks.push({name,body});try{localStorage.setItem(safetyKey,JSON.stringify(safetyBlocks))}catch(e){alert('Opslaan mislukt')}refreshSafety()}function insertSafety(){const x=safetyBlocks[Number(document.getElementById('v7safetySelect').value)];if(!x)return;d.coord=[d.coord,x.name+': '+x.body].filter(Boolean).join('\n');renderForm();renderPreview()}function issueDocument(){if(!confirm('Registreer deze W5H als uitgegeven? Controleer de inhoud handmatig vóór delen.'))return;const entry={title:d.titel||'Naamloos',date:new Date().toISOString(),version:d.versie||'1',filename:fileBase()+'.pdf'};let a=[];try{a=JSON.parse(localStorage.getItem(issueKey)||'[]')}catch(e){}a.unshift(entry);localStorage.setItem(issueKey,JSON.stringify(a));d.docStatus='Definitief';save();showIssues()}function showIssues(){let a=[];try{a=JSON.parse(localStorage.getItem(issueKey)||'[]')}catch(e){}document.getElementById('v7issues').innerHTML=a.map(x=>'

'+esc(x.date.slice(0,16).replace('T',' '))+' – '+esc(x.title)+' ('+esc(x.filename)+')

').join('')||'Nog geen uitgiftes'}function compareVersion(){const old=(d.history||[]).at(-1)?.data;if(!old){document.getElementById('v7diff').textContent='Leg eerst een versie vast via Werkcentrum.';return}const keys=[...new Set([...Object.keys(old),...Object.keys(d)])].filter(k=>!k.startsWith('_')&&k!=='history'&&k!=='logo'&&k!=='attachments');const diff=keys.filter(k=>JSON.stringify(old[k])!==JSON.stringify(d[k]));document.getElementById('v7diff').innerHTML=diff.length?diff.map(k=>'

'+esc(k)+'
'+esc(JSON.stringify(old[k]??'').slice(0,180))+'
'+esc(JSON.stringify(d[k]??'').slice(0,180))+'

').join(''):'Geen verschillen gevonden'}function downloadNamed(){const a=document.createElement('a'),url=URL.createObjectURL(new Blob([JSON.stringify(d,null,2)],{type:'application/json'}));a.href=url;a.download=fileBase()+'.json';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)}function printNamed(){document.title=fileBase();window.print()}let diskHandle=null;function diskFilename(){return (typeof fileBase==='function'?fileBase():'W5H_3e_Peloton_'+(d.titel||'Nieuw')).replace(/[^a-zA-Z0-9_-]/g,'_')+'.json'}async function saveToDisk(){try{const data=JSON.stringify({type:'w5h-3pel-document',version:9,document:d},null,2);if(window.showSaveFilePicker){diskHandle=await window.showSaveFilePicker({suggestedName:diskFilename(),types:[{description:'W5H-document',accept:{'application/json':['.json']}}]});const stream=await diskHandle.createWritable();await stream.write(data);await stream.close();}else{const a=document.createElement('a'),u=URL.createObjectURL(new Blob([data],{type:'application/json'}));a.href=u;a.download=diskFilename();a.click();setTimeout(()=>URL.revokeObjectURL(u),1500)}document.getElementById('diskState').textContent='Bestand opgeslagen';save()}catch(e){if(e.name!=='AbortError')alert('Opslaan als bestand mislukt: '+e.message)}}async function openFromDisk(){try{let f;if(window.showOpenFilePicker){const handles=await window.showOpenFilePicker({multiple:false,types:[{description:'W5H-document',accept:{'application/json':['.json']}}]});diskHandle=handles[0];f=await diskHandle.getFile()}else{const el=document.createElement('input');el.type='file';el.accept='.json';el.onchange=()=>{if(el.files[0])readDiskFile(el.files[0])};el.click();return}await readDiskFile(f)}catch(e){if(e.name!=='AbortError')alert('Openen mislukt: '+e.message)}}async function readDiskFile(f){try{const obj=JSON.parse(await f.text());if(obj.type!=='w5h-3pel-document'||!obj.document||!Array.isArray(obj.document.rows))throw Error('Dit is geen geldig W5H V9-document');if(!confirm('Huidige formulier vervangen door het gekozen bestand? Sla wijzigingen eerst op.'))return;d={...blank(),...obj.document};renderForm();renderPreview();document.getElementById('diskState').textContent='Geopend: '+f.name;status('W5H-bestand geopend')}catch(e){alert(e.message)}}function logoFile(ev){let f=ev.target.files[0];if(!f)return;if(f.size>2000000){alert('Kies een logo kleiner dan 2 MB');return}let r=new FileReader();r.onload=()=>{d.logo=r.result;renderPreview();status('Logo in huidige W5H geladen; bewaar via Standaardbibliotheek voor nieuwe formulieren')};r.readAsDataURL(f)}try{standards={...defaultStandards(),...JSON.parse(localStorage.getItem(stdKey)||'{}')}}catch(e){standards=defaultStandards()}try{d={...blank(),...JSON.parse(localStorage.getItem(key)||'{}')}}catch(e){d=blank()}try{extra={...extra,...JSON.parse(localStorage.getItem(extraKey)||'{}')}}catch(e){}renderForm();renderPreview();try{const draft=JSON.parse(localStorage.getItem(draftKey)||'null');if(draft&&draft.data&&JSON.stringify(draft.data)!==JSON.stringify(d)){status('Er is een automatisch bewaard concept. Klik op Concept herstellen om het te openen.')}}catch(e){} let editingPoint=null; function openPoints(){document.getElementById('pointsPanel').style.display='block';pointsRender();document.getElementById('pointsPanel').scrollIntoView({behavior:'smooth'})} function pointClear(){editingPoint=null;for(let id of ['ptName','ptCoord','ptNotes','ptLat','ptLon'])document.getElementById(id).value='';document.getElementById('ptType').value='Logistiek'} function pointSave(){const name=document.getElementById('ptName').value.trim();if(!name){alert('Vul een puntnaam in');return}d.points=d.points||[];let pt=editingPoint===null?{id:'pt-'+Date.now()+'-'+Math.floor(Math.random()*100000)}:d.points.find(x=>x.id===editingPoint);if(!pt){editingPoint=null;return}Object.assign(pt,{name,type:document.getElementById('ptType').value,coord:document.getElementById('ptCoord').value.trim(),notes:document.getElementById('ptNotes').value.trim()});const latRaw=document.getElementById('ptLat').value.trim().replace(',','.');const lonRaw=document.getElementById('ptLon').value.trim().replace(',','.');if(latRaw||lonRaw){const lat=Number(latRaw),lon=Number(lonRaw);if(!latRaw||!lonRaw||!Number.isFinite(lat)||!Number.isFinite(lon)||Math.abs(lat)>90||Math.abs(lon)>180){alert('Voer geldige GPS-breedte- én lengtegraden in (WGS84).');return}pt.lat=lat;pt.lon=lon}else{delete pt.lat;delete pt.lon}if(editingPoint===null)d.points.push(pt);pointClear();pointsRender();renderPreview();saveDraftNow()} function pointEdit(id){const pt=(d.points||[]).find(x=>x.id===id);if(!pt)return;editingPoint=id;document.getElementById('ptName').value=pt.name;document.getElementById('ptType').value=pt.type;document.getElementById('ptCoord').value=pt.coord||'';document.getElementById('ptNotes').value=pt.notes||'';document.getElementById('ptLat').value=pt.lat??'';document.getElementById('ptLon').value=pt.lon??''} function pointDelete(id){if(!confirm('Dit punt verwijderen?'))return;d.points=(d.points||[]).filter(x=>x.id!==id);pointsRender();renderPreview();saveDraftNow()} function pointsRender(){const pts=d.points||[];const list=document.getElementById('pointsList');list.innerHTML='';pts.forEach(pt=>{let div=document.createElement('div');div.className='pt-entry';let label=document.createElement('div');label.textContent=pt.name+' • '+pt.type+' • '+(pt.coord||'Geen coördinaten');div.appendChild(label);let edit=document.createElement('button');edit.textContent='Bewerken';edit.onclick=()=>pointEdit(pt.id);div.appendChild(edit);let del=document.createElement('button');del.textContent='Verwijderen';del.onclick=()=>pointDelete(pt.id);div.appendChild(del);list.appendChild(div)});if(!pts.length)list.textContent='Nog geen punten ingevoerd';let select=document.getElementById('pointSelect');select.replaceChildren();pts.forEach(pt=>{let opt=document.createElement('option');opt.value=pt.id;opt.textContent=pt.name;select.appendChild(opt)});let map=document.getElementById('pointMap');map.replaceChildren();if(d.pointMap){let img=document.createElement('img');img.src=d.pointMap;img.style.cssText='width:100%;display:block';map.appendChild(img);pts.filter(pt=>pt.x!=null&&pt.y!=null).forEach(pt=>{let dot=document.createElement('span');dot.className='pt-dot';dot.style.left=pt.x+'%';dot.style.top=pt.y+'%';dot.textContent=pt.name;map.appendChild(dot)})}else{map.textContent='Voeg eerst een kaartafbeelding toe'}} function pointMapClick(ev){if(!d.pointMap)return;const id=document.getElementById('pointSelect').value,pt=(d.points||[]).find(x=>x.id===id);if(!pt){alert('Maak eerst een punt aan');return}const r=ev.currentTarget.getBoundingClientRect();pt.x=Math.max(0,Math.min(100,Math.round((ev.clientX-r.left)/r.width*1000)/10));pt.y=Math.max(0,Math.min(100,Math.round((ev.clientY-r.top)/r.height*1000)/10));pointsRender();renderPreview();saveDraftNow()} function pointMapUpload(ev){let f=ev.target.files[0];if(!f)return;if(f.size>1500000){alert('Kaart is te groot voor lokale opslag (max 1,5 MB). Exporteer een kleinere afbeelding.');return}const r=new FileReader();r.onload=()=>{d.pointMap=r.result;pointsRender();renderPreview();saveDraftNow()};r.readAsDataURL(f)} function pointRemoveMap(){if(!confirm('Kaart en visuele puntposities verwijderen?'))return;delete d.pointMap;(d.points||[]).forEach(x=>{delete x.x;delete x.y});pointsRender();renderPreview();saveDraftNow()} function pointsCsv(){let rows=[['Naam','Type','Coordinaten/MGRS','Breedtegraad WGS84','Lengtegraad WGS84','Bijzonderheden'],...(d.points||[]).map(x=>[x.name,x.type,x.coord||'',x.lat??'',x.lon??'',x.notes||''])];let csv=rows.map(r=>r.map(v=>'"'+String(v).replace(/"/g,'""')+'"').join(';')).join('\r\n');let a=document.createElement('a'),u=URL.createObjectURL(new Blob(['\ufeff'+csv],{type:'text/csv;charset=utf-8'}));a.href=u;a.download='W5H_3e_Peloton_Punten.csv';a.click();setTimeout(()=>URL.revokeObjectURL(u),1000)} function pointsToText(){let txt=(d.points||[]).map(x=>[x.name,x.type,x.coord||'Coördinaten nog in te vullen',x.notes||''].join(' | ')).join('\n');if(!txt)return;d.bijlagen=[d.bijlagen||'','PUNTENLIJST 3e PELOTON\n'+txt].filter(Boolean).join('\n\n');renderForm();renderPreview();saveDraftNow()} function gpxXmlEscape(value){return String(value??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))} function validGps(lat,lon){return Number.isFinite(lat)&&Number.isFinite(lon)&&Math.abs(lat)<=90&&Math.abs(lon)<=180} function pointsExportGpx(){ const pts=(d.points||[]).filter(p=>validGps(p.lat,p.lon)); const skipped=(d.points||[]).length-pts.length; if(!pts.length){alert('Geen punten met geldige WGS84-breedte- en lengtegraden. Vul die eerst in.');return} const waypoints=pts.map(p=>''+gpxXmlEscape(p.name)+''+gpxXmlEscape([p.type,p.coord,p.notes].filter(Boolean).join(' | '))+''+gpxXmlEscape(p.type)+'').join('\n'); const routes=(d.gpxRoutes||[]).filter(r=>Array.isArray(r.points)&&r.points.length>=2).map(r=>''+gpxXmlEscape(r.name)+''+r.points.filter(p=>validGps(p.lat,p.lon)).map(p=>''+gpxXmlEscape(p.name||'')+'').join('')+'').join('\n'); const xml='\n\n'+waypoints+'\n'+routes+'\n'; const url=URL.createObjectURL(new Blob([xml],{type:'application/gpx+xml;charset=utf-8'})),a=document.createElement('a');a.href=url;a.download='W5H_3e_Peloton_Punten_Routes.gpx';a.click();setTimeout(()=>URL.revokeObjectURL(url),2000); document.getElementById('gpxStatus').textContent=pts.length+' punten en '+(d.gpxRoutes||[]).length+' routes geëxporteerd. '+(skipped?skipped+' punten zonder GPS-coördinaten overgeslagen.':''); } function gpxChildren(node,local){return [...node.children].filter(n=>n.localName===local)} function gpxFirstText(node,local){const n=gpxChildren(node,local)[0];return n?n.textContent.trim():''} function gpxPoint(node){const lat=Number(node.getAttribute('lat')),lon=Number(node.getAttribute('lon'));if(!node.hasAttribute('lat')||!node.hasAttribute('lon')||!validGps(lat,lon))return null;return {lat,lon,name:gpxFirstText(node,'name')}} function pointsImportGpx(ev){ const file=ev.target.files?.[0];if(!file)return; if(file.size>5_000_000){alert('GPX-bestand te groot (maximaal 5 MB).');ev.target.value='';return} const reader=new FileReader();reader.onload=()=>{ try{ const xml=new DOMParser().parseFromString(reader.result,'application/xml'); if(xml.querySelector('parsererror')||xml.documentElement.localName!=='gpx')throw Error('Geen geldig GPX-bestand'); const nodes=[...xml.documentElement.children]; const wpts=nodes.filter(n=>n.localName==='wpt').map(gpxPoint).filter(Boolean); const routes=nodes.filter(n=>n.localName==='rte'||n.localName==='trk').map((node,i)=>{ const segments=node.localName==='rte'?gpxChildren(node,'rtept'):gpxChildren(node,'trkseg').flatMap(seg=>gpxChildren(seg,'trkpt')); return {name:gpxFirstText(node,'name')||'Route '+(i+1),points:segments.map(gpxPoint).filter(Boolean)} }).filter(r=>r.points.length>0); if(!wpts.length&&!routes.length)throw Error('Geen bruikbare waypoints of routes gevonden'); const promptText=wpts.length+' waypoint(s) en '+routes.length+' route(s)/track(s) gevonden. Toevoegen aan deze W5H? Bestaande gegevens blijven staan.'; if(!confirm(promptText))return; d.points=d.points||[];d.gpxRoutes=d.gpxRoutes||[]; wpts.forEach((p,i)=>d.points.push({id:'gpx-'+Date.now()+'-'+i+'-'+Math.random().toString(36).slice(2,7),name:p.name||'Waypoint '+(i+1),type:'Overig',coord:'',notes:'Geïmporteerd uit '+file.name,lat:p.lat,lon:p.lon})); d.gpxRoutes.push(...routes); pointsRender();renderPreview();saveDraftNow(); document.getElementById('gpxStatus').textContent=wpts.length+' waypoint(s) en '+routes.length+' route(s) geïmporteerd. Routes zijn opgeslagen in het W5H-bestand; kaartpositie op een losse afbeelding moet je nog handmatig aanwijzen.'; }catch(e){alert('GPX importeren mislukt: '+e.message)} finally{ev.target.value=''} };reader.onerror=()=>alert('GPX-bestand lezen mislukt');reader.readAsText(file); } const V12_TABS=[['dashboard','Overzicht'],['smart','Slimme assistent'],['intelligence','Intelligentie V14'],['builder','Oefenbouwer'],['map','Kaartmanager'],['log','Logistiek'],['rhythm','Battle rhythm'],['tasks','Taakkaarten'],['checks','Controle'],['compare','Vergelijken'],['favorites','Favorieten'],['issue','Uitgifte']]; let v12tab='dashboard',v12compare=0; function openV12(tab='dashboard'){document.getElementById('v12Panel').style.display='block';v12Show(tab);document.getElementById('v12Panel').scrollIntoView({behavior:'smooth'})} function v12Esc(s){return esc(s)} function v12Show(tab){v12tab=tab;document.getElementById('v12Nav').innerHTML=V12_TABS.map(([id,label])=>'').join('');const e=document.getElementById('v12Screen');let h=''; const rows=d.rows||[]; if(tab==='dashboard'){ h='

Oefeningsoverzicht

'+[['Document',d.titel||'Nog geen titel'],['Activiteiten',rows.length],['Kaartpunten',(d.points||[]).length],['Status',d.docStatus||'Concept']].map(([k,v])=>'

'+k+'

'+v12Esc(v)+'
').join('')+'

Vul links het W5H-formulier in of werk vanuit de modules. Wijzigingen worden opgenomen in de documentweergave.

'; } if(tab==='smart'){ const report=v13Audit(), percent=Math.round(100*(report.total-report.missing)/report.total); h='

Slimme oefenassistent

Voorbereidingsgraad

'+percent+'%

Indicatie van ingevulde basisvelden; geen formele goedkeuring.

Snelle acties

Voorbereidingschecklist

'+report.items.map(x=>'').join('')+'
OnderdeelStatus
'+v12Esc(x.label)+''+(x.ok?'✓ Ingevuld':'⚠ Nog controleren')+'

Uitgiftecontrole

Vink de aanvullende controles zelf af voordat je de W5H verspreidt.

'+v13Checks.map((x,i)=>'').join('')+'

Geen automatische inhoudelijke goedkeuring. Controleer route, logistiek, veiligheid en actuele informatie zelf.

'; } if(tab==='builder'){ h='

Visuele oefenbouwer

Vul activiteiten hieronder aan. Gebruik ↑ en ↓ om de volgorde aan te passen; sorteer op tijd via Werkcentrum.

'+rows.map((r,i)=>'
'+v12Esc(r.tijd||'--:--')+'
'+v12Esc(r.groep||'Alle groepen')+' — '+v12Esc(r.actie||'Nieuwe activiteit')+'
'+v12Esc(r.locatie||'Locatie nog invullen')+' · '+v12Esc(r.verantwoordelijke||'Personeel nog invullen')+' · '+v12Esc(r.einde||'--:--')+'
').join('')+'

Activiteit toevoegen

'; } if(tab==='map'){h='

Slimme kaartmanager

Kaartpunten

'+(d.points||[]).length+' punten

LOG 3e PEL, DELTA, medische posten en eigen punten. Coördinaten en Topo GPS-GPX beheren.

Geïmporteerde routes

'+(d.gpxRoutes||[]).length+' routes

'+v12Esc((d.gpxRoutes||[]).map(x=>x.name).join(', ')||'Nog geen routes')+'

Kaartafbeeldingen zijn niet automatisch gegeorefereerd; plaatsing van markeringen op afbeeldingen blijft handmatig.

'} if(tab==='log'){ const L=d.v12Log||{};h='

Logistiek overzicht

'+[['personeel','Personeel / bezetting'],['transport','Voertuigen / transport'],['water','Water / voeding'],['materieel','Materieel'],['delta','LOG 3e PEL / DELTA'],['contact','Contact / bijzonderheden']].map(([id,label])=>'
').join('')+'
'; } if(tab==='rhythm'){ const rhythm=d.v12Rhythm||[];h='

Battle rhythm

Eigen vaste dagmomenten, zonder automatisch instructeursuren te tellen.

'+rhythm.map((r,i)=>'').join('')+'
TijdMomentActie
'+v12Esc(r.time)+''+v12Esc(r.name)+'
'; } if(tab==='tasks'){ const groups=['Alpha','Bravo','Charlie'];h='

Taakkaarten

Genereer een afzonderlijke afdruk voor elke groep, op basis van de huidige planning.

'+groups.map(g=>'').join('')+'
'+groups.map(g=>'

'+g+'

'+rows.filter(r=>r.groep===g||r.groep==='Alle groepen').map(r=>'

'+v12Esc(r.tijd||'--:--')+' '+v12Esc(r.actie||'')+'
'+v12Esc(r.locatie||'')+' · '+v12Esc(r.verantwoordelijke||'')+'

').join('')+'
').join('')+'
'; } if(tab==='checks'){ const issues=v12Issues();h='

Controle op conflicten en ontbrekende gegevens

'+issues.length+' aandachtspunt(en)

'+(issues.length?issues.map(x=>'

'+v12Esc(x)+'

').join(''):'

Geen conflicten gevonden in de gecontroleerde velden.

')+'

Automatische controles zijn hulpmiddelen en vervangen geen inhoudelijke controle.

'; } if(tab==='compare'){ const hist=d.history||[];h='

Documentvergelijking

Vergelijk het huidige document met een eerder vastgelegde versie.

'; if(hist.length){const old=hist[Math.min(v12compare,hist.length-1)].data||{};const fields=['titel','datum','wie','wat','waar','waarom','hoe','coord','logistiek','bevel','verbinding'];let diffs=fields.filter(k=>String(old[k]||'')!==String(d[k]||''));h+='

'+diffs.length+' gewijzigde tekstvelden; tijdschema: '+((old.rows||[]).length)+' → '+rows.length+' regels.

'+diffs.map(k=>'

'+k+'

Vorige versie

'+v12Esc(old[k]||'—')+'

Huidige versie

'+v12Esc(d[k]||'—')+'

').join('');}else h+='

Leg eerst een versie vast om later te vergelijken.

'; } if(tab==='favorites'){h='

Favoriete oefenopzetten

Gebruik bestaande favoriete sjablonen uit het Werkcentrum.

'+(extra.favorites||[]).map((f,i)=>'

'+v12Esc(f.name)+'

').join('')+'
'} if(tab==='issue'){h='

Uitgiftecentrum

Document

Bijlagen & punten

Instructeurs

Versies en veiligheid

WhatsApp voegt het PDF-bestand niet automatisch toe. Maak eerst de PDF en voeg die handmatig toe.

'} if(tab==='intelligence'){ const saved=v14GetLibrary(),change=v14ChangeReport(); h='

Intelligentiecentrum V14

Hulpmiddelen voor voorbereiding, hergebruik en uitgifte. Alles blijft lokaal op deze laptop.

'+ '

Logistieke rekenhulp

Vul zelf de aantallen en normen in. Geen automatische aannames over benodigde hoeveelheden.

'+v12Esc(d.v14Calculation||'Nog niet berekend')+'

'+ '

Centrale puntenbibliotheek

'+saved.length+' herbruikbare punten in deze browser.

Bestaande punten in de W5H worden niet overschreven. Dubbele namen worden overgeslagen.

'+ '

Wijzigingen sinds laatste uitgifte

'+v12Esc(change)+'

Markeer pas na feitelijke uitgifte. Dit verstuurt niets.

'+ '

Verder werken en back-up

Gebruik regelmatig een JSON-bestand buiten de browser. Browseropslag kan gewist worden.

'; } e.innerHTML=h; } function v12Persist(){renderForm();renderPreview();markDraft();saveDraftNow()} function v12AddRow(){const g=id=>document.getElementById(id).value;const row={groep:g('v12Group'),tijd:g('v12Start'),einde:g('v12End'),actie:g('v12Action'),locatie:g('v12Loc'),verantwoordelijke:g('v12Lead')};if(!row.actie.trim()){alert('Vul een activiteit in');return}d.rows.push(row);v12Persist();v12Show('builder')} function v12Move(i,dir){const j=i+dir;if(j<0||j>=d.rows.length)return;[d.rows[i],d.rows[j]]=[d.rows[j],d.rows[i]];v12Persist();v12Show('builder')} function v12EditRow(i){document.getElementById('v12Panel').style.display='none';document.getElementById('form').scrollIntoView({behavior:'smooth'});const el=document.querySelector('[data-row="'+i+'"]');if(el)el.focus()} function v12RemoveRow(i){if(!confirm('Activiteit verwijderen?'))return;d.rows.splice(i,1);v12Persist();v12Show('builder')} function v12SaveLog(){d.v12Log={};for(const id of ['personeel','transport','water','materieel','delta','contact'])d.v12Log[id]=document.getElementById('v12log_'+id).value;v12Persist();v12Show('log')} function v12AddRhythm(){const time=document.getElementById('v12RTime').value,name=document.getElementById('v12RName').value.trim();if(!time||!name){alert('Vul tijd en moment in');return}d.v12Rhythm=d.v12Rhythm||[];d.v12Rhythm.push({time,name});d.v12Rhythm.sort((a,b)=>a.time.localeCompare(b.time));v12Persist();v12Show('rhythm')} function v12DeleteRhythm(i){d.v12Rhythm.splice(i,1);v12Persist();v12Show('rhythm')} function v12DefaultRhythm(){d.v12Rhythm=d.v12Rhythm||[];for(const [time,name] of [['07:00','Ontbijt'],['11:45','Lunch'],['17:00','Diner']])if(!d.v12Rhythm.some(r=>r.time===time&&r.name===name))d.v12Rhythm.push({time,name});d.v12Rhythm.sort((a,b)=>a.time.localeCompare(b.time));v12Persist();v12Show('rhythm')} function v12RhythmRows(){const a=d.v12Rhythm||[];if(!a.length){alert('Voeg eerst momenten toe');return}for(const r of a)if(!d.rows.some(x=>x.tijd===r.time&&x.actie===r.name))d.rows.push({groep:'Alle groepen',tijd:r.time,einde:'',actie:r.name,locatie:'',verantwoordelijke:''});v12Persist();v12Show('builder')} function v12Issues(){let issues=[];if(!d.titel)issues.push('Documenttitel ontbreekt');if(!d.datum)issues.push('Uitvoeringsdatum ontbreekt');if(!(d.points||[]).length)issues.push('Nog geen kaartpunten ingevoerd (indien relevant)');const rows=d.rows||[];rows.forEach((r,i)=>{if(!r.actie)issues.push('Regel '+(i+1)+': activiteit ontbreekt');if(r.tijd&&r.einde&&r.einde<=r.tijd)issues.push('Regel '+(i+1)+': eindtijd is niet later dan starttijd');if(r.actie&&(!r.locatie||!r.verantwoordelijke))issues.push('Regel '+(i+1)+': locatie of verantwoordelijke ontbreekt')});const toMin=s=>/^\d{2}:\d{2}$/.test(s||'')?Number(s.slice(0,2))*60+Number(s.slice(3)):null;for(let i=0;ix===null)||!(asr.groep===group||r.groep==='Alle groepen');const w=window.open('','_blank');if(!w){alert('Sta pop-ups toe om een taakkaart af te drukken');return}const tr=rs.map(r=>''+['tijd','einde','actie','locatie','verantwoordelijke'].map(k=>''+v12Esc(r[k]||'')+'').join('')+'').join('');w.document.write('Taakkaart '+group+'

3e Peloton • Taakkaart '+group+'

Oefening: '+v12Esc(d.titel||'')+'
Datum: '+v12Esc(d.datum||'')+'

'+tr+'
StartEindeActiviteitLocatieVerantwoordelijke

Controleer gegevens vóór uitgifte.

');w.document.close();w.focus();setTimeout(()=>w.print(),350)} const _v12Preview=renderPreview; renderPreview=function(){_v12Preview();const paper=document.getElementById('preview');if(!paper)return;const L=d.v12Log||{},r=d.v12Rhythm||[];let addition='';if(Object.values(L).some(Boolean)){addition+='

Logistiek overzicht 3e Peloton

'+[['personeel','Personeel'],['transport','Transport'],['water','Voeding en water'],['materieel','Materieel'],['delta','LOG 3e PEL / DELTA'],['contact','Contact']].filter(([id])=>L[id]).map(([id,label])=>'').join('')+'
'+label+''+esc(L[id])+'
'}if(r.length){addition+='

Battle rhythm

'+r.map(x=>'').join('')+'
TijdMoment
'+esc(x.time)+''+esc(x.name)+'
'}paper.insertAdjacentHTML('beforeend',addition)} const v13Checks=['Inhoud en doel van de oefening gecontroleerd','Tijden en groepsindeling afgestemd','Locaties en coördinaten gecontroleerd op de bronkaart','Logistieke voorzieningen en bereikbaarheid bevestigd','Geneeskundige informatie en contactgegevens gecontroleerd','Bijlagen, kaartversies en uitgifteontvangers gecontroleerd']; function v13Audit(){const fields=[['Titel',d.titel],['Uitvoeringsdatum',d.datum],['Opsteller',d.opsteller],['Wie',d.wie],['Wat',d.wat],['Waar',d.waar],['Waarom',d.waarom],['Hoe',d.hoe],['Coördinerende bepalingen',d.coord],['Logistiek plan',d.logplan],['Geneeskundige spoedzorg',d.spoed],['Bevelvoering',d.bevel],['Verbindingen',d.verbinding],['Tijdschema',(d.rows||[]).length],['Kaartpunten',(d.points||[]).length]];const items=fields.map(([label,val])=>({label,ok:typeof val==='number'?val>0:!!String(val||'').trim()}));return {items,total:items.length,missing:items.filter(x=>!x.ok).length}} function v13Check(i,checked){d.v13Checks=d.v13Checks||{};d.v13Checks[i]=checked;markDraft()} function v13QuickTitle(){if(d.titel&& !confirm('Bestaande titel vervangen door een voorstel?'))return;const a=(d.rows||[]).find(x=>x.actie&&x.actie.trim());const proposed=[a?a.actie.trim():'Oefening',d.week?'week '+d.week:'',d.datum||''].filter(Boolean).join(' – ');d.titel=proposed;v12Persist();v12Show('smart')} function v13FillLocations(){const pts=d.points||[];let changed=0;for(const row of d.rows||[]){if(row.locatie&&row.locatie.trim())continue;const matches=pts.filter(p=>p.name&&[row.actie,row.groep].some(t=>t&&String(t).toLowerCase().includes(p.name.toLowerCase())));if(matches.length===1){row.locatie=matches[0].name;changed++}}if(changed)v12Persist();alert(changed+' locatie(s) aangevuld op basis van exacte puntnamen in activiteit of groep. Overige locaties blijven leeg; er wordt niets geraden.');v12Show('smart')} function v13ExportAudit(){const audit=v13Audit();const lines=['W5H 3e Peloton – Controleverslag','Titel: '+(d.titel||'Niet ingevuld'),'Datum: '+(d.datum||'Niet ingevuld'),'Gegenereerd: '+new Date().toLocaleString('nl-NL'),'','Basisvelden:',...audit.items.map(x=>(x.ok?'[OK] ':'[OPEN] ')+x.label),'','Aanvullende controles:',...v13Checks.map((x,i)=>((d.v13Checks||{})[i]?'[AFGEVINKT] ':'[OPEN] ')+x),'','Automatische aandachtspunten:',...v12Issues().map(x=>'- '+x),'','Dit is geen formele goedkeuring.'];const blob=new Blob([lines.join('\r\n')],{type:'text/plain;charset=utf-8'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='W5H_3e_Peloton_Controleverslag.txt';a.click();setTimeout(()=>URL.revokeObjectURL(url),2000)} // V14: local-first utilities, with explicit user-controlled import/export and issuance. const V14_POINT_KEY='w5h_v14_point_library'; function v14GetLibrary(){try{const v=JSON.parse(localStorage.getItem(V14_POINT_KEY)||'[]');return Array.isArray(v)?v:[]}catch{return []}} function v14SavePoints(){const old=v14GetLibrary(),items=d.points||[];let added=0;for(const p of items){if(!p.name)continue;if(!old.some(x=>x.name.toLowerCase()===p.name.toLowerCase())){old.push({...p});added++}}try{localStorage.setItem(V14_POINT_KEY,JSON.stringify(old));alert(added+' punt(en) toegevoegd aan de bibliotheek.');v12Show('intelligence')}catch(e){alert('Opslaan mislukt. Maak een JSON-back-up; mogelijk is de browseropslag vol.')}} function v14ImportPoints(){const items=v14GetLibrary();d.points=d.points||[];let added=0;for(const p of items){if(!d.points.some(x=>x.name.toLowerCase()===p.name.toLowerCase())){d.points.push({...p,id:'lib-'+Date.now()+'-'+added});added++}}v12Persist();alert(added+' punt(en) overgenomen. Controleer of coördinaten nog actueel zijn.');v12Show('intelligence')} function v14Download(name,content,type){const url=URL.createObjectURL(new Blob([content],{type})),a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1500)} function v14ExportLibrary(){v14Download('W5H_3e_Peloton_Puntenbibliotheek.json',JSON.stringify({format:'w5h-point-library-v1',points:v14GetLibrary()},null,2),'application/json')} function v14RestoreLibrary(ev){const f=ev.target.files?.[0];if(!f)return;if(f.size>2e6){alert('Bestand te groot');return}const r=new FileReader();r.onload=()=>{try{const obj=JSON.parse(r.result);if(obj.format!=='w5h-point-library-v1'||!Array.isArray(obj.points)||obj.points.some(p=>!p||typeof p.name!=='string'||typeof p.type!=='string'))throw Error('Onjuist bibliotheekformaat');if(!confirm(obj.points.length+' punten uit dit bestand toevoegen?'))return;const old=v14GetLibrary();let n=0;for(const p of obj.points){if(!old.some(x=>x.name.toLowerCase()===p.name.toLowerCase())){old.push({name:p.name,type:p.type,coord:String(p.coord||''),notes:String(p.notes||''),...(validGps(p.lat,p.lon)?{lat:p.lat,lon:p.lon}:{})});n++}}localStorage.setItem(V14_POINT_KEY,JSON.stringify(old));alert(n+' punten toegevoegd');v12Show('intelligence')}catch(e){alert('Herstellen mislukt: '+e.message)}};r.readAsText(f);ev.target.value=''} function v14Calculate(){const ids=['v14Persons','v14Water','v14Meals'];const values=ids.map(id=>document.getElementById(id).value.trim());if(values.some(x=>x==='')){alert('Vul alle drie de waarden in.');return}const [n,w,m]=values.map(Number);if(!Number.isInteger(n)||n<0||!Number.isFinite(w)||w<0||!Number.isInteger(m)||m<0){alert('Gebruik geldige niet-negatieve waarden; deelnemers en maaltijden zijn gehele aantallen.');return}d.v14Persons=n;d.v14Water=w;d.v14Meals=m;d.v14Calculation=`${n} personen × ${w} liter = ${(n*w).toLocaleString('nl-NL',{maximumFractionDigits:2})} liter water; ${n} × ${m} = ${n*m} maaltijden.`;v12Persist();v12Show('intelligence')} function v14Snapshot(){return JSON.stringify({titel:d.titel||'',datum:d.datum||'',rows:d.rows||[],points:d.points||[],log:d.v12Log||{},rhythm:d.v12Rhythm||[],wie:d.wie||'',wat:d.wat||'',waar:d.waar||'',waarom:d.waarom||'',hoe:d.hoe||''})} function v14ChangeReport(){if(!d.v14Issued)return 'Nog geen uitgegeven versie gemarkeerd.';const old=d.v14Issued.snapshot;if(old===v14Snapshot())return 'Geen wijzigingen sinds de gemarkeerde uitgifte ('+d.v14Issued.at+').';try{const a=JSON.parse(old),b=JSON.parse(v14Snapshot());const keys=Object.keys(b).filter(k=>JSON.stringify(a[k])!==JSON.stringify(b[k]));return 'Gewijzigd sinds '+d.v14Issued.at+': '+keys.join(', ')+'. Controleer of heruitgifte nodig is.'}catch{return 'Wijzigingen vastgesteld; vergelijk de documenten handmatig.'}} function v14MarkIssued(){if(!confirm('Bevestig dat deze versie daadwerkelijk is uitgegeven. Dit verstuurt geen bestanden.'))return;d.v14Issued={at:new Date().toLocaleString('nl-NL'),snapshot:v14Snapshot()};v12Persist();v12Show('intelligence')}