NWT Oefenverloop Center logo
NWT Oefenverloop Center3e Peloton NWT

◈ NWT Oefenverloop Center

Zelfstandig oefenverloop- en formulierenbeheer
Lokaal opgeslagen
`} function v5exportHtml(){let x=current();let html=v5documentHtml(x);download('NWT_Oefenverloop_'+safeName(x.name)+'.html',html,'text/html;charset=utf-8')} const v5OldRender=render; render=function(){let x=current();if(x)v5ensure(x);v5OldRender();if(!x)return;let views={v5command:v5command,v5battle:v5battle,v5assignment:v5assignment,v5document:v5doc,v5archive:v5archive};if(views[tab])$('app').innerHTML=toolbar()+views[tab](x)}; // V5.2 extensions. The original storage key and earlier exercise structures are retained. const v52Tabs=[['v52assistant','Oefenassistent'],['v52locations','Locaties'],['v52materials','Materieel'],['v52tasks','Actielijst'],['v52links','Afhankelijkheden'],['v52live','Uitvoeringsmodus'],['v52package','Documentpakket'],['v52history','Oefenhistorie']]; for(const item of v52Tabs)tabs.splice(tabs.length-2,0,item); function v52ensure(x){ v5ensure(x); x.v52??={locations:[],materials:[],tasks:[],links:[],events:[]}; for(let key of ['locations','materials','tasks','links','events'])if(!Array.isArray(x.v52[key]))x.v52[key]=[]; } const v52Defaults={ locations:{name:'',kind:'Oefenterrein',address:'',coordinates:'',notes:''}, materials:{item:'',quantity:'',unit:'stuks',owner:'',issued:'Nee',returned:'Nee',notes:''}, tasks:{title:'',owner:'',due:'',status:'Open',notes:''}, links:{before:'',after:'',note:''}, events:{time:'',activity:'',note:''} }; function v52add(type){let x=current();v52ensure(x);x.v52[type].push({id:crypto.randomUUID(),...structuredClone(v52Defaults[type])});v52save('Onderdeel toegevoegd: '+type)} function v52save(msg){log('V5.2: '+msg);render()} function v52edit(type,id,key,value){let x=current();v52ensure(x);let r=x.v52[type].find(z=>z.id===id);if(r){r[key]=value;persist()}} function v52delete(type,id){if(!confirm('Dit onderdeel verwijderen?'))return;let x=current();x.v52[type]=x.v52[type].filter(z=>z.id!==id);v52save('Onderdeel verwijderd: '+type)} function v52input(type,r,key,label,kind='text'){return `
`} function v52select(type,r,key,label,values){return `
`} function v52head(kicker,title,desc){return `
${esc(kicker)}

${esc(title)}

${esc(desc)}

LOKAAL · V5.2
`} function v52list(type,title,desc,fields){ let x=current();v52ensure(x); return v52head('NWT · BEHEER',title,desc)+`

${esc(title)}

${x.v52[type].map(r=>`
${fields.map(f=>f[2]?v52select(type,r,f[0],f[1],f[2]):v52input(type,r,f[0],f[1],f[3]||'text')).join('')}
`).join('')||'

Er zijn nog geen onderdelen ingevoerd.

'}
`; } function v52locations(){return v52list('locations','Locatie- en terreinbibliotheek','Leg lokalen, oefenterreinen, verzamelpunten en coördinaten vast.',[['name','Locatie'],['kind','Type',['Oefenterrein','Leslokaal','Verzamelpunt','Buitenlocatie','Overig']],['address','Adres / omschrijving'],['coordinates','Coördinaten'],['notes','Bijzonderheden']])} function v52materials(){return v52list('materials','Materieel & logistiek','Controleer aanvraag, uitgifte en retour per oefening.',[['item','Materieel'],['quantity','Aantal'],['unit','Eenheid'],['owner','Verantwoordelijke'],['issued','Uitgegeven',['Nee','Ja','Niet van toepassing']],['returned','Retour',['Nee','Ja','Niet van toepassing']],['notes','Opmerkingen']])} function v52tasks(){return v52list('tasks','Taken & actielijst','Leg verantwoordelijke, deadline en voortgang vast.',[['title','Taak'],['owner','Verantwoordelijke'],['due','Deadline',null,'date'],['status','Status',['Open','Bezig','Afgerond','Geblokkeerd']],['notes','Toelichting']])} function v52links(){ let x=current();v52ensure(x),options=x.activities.map(a=>({id:a.id,title:(a.date||'')+' '+(a.title||'Activiteit')})); return v52head('PLANNING','Afhankelijkheden','Leg vast welke activiteit eerst moet plaatsvinden.')+`
${x.v52.links.map(r=>`
${[['before','Eerst'],['after','Daarna']].map(([key,label])=>`
`).join('')}${v52input('links',r,'note','Toelichting')}
`).join('')}

Afhankelijkheidscontrole

${v52dependencyWarnings(x).map(w=>`
${esc(w)}
`).join('')||'

Geen conflicten gevonden in de ingevoerde afhankelijkheden.

'}
`; } function v52dependencyWarnings(x){ let warnings=[],acts=new Map(x.activities.map(a=>[a.id,a])),edges=x.v52.links.filter(r=>r.before&&r.after); for(let l of edges){let a=acts.get(l.before),b=acts.get(l.after);if(!a||!b){warnings.push('Verwijzing naar een ontbrekende activiteit');continue} if(a.id===b.id)warnings.push('Een activiteit verwijst naar zichzelf: '+a.title); if(a.date&&a.end&&b.date&&b.start&&(a.date+'T'+a.end)>(b.date+'T'+b.start))warnings.push('Volgordeconflict: '+a.title+' → '+b.title); } let visiting=new Set(),visited=new Set(),adj=new Map();for(let l of edges){if(!adj.has(l.before))adj.set(l.before,[]);adj.get(l.before).push(l.after)} function walk(n){if(visiting.has(n)){warnings.push('Cirkel in afhankelijkheden gevonden');return}if(visited.has(n))return;visiting.add(n);for(let v of adj.get(n)||[])walk(v);visiting.delete(n);visited.add(n)} for(let k of adj.keys())walk(k);return [...new Set(warnings)]; } function v52assistant(){ let x=current();v52ensure(x);let checks=v4Checks(x),suggestions=[]; if(!x.v5.rhythm.length)suggestions.push('Maak een Battle Rhythm aan met briefings, pauzes en verplaatsingen.'); if(!x.v52.tasks.length)suggestions.push('Maak een actielijst voor de voorbereiding.'); if(!x.v52.locations.length)suggestions.push('Vul locaties en verzamelpunten in.'); if(!x.v52.materials.length)suggestions.push('Controleer benodigde middelen en materieel.'); if(!x.v5.safety.emergency)suggestions.push('Vul de noodprocedure in bij Documentgenerator.'); if(!x.v5.safety.assembly)suggestions.push('Vul het verzamelpunt in bij Documentgenerator.'); if(!x.evaluation?.followup)suggestions.push('Leg na afloop vervolgacties vast in Evaluatie.'); suggestions.push(...v52dependencyWarnings(x)); return v52head('LOKALE REGELASSISTENT','Slimme oefenassistent','Suggesties op basis van je ingevulde gegevens, zonder externe AI-dienst.')+`
${checks.errors.length}Fouten
${checks.warnings.length}Aandachtspunten
${suggestions.length}Verbeteradviezen

Adviezen en acties

${[...checks.errors,...checks.warnings,...suggestions].map(m=>`
${esc(m)}
`).join('')||'
Geen aandachtspunten volgens de ingebouwde regels.
'}

Dit zijn automatische regels, geen AI-analyse en geen formele veiligheidsgoedkeuring.

`; } function v52live(){ let x=current();v52ensure(x);let now=new Date(),today=now.getFullYear()+'-'+String(now.getMonth()+1).padStart(2,'0')+'-'+String(now.getDate()).padStart(2,'0'),clock=now.toTimeString().slice(0,5); let items=[...x.activities.map(a=>({...a,kind:'Activiteit'})),...x.v5.rhythm].filter(a=>a.date===today).sort((a,b)=>(a.start||'').localeCompare(b.start||'')); let active=items.find(a=>a.start<=clock&&a.end>clock),next=items.find(a=>a.start>clock); return v52head('UITVOERING','Live uitvoeringsmodus','Dagoverzicht op basis van de klok van dit apparaat.')+`
HUIDIG · ${esc(dateFmt(today))} · ${esc(clock)}

${esc(active?.title||'Geen actieve activiteit')}

${active?esc((active.start||'')+'–'+(active.end||'')+' · '+(active.location||'Locatie onbekend')):'Geen ingeplande activiteit op dit moment.'}

${esc(active?.instructors||active?.owner||'')}

Hierna

${next?esc(next.start+' · '+next.title+' · '+(next.location||'Locatie onbekend')):'Geen volgende activiteit voor vandaag.'}

Geen automatische synchronisatie, live GPS of achtergrondverversing. De klok van het apparaat bepaalt de weergave.

Vandaag

${items.map(a=>`
${esc(a.start)}–${esc(a.end)}${esc(a.title)}${esc(a.location||'Nog in te vullen')}
`).join('')||'

Geen activiteiten voor vandaag.

'}
`; } function v52package(){ let x=current();v52ensure(x); return v52head('DOCUMENTEN','Documentpakket','Exporteer afzonderlijke overzichten voor de oefening.')+`

Open een geëxporteerd HTML-document in een browser en kies Afdrukken → Opslaan als PDF. Dit is geen directe DOCX- of PDF-generator.

`; } function v52export(kind){ let x=current();v52ensure(x); let titles={activities:'Dagprogramma',materials:'Materieeloverzicht',safety:'Veiligheidsbijlage',tasks:'Actielijst',hours:'Instructeursuren'},rows=[],headers=[]; if(kind==='activities'){headers=['Datum','Tijd','Activiteit','Groep','Instructeurs','Locatie'];rows=x.activities.map(a=>[a.date,(a.start||'')+'–'+(a.end||''),a.title,a.group,a.instructors,a.location])} if(kind==='materials'){headers=['Materieel','Aantal','Eenheid','Verantwoordelijke','Uitgifte','Retour'];rows=x.v52.materials.map(a=>[a.item,a.quantity,a.unit,a.owner,a.issued,a.returned])} if(kind==='tasks'){headers=['Taak','Verantwoordelijke','Deadline','Status'];rows=x.v52.tasks.map(a=>[a.title,a.owner,a.due,a.status])} if(kind==='safety'){headers=['Onderdeel','Inhoud'];rows=[['Noodprocedure',x.v5.safety.emergency],['Verzamelpunt',x.v5.safety.assembly],['Medische ondersteuning',x.v5.safety.medical],['Communicatie',x.v5.safety.communications],...x.risks.map(r=>['Risico: '+r.hazard,'Maatregel: '+r.measure])]} if(kind==='hours'){headers=['Instructeur','Datum','Activiteit','Start','Eind','Pauze'];rows=hourRows(x).map(r=>[r.person,r.date,r.activity,r.start,r.end,r.breakMinutes||r.break||''])} let table=`${headers.map(h=>``).join('')}${rows.map(r=>`${r.map(c=>``).join('')}`).join('')}
${esc(h)}
${esc(c)}
`; let html=`${esc(titles[kind])}

${esc(titles[kind])}

${esc(x.name)} · ${dateFmt(x.start)} – ${dateFmt(x.end)}

${table}`; download('NWT_'+kind+'_'+safeName(x.name)+'.html',html,'text/html;charset=utf-8'); } function v52history(){ let x=current();v52ensure(x); let rows=state.exercises.map(z=>({name:z.name,start:z.start,status:z.status||'Concept',acts:z.activities?.length||0,lessons:z.evaluation?.lessons||'',improve:z.evaluation?.improvements||''})).sort((a,b)=>(b.start||'').localeCompare(a.start||'')); return v52head('KENNIS & ARCHIEF','Oefenhistorie','Zoek eerder gemaakte oefeningen en terugkerende leerpunten.')+`
${rows.map(r=>``).join('')}
OefeningStartStatusActiviteitenLeerpunten
${esc(r.name)}${esc(r.start)}${esc(r.status)}${r.acts}${esc([r.lessons,r.improve].filter(Boolean).join(' · '))}
`; } function v52filterHistory(query){let q=query.toLowerCase();for(let r of [...$('v52hist').rows].slice(1))r.style.display=r.textContent.toLowerCase().includes(q)?'':'none'} const v52OldRender=render; render=function(){let x=current();if(x)v52ensure(x);v52OldRender();if(!x)return;const views={v52assistant,v52locations,v52materials,v52tasks,v52links,v52live,v52package,v52history};if(views[tab])$('app').innerHTML=toolbar()+views[tab]()}; // V5.3: six requested modules. Templates are stored locally and can be imported/exported. const v53Tabs=[['v53templates','Sjablonen PRO'],['v53conflicts','Personeelscontrole'],['v53ready','Gereedheidscheck'],['v53changes','Versievergelijking'],['v53timeline','Tijdlijn'],['v53close','Afsluitprocedure']]; for(const t of v53Tabs)tabs.splice(tabs.length-2,0,t); function v53ensure(x){ v52ensure(x);x.v53??={ready:[],closing:[],fieldLog:[]}; for(let k of ['ready','closing','fieldLog'])if(!Array.isArray(x.v53[k]))x.v53[k]=[]; state.v53Templates??=[]; } const v53Groups={ 'Volledig oefensjabloon':['days','activities','logistics','customFields','people','risks','sections','hourEntries','v5.groups','v5.rhythm','v5.safety','v5.doc','v5.milestones','v52.locations','v52.materials','v52.tasks','v52.links'], 'Dagprogramma':['days','activities','v5.rhythm'], 'Personeel en groepen':['people','v5.groups'], 'Veiligheid':['risks','v5.safety'], 'Logistiek':['logistics','v52.materials'], 'Documentstructuur':['sections','customFields','v5.doc'], 'Voorbereidingstaken':['v5.milestones','v52.tasks'], 'Locaties':['v52.locations'] }; function v53get(obj,path){return path.split('.').reduce((a,k)=>a?.[k],obj)} function v53set(obj,path,value){let parts=path.split('.'),o=obj;for(let i=0;it.id===id);if(!t)return; let mode=confirm('OK = toevoegen aan bestaande onderdelen.\nAnnuleren = niet toepassen. Voor vervangen gebruik de aparte knop.'); if(!mode)return; v53apply(t,false); } function v53templateReplace(id){ let t=state.v53Templates.find(t=>t.id===id);if(!t)return; if(!confirm('De onderdelen in dit sjabloon vervangen de overeenkomstige onderdelen in de huidige oefening. Maak eerst een back-up. Doorgaan?'))return; v53apply(t,true); } function v53apply(t,replace){ let x=current();v53ensure(x); for(let [path,original] of Object.entries(t.payload)){ let incoming=v53remap(original),old=v53get(x,path); if(Array.isArray(incoming)){v53set(x,path,replace?incoming:[...(Array.isArray(old)?old:[]),...incoming])} else if(incoming&&typeof incoming==='object'){v53set(x,path,replace?incoming:{...(old||{}),...Object.fromEntries(Object.entries(incoming).filter(([k,v])=>v!==''))})} } // A template must never silently copy an approval or final document status. x.status='Concept';x.v5.safety.approval='';x.v53.ready=[];x.v53.closing=[]; log('Sjabloon '+(replace?'vervangend':'toegevoegd')+': '+t.name);render(); } function v53templateDelete(id){let t=state.v53Templates.find(t=>t.id===id);if(!t||!confirm('Sjabloon verwijderen: '+t.name+'?'))return;state.v53Templates=state.v53Templates.filter(z=>z.id!==id);log('Sjabloon verwijderd');render()} function v53templateExport(id){ let t=state.v53Templates.find(t=>t.id===id);if(!t)return; download('NWT_Sjabloon_'+safeName(t.name)+'.json',JSON.stringify({format:'nwt-template-v1',template:t},null,2),'application/json'); } async function v53templateImport(file){ if(!file)return; try{let o=JSON.parse(await file.text()),t=o.template; if(o.format!=='nwt-template-v1'||!t||typeof t.name!=='string'||!v53Groups[t.category]||!t.payload||typeof t.payload!=='object')throw Error('Onbekend sjabloonformaat'); let safe={id:crypto.randomUUID(),name:t.name.slice(0,120),category:t.category,description:String(t.description||'').slice(0,1000),created:new Date().toISOString(),payload:{}}; for(let k of v53Groups[t.category])if(Object.prototype.hasOwnProperty.call(t.payload,k)){let v=t.payload[k];if(v&&typeof v==='object')safe.payload[k]=v53clone(v)} if(JSON.stringify(safe).length>2e6)throw Error('Sjabloon is te groot'); state.v53Templates??=[];state.v53Templates.push(safe);log('Sjabloon geïmporteerd: '+safe.name);render(); }catch(e){alert('Import mislukt: '+e.message)} } function v53templates(){ let x=current();v53ensure(x); return v52head('SJABLONEN · PRO','Oefensjablonenbibliotheek','Bewaar complete oefeningen of losse bouwblokken en gebruik ze in een nieuwe oefening.')+ `

Nieuw sjabloon uit deze oefening

Sjablonen staan in de lokale browseropslag en gaan mee in een volledige JSON-back-up. Een afzonderlijke sjabloonexport is geschikt om op een andere laptop te importeren.

`+ `

Mijn sjablonen (${state.v53Templates.length})

${state.v53Templates.map(t=>`

${esc(t.name)}

${esc(t.category)} · ${esc(new Date(t.created).toLocaleDateString('nl-NL'))}

${esc(t.description)}

${Object.entries(t.payload).map(([k,v])=>esc(k)+(Array.isArray(v)?' ('+v.length+')':'')).join(' · ')}

`).join('')||'

Je hebt nog geen sjablonen. Sla de huidige oefening of een onderdeel op als eerste sjabloon.

'}
`+ `

Zo werk je met sjablonen

1. Bouw een goede oefening of een herbruikbaar onderdeel. 2. Kies een sjabloontype en sla op. 3. Maak een nieuwe oefening aan. 4. Voeg de gewenste sjablonen toe. 5. Vul datum, personen en locatie aan en voer de controles opnieuw uit.

Bewust niet overgenomen: definitieve goedkeuring, eerdere gereedheidsvinkjes en afsluitstatus. Let bij het combineren op dubbele regels en onjuiste oude datums.

`; } function v53names(s){return String(s||'').split(/[,;\n]+/).map(v=>v.trim()).filter(Boolean)} function v53interval(a){let s=minuteCount(a.start),e=minuteCount(a.end);return s!==null&&e!==null&&e>s?{s,e}:null} function v53conflictData(x){ let warnings=[],people=new Map(); for(let a of [...x.activities.map(z=>({...z,personText:z.instructors,source:'Activiteit'})),...x.v5.rhythm.map(z=>({...z,personText:z.owner,source:'Battle Rhythm'}))]){ let t=v53interval(a);if(!t||!a.date)continue; for(let n of v53names(a.personText)){let k=n.toLocaleLowerCase('nl-NL')+'|'+a.date;if(!people.has(k))people.set(k,[]);people.get(k).push({...t,title:a.title||'Onbenoemd',person:n,source:a.source,date:a.date})} } for(let records of people.values())for(let i=0;i
${warnings.length}Mogelijke conflicten
${x.activities.length}Activiteiten gecontroleerd
${warnings.map(w=>`
${esc(w)}
`).join('')||'
Geen overlappende inzet gevonden in ingevulde namen en tijden.
'}

Namen worden vergeleken op spelling, zonder koppeling aan het roosterproject. Onbekende tijden, reistijd en externe beschikbaarheid worden niet automatisch gecontroleerd.

`; } const v53ReadyList=['Oefendoel en datums gecontroleerd','Oefenleider en verantwoordelijkheden bevestigd','Instructeurs en groepsindeling gecontroleerd','Activiteiten en tijden gecontroleerd','Locaties en verzamelpunten bevestigd','Materieel beschikbaar en uitgegeven','Risicoanalyse en veiligheidsmaatregelen beoordeeld','Noodprocedure en medische ondersteuning gecontroleerd','Communicatie en briefing gereed','Documenten gecontroleerd en gedeeld waar toegestaan']; const v53CloseList=['Alle activiteiten afgerond of afwijkingen vastgelegd','Materieel retour en tekorten vastgelegd','Instructeursuren gecontroleerd','Incidenten en veiligheidsmeldingen afgehandeld','Evaluatie en verbeterpunten vastgelegd','Vervolgacties toegewezen','Documenten en back-up veilig gearchiveerd']; function v53toggle(key,label,checked){ let x=current();v53ensure(x);let rows=x.v53[key],existing=rows.find(z=>z.label===label); if(existing)existing.checked=checked;else rows.push({label,checked,date:new Date().toISOString()}); persist(); } function v53checklist(key,items,title,description){ let x=current();v53ensure(x);let rows=x.v53[key],count=items.filter(label=>rows.some(z=>z.label===label&&z.checked)).length; return v52head('CHECKLIST',title,description)+`

Voortgang: ${count} van ${items.length}

${items.map(label=>{let checked=rows.some(z=>z.label===label&&z.checked);return `
`}).join('')}

Dit zijn handmatige bevestigingen, geen automatische veiligheidsgoedkeuring. Nieuwe kopieën uit sjablonen beginnen met lege checklists.

`; } function v53ready(){let x=current(),c=v53conflictData(x),q=v4Checks(x);return v53checklist('ready',v53ReadyList,'Dagelijkse gereedheidscontrole','Voor aanvang zelf bevestigen wat daadwerkelijk gereed is.')+`

Automatische signalen

${[...q.errors,...c].map(w=>`
${esc(w)}
`).join('')||'

Geen basisfouten of personeelsconflicten gevonden.

'}
`} function v53close(){let x=current();return v53checklist('closing',v53CloseList,'Afsluitprocedure','Sluit de oefening gecontroleerd af en archiveer de resultaten.')+`
`} function v53snapshotDiff(){ let x=current();v4ensure(x); return v52head('VERSIEBEHEER','Wijzigingsrapport','Vergelijk een eerdere momentopname met de actuele oefening.')+ `

Kies een momentopname om verschillen te bekijken.

`; } function v53showDiff(id){ let x=current(),v=(x.versions||[]).find(z=>z.id===id),out=$('v53diff');if(!v){out.innerHTML='';return} let before=v.snapshot||{},fields=['name','code','start','end','location','commander','goal','notes'],changes=[]; for(let key of fields)if(JSON.stringify(before[key]??'')!==JSON.stringify(x[key]??''))changes.push(`${key}: ${String(before[key]??'—').slice(0,100)} → ${String(x[key]??'—').slice(0,100)}`); for(let key of ['days','activities','logistics','customFields','people','risks','sections','hourEntries']){let a=before[key]||[],b=x[key]||[];if(JSON.stringify(a)!==JSON.stringify(b))changes.push(`${key}: inhoud gewijzigd (eerder ${a.length} regels, nu ${b.length})`)} out.innerHTML=changes.length?changes.map(c=>`
${esc(c)}
`).join(''):'
Geen verschillen gevonden in de gecontroleerde velden.
'; } function v53move(id,delta){ let x=current(),a=x.activities.find(z=>z.id===id);if(!a||!a.date||!a.start||!a.end)return; let s=minuteCount(a.start),e=minuteCount(a.end);if(s===null||e===null||e<=s)return; let nextS=s+delta,nextE=e+delta; if(nextS<0||nextE>=1440){alert('Verplaatsen buiten deze dag is niet toegestaan. Pas de datum handmatig aan.');return} let fmt=n=>String(Math.floor(n/60)).padStart(2,'0')+':'+String(n%60).padStart(2,'0'); a.start=fmt(nextS);a.end=fmt(nextE);log('Tijd verschoven: '+a.title+' ('+(delta>0?'+':'')+delta+' min)');render(); } function v53timeline(){ let x=current(),dates=[...new Set(x.activities.map(a=>a.date).filter(Boolean))].sort(); return v52head('VISUEEL · TIJDLIJN','Tijdlijn & verschuiven','Verplaats activiteiten met 15 minuten en controleer conflicten opnieuw.')+ `
${dates.map(date=>`

${esc(dateFmt(date))}

${x.activities.filter(a=>a.date===date).sort((a,b)=>(a.start||'').localeCompare(b.start||'')).map(a=>`
${esc(a.start)}–${esc(a.end)}${esc(a.title)}${esc(a.instructors||'')}
`).join('')}`).join('')||'

Voeg eerst activiteiten met een datum toe.

'}

Controle na verschuiving

${[...v53conflictData(x),...v52dependencyWarnings(x)].map(w=>`
${esc(w)}
`).join('')||'

Geen conflicten gevonden.

'}

De knoppen verschuiven uitsluitend de geselecteerde activiteit; afhankelijkheden worden niet automatisch verplaatst. Dat voorkomt ongewenste wijzigingen.

`; } const v53OldRender=render; render=function(){let x=current();if(x)v53ensure(x);v53OldRender();if(!x)return;let views={v53templates,v53conflicts,v53ready,v53changes:v53snapshotDiff,v53timeline,v53close};if(views[tab])$('app').innerHTML=toolbar()+views[tab]()}; // V5.4: dedicated template composer. Does not alter the roster application. let v54Selected=[]; function v54Pick(id,checked){v54Selected=checked?[...new Set([...v54Selected,id])]:v54Selected.filter(x=>x!==id);v54UpdatePreview()} function v54SelectedTemplates(){return (state.v53Templates||[]).filter(t=>v54Selected.includes(t.id))} function v54Summarize(){ let parts={},sources=[]; for(let t of v54SelectedTemplates()){sources.push(t.name);for(let [key,val] of Object.entries(t.payload||{})){let item=parts[key]??={count:0,contributors:[]};item.count+=Array.isArray(val)?val.length:1;item.contributors.push(t.name)}} return {parts,sources}; } function v54UpdatePreview(){ let e=$('v54preview');if(!e)return; let summary=v54Summarize(); e.innerHTML=`${summary.sources.length} sjablo(o)n(en) geselecteerd

${summary.sources.map(esc).join(' · ')||'Kies één of meer sjablonen.'}

${Object.entries(summary.parts).map(([k,v])=>`
${esc(k)}${v.count} onderdelen uit ${v.contributors.length} sjablo(o)n(en)
`).join('')}
`; } function v54Shift(d,delta){if(!/^\d{4}-\d{2}-\d{2}$/.test(d||''))return d;let date=new Date(d+'T12:00:00Z');date.setUTCDate(date.getUTCDate()+delta);return date.toISOString().slice(0,10)} function v54Build(){ let ts=v54SelectedTemplates();if(!ts.length){alert('Selecteer minimaal één sjabloon.');return} let name=$('v54name').value.trim(),date=$('v54start').value; if(!name||!date){alert('Vul de oefennaam en startdatum in.');return} let old=state.selected,x=blank();v53ensure(x); x.name=name;x.start=date;x.status='Concept';x.v5.safety.approval=''; let sourceDates=[]; for(let t of ts)for(let [key,v] of Object.entries(t.payload||{})){ if(Array.isArray(v))for(let item of v)if(item?.date&&/^\d{4}-\d{2}-\d{2}$/.test(item.date))sourceDates.push(item.date); } let first=sourceDates.sort()[0],delta=first?Math.round((Date.parse(date)-Date.parse(first))/86400000):0; let ids=new Map(),warnings=[]; function cloneValue(v){ if(Array.isArray(v))return v.map(cloneValue); if(v&&typeof v==='object'){let z={};for(let [k,val] of Object.entries(v)){if(k==='id'&&typeof val==='string'){let next=crypto.randomUUID();ids.set(val,next);z[k]=next} else if(k==='date'&&typeof val==='string')z[k]=v54Shift(val,delta); else z[k]=cloneValue(val)}return z}return v; } for(let t of ts)for(let [key,v] of Object.entries(t.payload||{})){ if(!v||typeof v!=='object')continue; let val=cloneValue(v),previous=v53get(x,key); if(Array.isArray(val))v53set(x,key,[...(Array.isArray(previous)?previous:[]),...val]); else v53set(x,key,{...(previous||{}),...Object.fromEntries(Object.entries(val).filter(([k,z])=>z!==''))}); } // Repair links where the referenced activity was included in the same composed exercise. for(let l of x.v52.links||[])for(let k of ['before','after'])if(l[k]){if(ids.has(l[k]))l[k]=ids.get(l[k]);else{warnings.push('Afhankelijkheid verwijst mogelijk naar een niet-overgenomen activiteit');l[k]=''}} let dates=[...x.days.map(d=>d.date),...x.activities.map(a=>a.date)].filter(Boolean).sort(); x.end=dates.at(-1)||date; if(x.end({name:t.name,category:t.category,created:t.created})); x.v53.composerWarnings=[...new Set(warnings)]; state.exercises.push(x);state.selected=x.id; v54Selected=[];log('Nieuwe oefening samengesteld uit '+ts.length+' sjablonen: '+name); tab='v54review';render(); } function v54Composer(){ v53ensure(current()); return v52head('SJABLONEN · PRO','Oefening samenstellen','Combineer meerdere sjablonen tot één nieuwe, bewerkbare oefening.')+ `

Stap 1 · Selecteer bouwblokken

${(state.v53Templates||[]).map(t=>``).join('')||'

Er zijn nog geen sjablonen. Maak eerst een sjabloon via Sjablonen PRO.

'}

Stap 2 · Voorbeeld samenstelling

Stap 3 · Nieuwe oefening

De datums van overgenomen programmaonderdelen worden relatief verschoven op basis van de vroegste sjabloondatum. Controleer alle tijden, instructeurs, locaties, materieel en veiligheid vóór gebruik.

`; } function v54Review(){ let x=current();v53ensure(x); let q=v4Checks(x),c=v53conflictData(x),d=v52dependencyWarnings(x),warnings=x.v53.composerWarnings||[]; let sources=x.v53.sourceTemplates||[]; return v52head('CONTROLE · NIEUWE OEFENING','Samenstelling controleren','Bevestig de inhoud vóór het vaststellen of delen.')+ `

Gebruikte sjablonen

${sources.map(t=>`

${esc(t.name)} (${esc(t.category)})

`).join('')||'

Geen herkomstgegevens vastgelegd voor deze oefening.

'}

Controlepunten

${[...q.errors,...q.warnings,...c,...d,...warnings].map(w=>`
${esc(w)}
`).join('')||'
Geen problemen gevonden in de automatische controles.
'}
`; } const v54OldRender=render; tabs.splice(tabs.length-2,0,['v54composer','Oefening samenstellen'],['v54review','Samenstelling controleren']); render=function(){v54OldRender();let x=current();if(!x)return;if(tab==='v54composer'){$('app').innerHTML=toolbar()+v54Composer();v54UpdatePreview()}if(tab==='v54review')$('app').innerHTML=toolbar()+v54Review()}; // V5.5: safe example reuse and staged offline document imports. let v55Preview=null; tabs.splice(tabs.length-2,0,['v55reuse','Bestaand voorbeeld']); function v55copy(){let source=state.exercises.find(x=>x.id===$('v55source').value);if(!source)return;let name=$('v55name').value.trim();if(!name){alert('Vul een nieuwe oefennaam in');return}let date=$('v55date').value;if(!date){alert('Kies een startdatum');return}let copy=v53clone(source);let originalStart=source.start;copy.id=crypto.randomUUID();copy.name=name;copy.start=date;copy.status='Concept';copy.archived=false;copy.created=new Date().toISOString();copy.versions=[];copy.version=1;copy.v53??={};copy.v53.ready=[];copy.v53.closing=[];copy.v53.sourceExercise={id:source.id,name:source.name,date:originalStart};copy.v53.composerWarnings=['Controleer instructeurs, tijden, locaties en veiligheidsmaatregelen opnieuw.'];copy.v5.safety.approval='';if(originalStart){let delta=Math.round((Date.parse(date)-Date.parse(originalStart))/86400000);for(let key of ['end'])copy[key]=v54Shift(copy[key],delta);for(let key of ['days','activities'])for(let a of copy[key]||[])a.date=v54Shift(a.date,delta);for(let a of copy.v5.rhythm||[])a.date=v54Shift(a.date,delta);for(let a of copy.hourEntries||[])a.date=v54Shift(a.date,delta)}state.exercises.push(copy);state.selected=copy.id;log('Oefening gemaakt uit voorbeeld: '+source.name);tab='v54review';render()} function v55parseCsv(text){let lines=text.replace(/^\ufeff/,'').split(/\r?\n/).filter(x=>x.trim());let delimiter=(lines[0]||'').includes(';')?';':',';function parse(line){let out=[],v='',q=false;for(let i=0;ix.toLowerCase().trim());let map={datum:'date',date:'date',start:'start',begintijd:'start',eind:'end',eindtijd:'end',end:'end',activiteit:'title',onderwerp:'title',title:'title',groep:'group',group:'group',instructeur:'instructors',instructeurs:'instructors',instructors:'instructors',locatie:'location',location:'location',materiaal:'material',material:'material',omschrijving:'description',description:'description'};let records=lines.map(line=>{let r={};parse(line).forEach((v,i)=>{let key=map[headers[i]];if(key)r[key]=v});return r}).filter(r=>r.title||r.start||r.date);return records} async function v55readFile(file){if(!file)return;try{if(file.size>10000000)throw Error('Bestand groter dan 10 MB');let ext=file.name.split('.').pop().toLowerCase(),rows=[],sections=[],message='';if(ext==='csv'){rows=v55parseCsv(await file.text());message='CSV: kolommen op naam herkend'}else if(ext==='docx'){let buf=await file.arrayBuffer(),zip=await excelZipEntries(buf),xml=zip['word/document.xml'];if(!xml)throw Error('Geen Word-documenttekst gevonden');let doc=excelXml(xml),ns='http://schemas.openxmlformats.org/wordprocessingml/2006/main';let body=doc.getElementsByTagNameNS(ns,'body')[0];if(!body)throw Error('Geen documentinhoud');for(let node of body.children){if(node.localName==='p'){let text=[...node.getElementsByTagNameNS(ns,'t')].map(n=>n.textContent).join('');if(text.trim())sections.push(text.trim())}else if(node.localName==='tbl'){for(let tr of node.getElementsByTagNameNS(ns,'tr')){let cells=[...tr.children].filter(n=>n.localName==='tc').map(tc=>[...tc.getElementsByTagNameNS(ns,'t')].map(n=>n.textContent).join(' ').trim());if(cells.some(Boolean))sections.push(cells.join(' | '))}}}message='Word: tekst en tabelregels als bewerkbare voorbeeldsectie (geen betrouwbare automatische tijdsherkenning)'}else if(ext==='xlsx'){throw Error('Gebruik de bestaande Excel-import voor activiteiten, of exporteer dit bestand als CSV en gebruik deze voorbeeldimport.')}else throw Error('Gebruik .docx of .csv');v55Preview={filename:file.name,rows,sections,message};v55ShowPreview()}catch(e){alert('Voorbeeld inlezen mislukt: '+e.message)}} function v55ShowPreview(){let e=$('v55preview');if(!e||!v55Preview)return;let p=v55Preview;e.innerHTML=`

Importvoorbeeld: ${esc(p.filename)}

${esc(p.message)}

${p.rows.length} activiteiten · ${p.sections.length} tekst-/tabelregels

${(p.rows.length?p.rows.map(r=>[r.date,r.start,r.end,r.title,r.group,r.instructors].filter(Boolean).join(' | ')):p.sections).slice(0,35).map(r=>`

${esc(r)}

`).join('')}

Controleer de inhoud. Een Word-document wordt als bewerkbare tekstregels overgenomen; oorspronkelijke Word-opmaak, samengevoegde cellen en afbeeldingen worden niet automatisch gereconstrueerd.

`} function v55ImportPreview(){if(!v55Preview)return;let name=prompt('Naam nieuwe oefening:',v55Preview.filename.replace(/\.[^.]+$/,''));if(!name?.trim())return;let x=blank();v53ensure(x);x.name=name.trim();x.v53.sourceDocument=v55Preview.filename;x.v53.composerWarnings=['Controleer alle uit het brondocument overgenomen gegevens.'];if(v55Preview.rows.length){for(let r of v55Preview.rows)x.activities.push({id:crypto.randomUUID(),date:r.date||'',start:r.start||'',end:r.end||'',title:r.title||'Nog in te vullen',group:r.group||'',instructors:r.instructors||'',location:r.location||'',material:r.material||'',description:r.description||'',status:'Gepland'});x.start=x.activities.map(a=>a.date).filter(d=>/^\d{4}-\d{2}-\d{2}$/.test(d)).sort()[0]||'';x.end=x.activities.map(a=>a.date).filter(d=>/^\d{4}-\d{2}-\d{2}$/.test(d)).sort().at(-1)||''}else{x.sections.push({id:crypto.randomUUID(),title:'Overgenomen voorbeeld: '+v55Preview.filename,kind:'tekst',body:v55Preview.sections.join('\n')})}state.exercises.push(x);state.selected=x.id;log('Nieuwe oefening uit document: '+v55Preview.filename);v55Preview=null;tab='builder';render()} function v55reuse(){let x=current();return v52head('HERGEBRUIK · VEILIG','Bestaand oefenverloop als voorbeeld','Maak een aparte kopie of lees een document in. Het origineel blijft ongewijzigd.')+`

1 · Kopieer een bestaande oefening

De nieuwe oefening krijgt een eigen ID, conceptstatus en lege gereedheidscheck. Activiteiten en oefendagen worden relatief verschoven als de bronoefening een startdatum heeft.

2 · Gebruik een bestaand Word- of CSV-bestand

Word (.docx): neem de inhoud over als bewerkbare tekstregels. CSV: neem activiteiten over wanneer de kolommen herkenbaar zijn. De bestaande Excel-import blijft beschikbaar voor .xlsx-bestanden.

Documenten worden uitsluitend lokaal in je browser gelezen. Er worden geen bestanden geüpload.

3 · Vergelijk en werk verder

Na het kopiëren kun je alle onderdelen aanpassen en via Samenstelling controleren en Versievergelijking de wijzigingen bekijken. De oorspronkelijke oefening blijft beschikbaar.

`} const v55OldRender=render;render=function(){v55OldRender();if(tab==='v55reuse'&¤t()){$('app').innerHTML=toolbar()+v55reuse();v55ShowPreview()}}; // V5.6 — separate import buttons for a source example and a NEW Excel exercise. tabs.splice(tabs.length-2,0,['v56import','Importeer voorbeeld / Excel']); function v56ImportPage(){return v52head('IMPORT · TWEE ROUTES','Oefenverloop importeren','Kies duidelijk tussen een voorbeeld overnemen en een nieuw oefenverloop maken vanuit Excel.')+`

Voorbeeld importeren

Open een bestaand Word-oefenverloop (.docx) of CSV als voorbeeld. De bron blijft intact. Na controle wordt een nieuwe, bewerkbare oefening aangemaakt.

Nieuw oefenverloop uit Excel

Lees een bestaand Excel-bestand (.xlsx) in, kies werkblad en kolommen, bekijk de activiteiten en maak een geheel nieuwe oefening aan.

Voorbeeld bekijken

Beide routes lezen bestanden lokaal. Maak vooraf een JSON-back-up. Controleer na import altijd datums, tijden, locaties en personeel.

`} async function v56Example(file){if(!file)return;await v55readFile(file);if(v55Preview){tab='v56import';render()}} async function v56Excel(input){if(!input.files?.length)return;await excelLoad(input);if(excelState.sheets.length){tab='excel';render();let target=document.getElementById('v56newExcel');if(target)target.scrollIntoView({block:'nearest'})}} function v56CreateNewFromExcel(){ if(!excelState.preview?.length){alert('Lees eerst een Excel-bestand in en controleer de kolommen.');return} let selected=excelState.preview.filter(p=>p.selected).map(p=>p.activity); if(!selected.length){alert('Er zijn geen geldige geselecteerde activiteiten.');return} let name=prompt('Naam van het NIEUWE oefenverloop:',excelState.filename.replace(/\.[^.]+$/,''));if(!name?.trim())return; let keys=new Set(),rows=selected.filter(a=>{let key=[a.date,a.start,a.end,a.title,a.group].map(excelNormalize).join('|');if(keys.has(key))return false;keys.add(key);return true}); if(!confirm('Nieuwe oefening "'+name.trim()+'" maken met '+rows.length+' activiteiten? De bestaande oefening wordt niet aangepast.'))return; let x=blank();v53ensure(x);x.name=name.trim();x.v53.sourceDocument=excelState.filename;x.v53.composerWarnings=['Controleer de uit Excel overgenomen tijden, activiteiten, instructeurs en locaties.']; for(let a of rows)x.activities.push({id:crypto.randomUUID(),...a}); let dates=rows.map(a=>a.date).filter(d=>/^\d{4}-\d{2}-\d{2}$/.test(d)).sort();x.start=dates[0]||'';x.end=dates.at(-1)||''; state.exercises.push(x);state.selected=x.id;log('Nieuwe oefening uit Excel: '+excelState.filename+' ('+rows.length+' activiteiten)');tab='activities';render(); } const v56OldRender=render; render=function(){v56OldRender();if(tab==='v56import'&¤t()){$('app').innerHTML=toolbar()+v56ImportPage();v55ShowPreview()}if(tab==='excel'&¤t()){ let el=document.createElement('div');el.className='panel';el.id='v56newExcel';el.innerHTML='

Nieuwe oefening maken uit dit Excel-bestand

Dit maakt een aparte oefening. De huidige oefening blijft ongewijzigd.

'; let app=document.getElementById('app');app.appendChild(el); }} // V5.7 Phase 1 of the twelve-part roadmap: safer imports, review, backups and structured knowledge. tabs.splice(tabs.length-2,0,['v57import','Importcontrole PRO'],['v57library','Kennisbibliotheek'],['v57backup','Herstel & back-up']); let v57Staged=[]; function v57prepareRows(){ let data=excelState.preview||[]; v57Staged=data.map((p,i)=>({id:i,selected:!!p.selected,activity:structuredClone(p.activity||{})})); tab='v57import';render(); } function v57change(i,k,val){let r=v57Staged.find(r=>r.id===i);if(r)r.activity[k]=val} function v57toggle(i,yes){let r=v57Staged.find(r=>r.id===i);if(r)r.selected=yes} function v57ImportView(){ return v52head('FASE 1 · IMPORT','Importcontrole PRO','Controleer en corrigeer Excel-gegevens vóórdat je een nieuwe oefening aanmaakt.')+ `

Stap 1 · Bestand kiezen

Excel gebruikt de bestaande lokale werkblad- en kolomherkenning. Na de voorvertoning kun je hier de geselecteerde rijen corrigeren.

`+ `

Stap 2 · Controleer ${v57Staged.length} regels

${v57Staged.map(r=>`${[['date','date'],['start','time'],['end','time'],['title','text'],['group','text'],['instructors','text'],['location','text']].map(([k,t])=>``).join('')}`).join('')}
GebruikDatumStartEindActiviteitGroepInstructeursLocatie
`; } async function v57load(input){await excelLoad(input);tab='excel';render()} function v57commit(){ let rows=v57Staged.filter(r=>r.selected).map(r=>r.activity); if(!rows.length){alert('Selecteer eerst regels.');return} let invalid=rows.filter(r=>!r.title?.trim()||!r.date||!r.start||!r.end||minuteCount(r.start)===null||minuteCount(r.end)===null||minuteCount(r.end)<=minuteCount(r.start)); if(invalid.length&&!confirm(invalid.length+' regel(s) hebben ontbrekende of ongeldige velden. Toch als concept importeren?'))return; let name=prompt('Naam van de nieuwe oefening:',excelState.filename?.replace(/\.[^.]+$/,'')||'Geïmporteerde oefening');if(!name?.trim())return; let x=blank();v53ensure(x);x.name=name.trim();x.v53.sourceDocument=excelState.filename||'Excel'; x.activities=rows.map(a=>({id:crypto.randomUUID(),date:a.date||'',start:a.start||'',end:a.end||'',title:a.title||'',group:a.group||'',instructors:a.instructors||'',location:a.location||'',material:a.material||'',description:a.description||'',status:'Gepland'})); let dates=rows.map(a=>a.date).filter(d=>/^\d{4}-\d{2}-\d{2}$/.test(d)).sort();x.start=dates[0]||'';x.end=dates.at(-1)||''; state.exercises.push(x);state.selected=x.id;v57Staged=[];log('Gecontroleerde Excel-import: '+name+' ('+rows.length+' activiteiten)');tab='v54review';render(); } function v57knowledge(){ let x=current();v53ensure(x);state.v57Knowledge??=[]; return v52head('FASE 1 · KENNIS','Kennisbibliotheek','Bewaar herbruikbare teksten, briefings, checklists en standaardprocedures.')+ `
${v57KnowledgeItems()}
`; } function v57KnowledgeItems(query=''){return (state.v57Knowledge||[]).filter(r=>(r.title+' '+r.category+' '+r.body).toLowerCase().includes(query.toLowerCase())).map(r=>`

${esc(r.title)}

${esc(r.category)}

${esc(r.body)}

`).join('')||'

Geen kennisitems gevonden.

'} function v57filter(q){$('v57items').innerHTML=v57KnowledgeItems(q)} function v57addKnowledge(){let title=$('v57title').value.trim(),body=$('v57body').value.trim();if(!title||!body){alert('Titel en inhoud zijn verplicht.');return}state.v57Knowledge??=[];state.v57Knowledge.push({id:crypto.randomUUID(),title,body,category:$('v57cat').value});log('Kennisitem toegevoegd');render()} function v57removeKnowledge(id){if(!confirm('Kennisitem verwijderen?'))return;state.v57Knowledge=state.v57Knowledge.filter(r=>r.id!==id);log('Kennisitem verwijderd');render()} function v57useKnowledge(id){let r=(state.v57Knowledge||[]).find(r=>r.id===id);if(!r)return;current().customFields.push({id:crypto.randomUUID(),label:r.title,value:r.body});log('Kennisitem toegevoegd aan oefening');render()} function v57BackupView(){ let bytes=new Blob([JSON.stringify(state)]).size; return v52head('FASE 1 · GEGEVENS','Back-up & herstel','Maak een volledige back-up voordat je importeert of onderdelen vervangt.')+ `

Lokale gegevens

${state.exercises.length} oefeningen · ${(state.v53Templates||[]).length} sjablonen · ${(state.v57Knowledge||[]).length} kennisitems · ${(bytes/1024).toFixed(1)} KB

Herstelpunten staan in dezelfde browseropslag. Alleen een gedownload JSON-bestand beschermt tegen browseropruiming of computerverlies.

Herstelpunten

${(state.v57RestorePoints||[]).map((r,i)=>`
${esc(r.time)}
`).join('')||'

Geen herstelpunten.

'}
`; } function v57RestorePoint(){ let points=state.v57RestorePoints||[],data=structuredClone(state);delete data.v57RestorePoints; points.unshift({time:new Date().toLocaleString('nl-NL'),data});state.v57RestorePoints=points.slice(0,3); try{persist();render()}catch(e){state.v57RestorePoints=[];alert('Herstelpunt te groot voor browseropslag. Download een JSON-back-up.')} } function v57Restore(i){ let r=state.v57RestorePoints?.[i];if(!r||!confirm('Alle huidige gegevens vervangen door dit herstelpunt? Download eerst een actuele back-up.'))return; state=structuredClone(r.data);state.v57RestorePoints=[];persist();render(); } function v57DownloadRestore(i){let r=state.v57RestorePoints?.[i];if(r)download('NWT_herstelpunt_'+(i+1)+'.json',JSON.stringify(r.data,null,2),'application/json')} const v57OldRender=render; render=function(){v57OldRender();let x=current();if(!x)return;let views={v57import:v57ImportView,v57library:v57knowledge,v57backup:v57BackupView};if(views[tab])$('app').innerHTML=toolbar()+views[tab]()}; // V6.1 integrated features (phases 2–5): form designer, time checks, personnel, // equipment packs, activity safety, changes, release, mobile view, document styling. const v61Tabs=[ ['v61forms','Formulierontwerper'],['v61planning','Planningcontrole'], ['v61people','Personeelsbibliotheek'],['v61packs','Materieelpakketten'], ['v61safety','Veiligheid per activiteit'],['v61revisions','Wijzigingen PRO'], ['v61release','Vrijgave'],['v61mobile','Instructeursweergave'], ['v61documents','Documentstudio'],['v61audit','Systeemcontrole']]; tabs.splice(tabs.length-2,0,...v61Tabs); function v61init(x){ v53ensure(x); x.v61??={form:[],safety:[],release:{status:'Concept',by:'',at:'',note:'',snapshot:null},changeLog:[]}; for(let k of ['form','safety','changeLog'])if(!Array.isArray(x.v61[k]))x.v61[k]=[]; x.v61.release??={status:'Concept',by:'',at:'',note:'',snapshot:null}; state.v61People??=[];state.v61Packs??=[]; } function v61Save(msg){log('V6.1 · '+msg);render()} function v61FormAdd(type){ let x=current();v61init(x); x.v61.form.push({id:crypto.randomUUID(),type,title:'Nieuw onderdeel',content:'',rows:[['Kolom 1','Kolom 2']],columns:['Kolom 1','Kolom 2'],tone:'Normaal'}); v61Save('Formulieronderdeel toegevoegd'); } function v61FormField(id,key,value){let a=current().v61.form.find(a=>a.id===id);if(a){a[key]=value;persist()}} function v61FormDelete(id){if(!confirm('Formulieronderdeel verwijderen?'))return;current().v61.form=current().v61.form.filter(a=>a.id!==id);v61Save('Formulieronderdeel verwijderd')} function v61FormMove(id,delta){let a=current().v61.form,i=a.findIndex(z=>z.id===id),j=i+delta;if(i<0||j<0||j>=a.length)return;[a[i],a[j]]=[a[j],a[i]];v61Save('Formulieronderdelen verplaatst')} function v61TableRows(id,raw){ let a=current().v61.form.find(a=>a.id===id);if(!a)return; a.rows=raw.split('\n').map(r=>r.split('|').map(c=>c.trim()));persist(); } function v61Forms(){ let x=current();v61init(x); return v52head('FASE 2','Formulierontwerper PRO','Eigen hoofdstukken, toelichtingen, aandachtspunten en tabellen voor het oefenverloop.')+ `

Voeg een onderdeel toe

${[['heading','Hoofdstuk'],['text','Tekstblok'],['table','Tabel'],['warning','Aandachtspunt'],['check','Checklist']].map(([k,v])=>``).join('')}
`+ x.v61.form.map((a,i)=>`

${i+1}. ${esc(a.type)}

${a.type==='table'?``:``}
`).join('')+ `
`; } function v61TimeWarnings(x){ let warnings=[],activities=x.activities||[]; for(let a of activities){ let start=minuteCount(a.start),end=minuteCount(a.end); if(start===null||end===null||end<=start)warnings.push('Ongeldige tijd: '+(a.title||'activiteit')); } for(let i=0;i

${warnings.length} aandachtspunten

${warnings.map(w=>`
${esc(w)}
`).join('')||'

Geen conflicten gevonden binnen de ingevulde gegevens.

'}

Reistijden en externe beschikbaarheid zijn niet bekend; controleer die handmatig.

`; } function v61People(){ v61init(current()); return v52head('FASE 3','Personeelsbibliotheek','Instructeurs en vaste groepen bewaren, zonder koppeling met het roosterproject.')+ `
${state.v61People.map(r=>`
${esc(r.name)} · ${esc(r.role)}
`).join('')||'

Geen personen opgeslagen.

'}
`; } function v61AddPerson(){let n=$('v61personName').value.trim();if(!n)return;state.v61People.push({id:crypto.randomUUID(),name:n,role:$('v61personRole').value.trim()});v61Save('Persoon toegevoegd')} function v61CopyPerson(id){let r=state.v61People.find(r=>r.id===id);if(r)navigator.clipboard?.writeText(r.name).catch(()=>prompt('Kopieer naam:',r.name))} function v61RemovePerson(id){if(!confirm('Persoon verwijderen?'))return;state.v61People=state.v61People.filter(r=>r.id!==id);v61Save('Persoon verwijderd')} function v61Packs(){ v61init(current()); return v52head('FASE 3','Materieelpakketten','Herbruikbare standaardpakketten voor oefeningen en activiteiten.')+ `
${state.v61Packs.map(p=>`

${esc(p.name)}

${p.items.map(i=>esc(i.item)+' × '+esc(i.quantity)).join(' · ')}

`).join('')||'

Geen pakketten opgeslagen.

'}
`; } function v61AddPack(){ let name=$('v61packName').value.trim();if(!name)return; let items=$('v61packRows').value.split('\n').filter(Boolean).map(line=>{let [item,quantity,unit]=line.split('|').map(v=>v.trim());return {item,quantity:quantity||'1',unit:unit||'stuks'};}).filter(r=>r.item); if(!items.length){alert('Voeg eerst materieelregels toe.');return} state.v61Packs.push({id:crypto.randomUUID(),name,items});v61Save('Materieelpakket toegevoegd'); } function v61ApplyPack(id){ let p=state.v61Packs.find(p=>p.id===id);if(!p)return; let x=current();v61init(x); for(let item of p.items)x.v52.materials.push({id:crypto.randomUUID(),...item,owner:'',issued:'Nee',returned:'Nee',notes:'Pakket: '+p.name}); v61Save('Materieelpakket toegepast: '+p.name); } function v61DeletePack(id){if(!confirm('Pakket verwijderen?'))return;state.v61Packs=state.v61Packs.filter(p=>p.id!==id);v61Save('Pakket verwijderd')} function v61Safety(){ let x=current();v61init(x); return v52head('FASE 3','Veiligheid per activiteit','Leg risico, maatregel en verantwoordelijke vast per activiteit.')+ `
${x.activities.map(a=>{ let r=x.v61.safety.find(r=>r.activity===a.id)||{}; return `

${esc(a.title)} · ${esc(a.date)}

${[['risk','Risico'],['measure','Beheersmaatregel'],['owner','Verantwoordelijke'],['emergency','Noodmaatregel']].map(([k,l])=>`
`).join('')}
`}).join('')||'

Voeg eerst activiteiten toe.

'}

Deze registratie vervangt geen formele risicoanalyse of bevoegde veiligheidsbeoordeling.

`; } function v61SafetyEdit(activity,key,value){ let x=current();v61init(x);let r=x.v61.safety.find(r=>r.activity===activity); if(!r){r={activity,risk:'',measure:'',owner:'',emergency:''};x.v61.safety.push(r)}r[key]=value;persist(); } function v61DiffRecords(a,b,path='',depth=0){ if(depth>4)return []; if(JSON.stringify(a)===JSON.stringify(b))return []; if(!a||!b||typeof a!=='object'||typeof b!=='object'||Array.isArray(a)!==Array.isArray(b))return [path||'Algemeen']; let changes=[]; if(Array.isArray(a)){ if(a.length!==b.length)changes.push(path+' (aantal '+a.length+' → '+b.length+')'); let idsA=new Map(a.filter(v=>v&&typeof v==='object'&&v.id).map(v=>[v.id,v])),idsB=new Map(b.filter(v=>v&&typeof v==='object'&&v.id).map(v=>[v.id,v])); if(idsA.size&&idsB.size){for(let id of new Set([...idsA.keys(),...idsB.keys()]))changes.push(...v61DiffRecords(idsA.get(id),idsB.get(id),path+' / '+(idsB.get(id)?.title||idsA.get(id)?.title||id.slice(0,6)),depth+1))} else if(JSON.stringify(a)!==JSON.stringify(b))changes.push(path+' (inhoud gewijzigd)'); }else{ for(let k of new Set([...Object.keys(a),...Object.keys(b)])){ if(['versions','v4Audit','changeLog','snapshot','fieldLog','history'].includes(k))continue; changes.push(...v61DiffRecords(a[k],b[k],path?path+' / '+k:k,depth+1)); } } return changes.slice(0,150); } function v61Revisions(){ let x=current();v61init(x); let snap=x.v61.release.snapshot,changes=snap?v61DiffRecords(snap,x):[]; return v52head('FASE 4','Wijzigingen PRO','Vergelijk de actuele oefening met de laatst vrijgegeven momentopname.')+ `

${snap?changes.length+' gewijzigde onderdelen':'Nog geen vrijgegeven momentopname'}

${changes.map(c=>`
${esc(c)}
`).join('')||'

Geen verschillen ten opzichte van de vrijgegeven versie, of er is nog geen vrijgave.

'}

Dit is een vergelijking van velden en aantallen; geen juridisch auditlog of onwijzigbare versiehistorie.

`; } function v61Release(){ let x=current();v61init(x);let r=x.v61.release,problems=[...v4Checks(x).errors,...v53conflictData(x),...v61TimeWarnings(x)]; return v52head('FASE 4','Oefening vrijgeven','Maak een momentopname na menselijke controle; latere wijzigingen worden zichtbaar.')+ `

Status: ${esc(r.status)}

${r.at?esc(r.by)+' · '+esc(r.at):'Nog niet vrijgegeven'}

${problems.map(w=>`
${esc(w)}
`).join('')}

Dit is uitsluitend een interne vaststellingsregistratie; geen digitale handtekening of formele autorisatie. Een vastgestelde oefening blijft bewerkbaar, maar wijzigingen worden gemeld.

`; } function v61DoRelease(){ let x=current(),by=$('v61releaseBy').value.trim();if(!by){alert('Vul de naam van de controleur in.');return} let errors=[...v4Checks(x).errors,...v53conflictData(x),...v61TimeWarnings(x)]; if(errors.length&&!confirm('Er zijn '+errors.length+' aandachtspunten. Toch lokaal vaststellen?'))return; let r=x.v61.release; r.status='Vastgesteld';r.by=by;r.at=new Date().toISOString();r.note=$('v61releaseNote').value; let snap=structuredClone(x);snap.v61.release.snapshot=null;r.snapshot=snap; v61Save('Oefening lokaal vastgesteld door '+by); } function v61ResetRelease(){let x=current();if(!confirm('Status terugzetten naar Concept? De laatste momentopname blijft bewaard.'))return;x.v61.release.status='Concept';v61Save('Status teruggezet naar Concept')} function v61Mobile(){ let x=current(),days=[...new Set(x.activities.map(a=>a.date).filter(Boolean))].sort(); return v52head('FASE 4','Mobiele instructeursweergave','Compact dagprogramma om op een telefoon te bekijken of als HTML te delen.')+ `
${days.map(d=>`

${esc(dateFmt(d))}

${x.activities.filter(a=>a.date===d).sort((a,b)=>a.start.localeCompare(b.start)).map(a=>`
${esc(a.start)}–${esc(a.end)} · ${esc(a.title)}

${esc(a.group)} · ${esc(a.instructors)} · ${esc(a.location||'Nog in te vullen')}

`).join('')}`).join('')||'

Geen activiteiten beschikbaar.

'}
`; } function v61ExportMobile(){ let x=current(),rows=[...x.activities].sort((a,b)=>(a.date+a.start).localeCompare(b.date+b.start)); let html=`${esc(x.name)}

${esc(x.name)}

Alleen-lezen overzicht · ${esc(x.v61?.release?.status||'Concept')}

${rows.map(a=>`
${esc(a.date)} · ${esc(a.start)}–${esc(a.end)}

${esc(a.title)}

${esc(a.group)} · ${esc(a.instructors)}

${esc(a.location||'Nog in te vullen')}

`).join('')}`; download('NWT_mobiel_'+safeName(x.name)+'.html',html,'text/html;charset=utf-8'); } function v61DocumentSections(x){ v61init(x); return x.v61.form.map(a=>{ let cls=a.tone==='Belangrijk'?'v61-red':a.tone==='Aandacht'?'v61-yellow':''; if(a.type==='table')return `

${esc(a.title)}

${(a.rows||[]).map(row=>`${row.map(c=>``).join('')}`).join('')}
${esc(c)}
`; if(a.type==='check')return `

${esc(a.title)}

${String(a.content||'').split('\n').filter(Boolean).map(line=>`

☐ ${esc(line)}

`).join('')}
`; return `
<${a.type==='heading'?'h2':'h3'}>${esc(a.title)}

${esc(a.content)}

`; }).join(''); } function v61DocumentHtml(x){ let base=v5documentHtml(x); let extra=v61DocumentSections(x); let style=``; return base.replace('',style+'').replace('',`

Aanvullende formulieronderdelen

${extra}`); } function v61Documents(){ let x=current();v61init(x); return v52head('FASE 2 + 4','Documentstudio','Een documentpakket met eigen formulieronderdelen en duidelijke afdrukopmaak.')+ `

Open HTML in Word om verder te bewerken of gebruik de browser voor PDF. Native DOCX-generatie is niet ingebouwd.

Voorbeeld eigen formulieronderdelen

${v61DocumentSections(x)||'

Voeg eerst onderdelen toe in de formulierontwerper.

'}
`; } function v61ExportDocument(){let x=current();download('NWT_Compleet_'+safeName(x.name)+'.html',v61DocumentHtml(x),'text/html;charset=utf-8')} function v61Audit(){ let x=current();v61init(x);let issues=[]; if(!state.exercises?.length)issues.push('Geen oefeningen'); if(!x.start||!x.end)issues.push('Start- of einddatum ontbreekt'); issues.push(...v61TimeWarnings(x),...v53conflictData(x),...v52dependencyWarnings(x)); if(x.v61.release.status==='Vastgesteld'&&x.v61.release.snapshot&&v61DiffRecords(x.v61.release.snapshot,x).length)issues.push('Er zijn wijzigingen sinds de laatste vaststelling'); return v52head('FASE 5','Systeem- en oefencontrole','Overzicht van ingevoerde gegevens, controles en bekende beperkingen.')+ `

${issues.length} aandachtspunten

${issues.map(i=>`
${esc(i)}
`).join('')||'

Geen signalen binnen de beschikbare regels.

'}

De app is lokaal en werkt zonder account of server. Browseropslag is niet geschikt als enige archief. Test import en export met eigen documenten.

`; } const v61OldRender=render; render=function(){let x=current();if(x)v61init(x);v61OldRender();if(!x)return; const views={v61forms:v61Forms,v61planning:v61Planning,v61people:v61People,v61packs:v61Packs,v61safety:v61Safety,v61revisions:v61Revisions,v61release:v61Release,v61mobile:v61Mobile,v61documents:v61Documents,v61audit:v61Audit}; if(views[tab])$('app').innerHTML=toolbar()+views[tab](); }; // V6.2 visual shell; retains existing feature routes and data model. const cmdNavGroups=[ ['COMMANDO',[['dashboard','◈','Command Center'],['v61audit','◉','Situatie & controle'],['v61planning','◷','Planningcontrole']]], ['OEFENING OPBOUWEN',[['builder','▤','Basisgegevens'],['v61forms','▣','Formulierontwerper'],['activities','▦','Activiteiten'],['v5battle','◷','Battle Rhythm'],['v54composer','⊞','Oefening samenstellen']]], ['INVOER & SJABLONEN',[['v56import','⇩','Importeer voorbeeld / Excel'],['v53templates','▧','Sjablonen PRO'],['v57library','▤','Kennisbibliotheek'],['v55reuse','⧉','Bestaand voorbeeld']]], ['UITVOERING',[['v61people','♙','Personeel'],['v61packs','▣','Materieelpakketten'],['v61safety','⬡','Veiligheid'],['v52live','▶','Uitvoering'],['v52tasks','☑','Acties']]], ['DOCUMENTEN & BEHEER',[['v61documents','▤','Documentstudio'],['v61mobile','▯','Mobiele weergave'],['v61revisions','↺','Wijzigingen'],['v61release','✓','Vrijgave'],['v57backup','⬇','Back-up & herstel'],['manage','⚙','Beheer']]], ['ALLE FUNCTIES',[['cmdall','☷','Alle modules']]] ]; function cmdNav(){return `
Oefenverloop◈ NWT COMMANDOEFENVERLOOP CENTER · V6.2
`+cmdNavGroups.map(([title,items])=>`
${title}
${items.map(([id,icon,label])=>``).join('')}`).join('')} function cmdAllModules(){return `
MODULE OVERZICHT

Alle beschikbare modules

Alle eerdere functies blijven beschikbaar. Kies een onderdeel.

${tabs.filter(([id])=>id!=='dashboard'&&id!=='cmdall').map(([id,label])=>``).join('')}
`} function cmdDashboard(x){ v61init(x);let q=v4Checks(x),warnings=[...q.errors,...q.warnings,...v61TimeWarnings(x)]; let acts=[...x.activities].sort((a,b)=>(a.date+a.start).localeCompare(b.date+b.start)); let complete=acts.filter(a=>a.status==='Afgerond').length; let readiness=x.v61.release.status==='Vastgesteld'?'VASTGESTELD':warnings.length?'AANDACHT NODIG':'CONCEPT'; return `
NWT / OPERATIONS / COMMAND CENTER
OPERATIONEEL OEFENOVERZICHT · V6.2

${esc(x.name)}

${esc(x.code||'GEEN REFERENTIE')}   |   ${dateFmt(x.start)} – ${dateFmt(x.end)}   |   ${esc(x.location||'Locatie nog in te vullen')}

${x.days.length}Oefendagen
${acts.length}Geplande activiteiten
${complete}/${acts.length}Activiteiten afgerond
${warnings.length}Signalen / aandacht

◷ Operatieplanning · eerstvolgende onderdelen

${acts.slice(0,8).map(a=>`
${esc(a.start||'--:--')}–${esc(a.end||'--:--')}
${esc(a.title||'Activiteit zonder naam')}${dateFmt(a.date)} · ${esc(a.group||'Groep onbekend')} · ${esc(a.location||'Locatie niet ingevuld')}
${esc(a.status||'Gepland')}
`).join('')||'

Nog geen activiteiten. Begin met het importeren van een bestaand oefenverloop of voeg zelf activiteiten toe.

'}

⬡ Operationele status

STATUS: ${readiness}
${warnings.slice(0,4).map(w=>`
${esc(w)}
`).join('')||'

Geen meldingen vanuit de ingevulde gegevens.

'}

◈ Snel naar

`; } const cmdPriorRender=render; render=function(){cmdPriorRender();let x=current();$('nav').innerHTML=cmdNav();if(!x)return; if(tab==='dashboard')$('app').innerHTML=`
LOKAAL · OFFLINE
`+cmdDashboard(x); if(tab==='cmdall')$('app').innerHTML=toolbar()+cmdAllModules(); }; // V6.3 — direct table editing with live document preview; uses the existing data model. const sebLessonDefaults=[{name:'Navigeren',duration:90,material:'Kaart, kompas',description:''},{name:'ZHKH',duration:90,material:'ZHKH-materieel',description:''},{name:'Strong',duration:60,material:'Sportmateriaal',description:''},{name:'Bivak',duration:120,material:'Bivakmateriaal',description:''},{name:'Briefing',duration:30,material:'',description:''},{name:'Pauze',duration:15,material:'',description:''}]; let sebDay='',sebFocus=''; function sebDates(x){let dates=new Set([...(x.days||[]).map(d=>d.date),...(x.activities||[]).map(a=>a.date)]);if(x.start)dates.add(x.start);return [...dates].filter(d=>/^\d{4}-\d{2}-\d{2}$/.test(d)).sort()} function sebActiveDay(x){let days=sebDates(x);if(!sebDay||!days.includes(sebDay))sebDay=days[0]||'';return sebDay} function sebMinute(t){return minuteCount(t)} function sebTime(min){return `${String(Math.floor(min/60)).padStart(2,'0')}:${String(min%60).padStart(2,'0')}`} function sebSetDay(d){sebDay=d;render()} function sebAddDay(){let x=current(),d=prompt('Datum van nieuwe oefendag (JJJJ-MM-DD):',x.start||new Date().toISOString().slice(0,10));if(!d)return;if(!/^\d{4}-\d{2}-\d{2}$/.test(d)||Number.isNaN(Date.parse(d))){alert('Vul een geldige datum in als JJJJ-MM-DD.');return}if(!x.days.some(r=>r.date===d))x.days.push({id:crypto.randomUUID(),date:d,title:'Oefendag',briefing:'',notes:''});if(!x.start||dx.end)x.end=d;sebDay=d;log('Oefendag toegevoegd: '+d);render()} function sebAddActivity(preset=''){let x=current(),d=sebActiveDay(x);if(!d){sebAddDay();d=sebActiveDay(x);if(!d)return}let lesson=sebLessonDefaults.find(l=>l.name===preset),arr=x.activities.filter(a=>a.date===d).sort((a,b)=>(a.start||'').localeCompare(b.start||'')),last=arr.at(-1),start=last?.end&&sebMinute(last.end)!==null?last.end:'08:00',duration=lesson?.duration||60,end=sebMinute(start)+duration;if(end>=1440)start='08:00',end=480+duration; let a={id:crypto.randomUUID(),date:d,start,end:sebTime(end),title:lesson?.name||'',group:'',instructors:'',location:'',material:lesson?.material||'',description:lesson?.description||'',status:'Gepland'};x.activities.push(a);log('Activiteit toegevoegd aan '+d);render();sebFocus=a.id} function sebEdit(id,key,value){let a=current()?.activities.find(a=>a.id===id);if(!a)return;a[key]=value;if(key==='start'&&sebMinute(value)!==null){let dur=a.sebDuration||60;if(sebMinute(value)+dur<1440)a.end=sebTime(sebMinute(value)+dur)}if(key==='end'&&sebMinute(a.start)!==null&&sebMinute(value)>sebMinute(a.start))a.sebDuration=sebMinute(value)-sebMinute(a.start);persist();sebUpdatePreview();sebUpdateWarnings()} function sebDuplicate(id){let x=current(),a=x.activities.find(a=>a.id===id);if(!a)return;let c=structuredClone(a);c.id=crypto.randomUUID();c.start=a.end||a.start;let dur=(sebMinute(a.end)||0)-(sebMinute(a.start)||0);if(dur>0&&sebMinute(c.start)!==null&&sebMinute(c.start)+dur<1440)c.end=sebTime(sebMinute(c.start)+dur);x.activities.push(c);log('Activiteit gedupliceerd');render()} function sebRemove(id){if(!confirm('Deze activiteit verwijderen?'))return;let x=current();x.activities=x.activities.filter(a=>a.id!==id);log('Activiteit verwijderd');render()} function sebCopyDay(){let x=current(),d=sebActiveDay(x),target=prompt('Kopieer alle activiteiten naar datum (JJJJ-MM-DD):','');if(!target)return;if(!/^\d{4}-\d{2}-\d{2}$/.test(target)||Number.isNaN(Date.parse(target))){alert('Ongeldige datum.');return}let src=x.activities.filter(a=>a.date===d);if(!src.length){alert('Deze dag heeft nog geen activiteiten.');return}if(!x.days.some(a=>a.date===target))x.days.push({id:crypto.randomUUID(),date:target,title:'Oefendag (kopie)',briefing:'',notes:''});x.activities.push(...src.map(a=>({...structuredClone(a),id:crypto.randomUUID(),date:target,status:'Gepland'})));if(!x.start||targetx.end)x.end=target;sebDay=target;log(src.length+' activiteiten gekopieerd naar '+target);render()} function sebWarnings(x){let d=sebActiveDay(x),rows=x.activities.filter(a=>a.date===d),issues=[];for(let a of rows){if(!a.title?.trim())issues.push('Activiteit zonder naam');if(!a.location?.trim())issues.push('Locatie ontbreekt bij '+(a.title||'nieuwe activiteit'));if(!a.instructors?.trim()&&a.title!=='Pauze')issues.push('Instructeur ontbreekt bij '+(a.title||'nieuwe activiteit'));if(sebMinute(a.start)===null||sebMinute(a.end)===null||sebMinute(a.end)<=sebMinute(a.start))issues.push('Ongeldige tijd bij '+(a.title||'nieuwe activiteit'))}for(let i=0;iv.trim().toLowerCase()).filter(Boolean),bi=(b.instructors||'').split(/[,;]+/).map(v=>v.trim().toLowerCase()).filter(Boolean);for(let n of ai)if(bi.includes(n))issues.push('Instructeur dubbel gepland: '+n)}}return [...new Set(issues)]} function sebPreview(x){let d=sebActiveDay(x),rows=x.activities.filter(a=>a.date===d).sort((a,b)=>(a.start||'').localeCompare(b.start||''));return `
NWT · OEFENVERLOOP

${esc(x.name)}

${esc(x.code||'CONCEPT')} · ${esc(d||'Datum niet ingevuld')}

Dagprogramma · ${esc(d||'Nog in te vullen')}

${rows.map(a=>``).join('')||''}
TijdActiviteitGroepInstructeurLocatie
${esc(a.start)}–${esc(a.end)}${esc(a.title||'Nog in te vullen')}${esc(a.group)}${esc(a.instructors)}${esc(a.location||'—')}
Nog geen activiteiten

Werkdocument · live voorbeeld · lokaal opgeslagen

`} function sebUpdatePreview(){let x=current();if(!x)return;let p=$('sebPreview');if(p)p.innerHTML=sebPreview(x)} function sebUpdateWarnings(){let x=current(),e=$('sebWarnings');if(!x||!e)return;let issues=sebWarnings(x);e.innerHTML=issues.length?issues.slice(0,10).map(i=>`
⚠ ${esc(i)}
`).join(''):`
✓ Geen ontbrekende basisvelden of overlappende groeps-/instructeurinzet op deze dag.
`;let c=$('sebWarningCount');if(c)c.textContent=issues.length+' aandachtspunten'} function sebCreateNew(){if(!confirm('Een nieuwe oefening aanmaken? De huidige oefening blijft bewaard.'))return;create();tab='smartbuilder';sebDay='';render()} function sebView(x){let d=sebActiveDay(x),dates=sebDates(x),rows=x.activities.filter(a=>a.date===d).sort((a,b)=>(a.start||'').localeCompare(b.start||'')),issues=sebWarnings(x);return `
NWT OPERATIONS CENTER / OEFENING BOUWEN / SMART BUILDER V6.3

${esc(x.name)}

${esc(x.code||'Geen referentienummer')} · ${esc(x.start||'Startdatum ontbreekt')} t/m ${esc(x.end||'Einddatum ontbreekt')} · Status: ${esc(x.v61?.release?.status||'Concept')}
${['1 · Basis','2 · Dagen','3 · Activiteiten','4 · Personeel','5 · Veiligheid','6 · Controle'].map((v,i)=>`${v}`).join('')}

Oefening snel aanpassen

▦ Activiteiten rechtstreeks invullen

${dates.map((date,i)=>``).join('')}
${rows.map(a=>{let dur=sebMinute(a.start)!==null&&sebMinute(a.end)!==null?sebMinute(a.end)-sebMinute(a.start):0;return `${[['start','time'],['end','time'],['title','text'],['group','text'],['instructors','text'],['location','text']].map(([k,t])=>``).join('')}`}).join('')||''}
Tijd vanTotActiviteit / lesGroepInstructeur(s)LocatieDuurActies
${dur>0?dur+' min':'—'}
Nog geen activiteiten. Kies hieronder een les of klik op Activiteit toevoegen.
${[...new Set(x.activities.map(a=>a.location).filter(Boolean))].map(v=>`${[...new Set([...(state.v61People||[]).map(p=>p.name),...x.activities.map(a=>a.instructors)].filter(Boolean))].map(v=>`${[...new Set([...sebLessonDefaults.map(l=>l.name),...x.activities.map(a=>a.title)].filter(Boolean))].map(v=>`

Snelle les toevoegen

${sebLessonDefaults.map(l=>``).join('')}

Een nieuwe les krijgt automatisch een voorstel voor starttijd en duur. Je kunt alle velden direct aanpassen.

◉ Live documentvoorbeeld

${sebPreview(x)}

⬡ Controles · ${issues.length} aandachtspunten

${issues.slice(0,10).map(i=>`
⚠ ${esc(i)}
`).join('')||'
✓ Geen basiswaarschuwingen op deze dag.
'}

Snelle acties

Wijzigingen worden lokaal opgeslagen. Maak regelmatig een downloadbare JSON-back-up.

`} // V6.4: layout inspired by the approved screenshot, without hard-coded fictitious exercise data. const sebV63View=sebView; sebView=function(x){ let html=sebV63View(x),d=sebActiveDay(x),rows=x.activities.filter(a=>a.date===d); let issues=sebWarnings(x),filled=rows.filter(a=>a.title&&a.start&&a.end&&a.group&&a.location&&a.instructors).length; let pct=rows.length?Math.round(filled/rows.length*100):0; html=html.replace('
',`
⬡ NWT OPERATIONS CENTER · V6.4● LOKAAL · OFFLINE
`); html=html.replace('
'); html=html.replace('

Snelle acties

',`

GEREEDHEID & SNELACTIES

${pct}% ingevuld

${filled} van ${rows.length} activiteiten hebben alle basisvelden.

`); html=html.replace('

Snelle les toevoegen

','

+ SNELLE LESSENBIBLIOTHEEK

'); return `
${html}
`; }; // The builder is an extra route. All existing V6.2 modules stay intact. tabs.push(['smartbuilder','Smart Exercise Builder']); cmdNavGroups[1][1].unshift(['smartbuilder','▦','Oefening bouwen · SMART']); const sebPriorRender=render; render=function(){sebPriorRender();let x=current();if(!x)return;if(tab==='smartbuilder')$('app').innerHTML=toolbar()+sebView(x)}; // V6.6 weekeditie: quick lesson library, bulk group copy, clipboard import, // full exercise check, local evaluation, session snapshot and fast access. function weekInit(){state.weekLessons??=[];let x=current();if(x){x.weekEvaluation??=[];x.weekSnapshots??=[]}} function weekSnapshot(){weekInit();let x=current();x.weekSnapshots.unshift({at:new Date().toISOString(),data:structuredClone({...x,weekSnapshots:[]})});x.weekSnapshots=x.weekSnapshots.slice(0,5);log('Herstelmoment opgeslagen');render()} function weekRestore(i){let x=current(),r=x?.weekSnapshots?.[i];if(!r||!confirm('Dit herstelmoment terugzetten? Maak eerst een JSON-back-up.'))return;let copy=structuredClone(r.data);copy.weekSnapshots=x.weekSnapshots;let at=state.exercises.findIndex(v=>v.id===x.id);state.exercises[at]=copy;log('Herstelmoment teruggezet');render()} function weekAddLesson(){let name=prompt('Naam van de standaardles:');if(!name?.trim())return;let duration=Number(prompt('Standaardduur in minuten:', '60'));if(!Number.isFinite(duration)||duration<5||duration>720){alert('Gebruik een duur van 5 tot 720 minuten.');return}let material=prompt('Standaardmaterieel (optioneel):','')||'';state.weekLessons.push({id:crypto.randomUUID(),name:name.trim(),duration,material});log('Les toegevoegd aan bibliotheek');render()} function weekAddFromLesson(id){let lesson=state.weekLessons.find(l=>l.id===id);if(!lesson)return;let x=current(),d=sebActiveDay(x);if(!d){alert('Voeg eerst een oefendag toe.');return}let rows=x.activities.filter(a=>a.date===d).sort((a,b)=>(a.end||'').localeCompare(b.end||''));let last=rows.at(-1),start=last?.end||'08:00',m=sebMinute(start);if(m===null||m+lesson.duration>1439){start='08:00';m=480}x.activities.push({id:crypto.randomUUID(),date:d,start,end:sebTime(m+lesson.duration),title:lesson.name,group:'',instructors:'',location:'',material:lesson.material,description:'',status:'Gepland'});log('Les toegevoegd: '+lesson.name);render()} function weekCopyGroups(){let x=current(),d=sebActiveDay(x);let group=prompt('Bron-groep (exact zoals ingevuld):','Alpha');if(!group)return;let targets=prompt('Kopieer naar groepen (komma-gescheiden):','Bravo, Charlie');if(!targets)return;let src=x.activities.filter(a=>a.date===d&&a.group?.trim().toLowerCase()===group.trim().toLowerCase());if(!src.length){alert('Geen activiteiten gevonden voor deze groep op de gekozen dag.');return}let names=[...new Set(targets.split(',').map(v=>v.trim()).filter(Boolean))].filter(v=>v.toLowerCase()!==group.toLowerCase());if(!names.length)return;if(!confirm(src.length+' activiteiten kopiëren naar '+names.join(', ')+'? Instructeurs en locaties worden leeggelaten om dubbele inzet te voorkomen.'))return;for(let name of names)for(let a of src)x.activities.push({...structuredClone(a),id:crypto.randomUUID(),group:name,instructors:'',location:'',status:'Gepland'});log('Groepskopie: '+names.join(', '));render()} function weekPasteRows(){let raw=prompt('Plak Excel-regels met TAB tussen kolommen: start, eind, activiteit, groep, instructeur, locatie. Eén activiteit per regel.');if(!raw)return;let d=sebActiveDay(current());if(!d){alert('Voeg eerst een oefendag toe.');return}let lines=raw.trim().split(/\r?\n/).filter(Boolean);let items=[];for(let [i,line] of lines.entries()){let cells=line.split('\t');if(cells.length<3){alert('Regel '+(i+1)+' bevat minder dan 3 kolommen. Niets geïmporteerd.');return}let [start,end,title,group='',instructors='',location='']=cells.map(v=>v.trim());if(sebMinute(start)===null||sebMinute(end)===null||sebMinute(end)<=sebMinute(start)||!title){alert('Ongeldige tijden of activiteit op regel '+(i+1)+'. Niets geïmporteerd.');return}items.push({id:crypto.randomUUID(),date:d,start,end,title,group,instructors,location,material:'',description:'',status:'Gepland'})}if(!confirm(items.length+' activiteiten toevoegen aan '+d+'?'))return;current().activities.push(...items);log(items.length+' regels geplakt');render()} function weekQuality(x){let issues=[];for(let a of x.activities){let label=(a.date||'?')+' · '+(a.title||'naamloos');if(!a.title?.trim())issues.push(label+': activiteit zonder naam');if(!a.group?.trim())issues.push(label+': groep ontbreekt');if(!a.location?.trim())issues.push(label+': locatie ontbreekt');if(!a.instructors?.trim()&&!/pauze|ontbijt|lunch|diner/i.test(a.title||''))issues.push(label+': instructeur ontbreekt');if(sebMinute(a.start)===null||sebMinute(a.end)===null||sebMinute(a.end)<=sebMinute(a.start))issues.push(label+': ongeldige tijd')} let acts=x.activities;for(let i=0;iv===null)||as>=be||bs>=ae)continue;if(a.group&&b.group&&a.group.toLowerCase()===b.group.toLowerCase())issues.push(a.date+': groep '+a.group+' dubbel ingepland');let an=(a.instructors||'').split(/[,;]+/).map(v=>v.trim().toLowerCase()).filter(Boolean),bn=(b.instructors||'').split(/[,;]+/).map(v=>v.trim().toLowerCase()).filter(Boolean);if(an.some(v=>bn.includes(v)))issues.push(a.date+': instructeur dubbel ingepland: '+a.title+' / '+b.title);if(a.location&&b.location&&a.location.toLowerCase()===b.location.toLowerCase())issues.push(a.date+': locatie mogelijk dubbel: '+a.location)}return [...new Set(issues)]} function weekEvaluationAdd(){let x=current(),category=$('weekCat').value,body=$('weekText').value.trim();if(!body)return;x.weekEvaluation.push({id:crypto.randomUUID(),at:new Date().toISOString(),category,body});log('Evaluatiepunt toegevoegd');render()} function weekView(){weekInit();let x=current(),issues=weekQuality(x);return `

WEEKEDITIE · OEFENING GEREEDMAKEN

Werk sneller, controleer de volledige oefening en maak een herstelmoment vóór grote wijzigingen.

SNEL INVULLEN

Eigen lessenbibliotheek

${state.weekLessons.map(l=>``).join('')||'

Voeg je eigen lessen toe. Er worden geen onbekende gegevens verzonnen.

'}

Herstelmomenten

${x.weekSnapshots.map((r,i)=>`
${esc(new Date(r.at).toLocaleString('nl-NL'))}
`).join('')||'

Nog geen herstelmomenten.

'}

CONTROLE HELE OEFENING · ${issues.length} aandachtspunten

${issues.map(v=>`
⚠ ${esc(v)}
`).join('')||'
Geen signalen op de ingevulde velden.
'}

Controleer zelf ook veiligheidsmaatregelen, kwalificaties, beschikbaarheid en uitvoerbaarheid.

EVALUATIE & LESSONS LEARNED

${x.weekEvaluation.map(r=>`
${esc(r.category)} · ${esc(r.body)}
`).join('')}
`} tabs.push(['weekready','Weekeditie · gereedmaken']);cmdNavGroups[1][1].unshift(['weekready','✓','Weekeditie · gereedmaken']);const weekOldRender=render;render=function(){weekInit();weekOldRender();if(tab==='weekready'&¤t())$('app').innerHTML=toolbar()+weekView()}; // V6.7 visual drag/drop builder. Uses the existing local exercise store. let vdSelected='',vdDate=''; function vdDates(x){return sebDates(x)} function vdCurrentDate(x){let a=vdDates(x);if(!a.includes(vdDate))vdDate=a[0]||'';return vdDate} function vdHours(x){let day=vdCurrentDate(x),items=x.activities.filter(a=>a.date===day),h=new Set();for(let n=6;n<=22;n++)h.add(n);for(let a of items){let m=sebMinute(a.start);if(m!==null)h.add(Math.floor(m/60))}return [...h].sort((a,b)=>a-b)} function vdAdd(name='',duration=60){let x=current(),d=vdCurrentDate(x);if(!d){sebAddDay();d=vdCurrentDate(x);if(!d)return}let start='08:00',items=x.activities.filter(a=>a.date===d).sort((a,b)=>(a.end||'').localeCompare(b.end||'')),last=items.at(-1);if(last?.end&&sebMinute(last.end)+duration<1440)start=last.end;let end=sebTime(sebMinute(start)+duration);let a={id:crypto.randomUUID(),date:d,start,end,title:name||'Nieuwe activiteit',group:'Alpha',instructors:'',location:'',material:'',description:'',status:'Gepland'};x.activities.push(a);vdSelected=a.id;log('Activiteit visueel toegevoegd');render()} function vdChoose(id){vdSelected=id;render()} function vdEdit(key,value){let a=current()?.activities.find(a=>a.id===vdSelected);if(!a)return;a[key]=value;persist();if(['date','start','end','group'].includes(key)){render();return}let card=document.querySelector('.vd-item.selected');if(card){let title=card.querySelector('strong');if(title)title.textContent=a.title||'Nieuwe activiteit'}let prev=document.getElementById('vdPreview');if(prev)prev.innerHTML=sebPreview(current())} function vdDrop(event,group,hour){event.preventDefault();let id=event.dataTransfer.getData('text/plain'),a=current()?.activities.find(a=>a.id===id);if(!a)return;let duration=(sebMinute(a.end)||0)-(sebMinute(a.start)||0);if(duration<=0||duration>720)duration=60;let m=hour*60;if(m+duration>=1440){alert('De activiteit past niet in deze dag.');return}a.date=vdCurrentDate(current());a.group=group;a.start=sebTime(m);a.end=sebTime(m+duration);vdSelected=id;log('Activiteit verplaatst naar '+group+' om '+a.start);render()} function vdCopy(){if(!vdSelected)return;sebDuplicate(vdSelected)} function vdDelete(){if(!vdSelected)return;let id=vdSelected;vdSelected='';sebRemove(id)} function vdFields(a){if(!a)return '

Klik op een activiteit in het opbouwveld. Hier kun je alle gegevens rechtstreeks aanpassen.

'; let field=(label,key,type='text')=>``; return `${field('Activiteit / les','title')}${field('Datum','date','date')}
${field('Begin','start','time')}
${field('Einde','end','time')}
${field('Instructeur(s)','instructors')}${field('Locatie','location')}${field('Materieel','material')}
`} function vdView(x){let d=vdCurrentDate(x),dates=vdDates(x),items=x.activities.filter(a=>a.date===d),selected=x.activities.find(a=>a.id===vdSelected),hours=vdHours(x),warnings=sebWarnings(x); let head=`
TIJD
${['Alpha','Bravo','Charlie'].map(g=>`
${g.toUpperCase()}
`).join('')}
`; let lanes=hours.map(h=>`
${String(h).padStart(2,'0')}:00
${['Alpha','Bravo','Charlie'].map(g=>`
${items.filter(a=>(a.group||'').toLowerCase()===g.toLowerCase()&&Math.floor((sebMinute(a.start)||0)/60)===h).map(a=>`
${esc(a.title)}${esc(a.start)}–${esc(a.end)} · ${esc(a.location||'Locatie nog invullen')}${esc(a.instructors||'Instructeur nog invullen')}
`).join('')}
`).join('')}
`).join(''); let unassigned=items.filter(a=>!['alpha','bravo','charlie'].includes((a.group||'').toLowerCase())); return `
NWT OPERATIONS CENTER · VISUELE DESKTOP V6.8

${esc(x.name)}

Oefenplanning · voorbereiding · uitvoering · evaluatie
1 · Opbouwveld2 · Personeel3 · Materieel4 · Veiligheid5 · Controle6 · Vrijgeven
${items.length} activiteiten · ${warnings.length} aandachtspunten
${dates.length}Oefendagen
${x.activities.length}Activiteiten totaal
${items.length}Activiteiten vandaag
${warnings.length}Aandachtspunten

VISUEEL OPBOUWVELD · ${esc(d||'KIES EERST EEN DAG')}

${dates.map(v=>``).join('')||'Voeg een oefendag toe om te beginnen.'}
${head}${lanes}
${unassigned.length?`
Nog niet aan Alpha/Bravo/Charlie toegewezen: ${unassigned.map(a=>esc(a.title)).join(', ')}. Pas de groep rechts aan.
`:''}

Sleep een kaart naar een ander uurvak. Voor minutenprecisie pas je de tijd rechts aan. Bij een lange activiteit staat de kaart in het startuur.

`} tabs.push(['visualdesktop','Visueel desktop opbouwveld']);cmdNavGroups[1][1].unshift(['visualdesktop','▣','Visueel opbouwveld · DESKTOP']);const vdOldRender=render;render=function(){vdOldRender();if(tab==='visualdesktop'&¤t())$('app').innerHTML=toolbar()+vdView(current())}; // Set the quick workflow as the first page in the command center without changing saved data. render();