TIDLETT

Tid. Projekt. Kontroll.

Logga in

Testversion v160 – anslutning till befintlig Supabase-inloggning.

Kontrollerar inloggning …

TIDLETT / ARBETSYTA

Företagsöversikt

`; } // v123: ÄTA uses the same direct PDF download as offers and work diaries. async function downloadATAPDF(id){ const a=ataItems().find(x=>String(x.id)===String(id)); if(!a)return alert('ÄTA saknas.'); const doc=new DOMParser().parseFromString(ataDocument(a),'text/html'); const sheet=doc.querySelector('.sheet'); if(!sheet)return alert('Kunde inte skapa ÄTA-dokumentet.'); const style=document.createElement('style'); style.id='tidlett-ata-export-style'; // Scoped rules prevent the ÄTA styling from changing the surrounding app. style.textContent=` .ata-pdf-export{box-sizing:border-box!important;font:15px/1.6 system-ui,-apple-system,Segoe UI,sans-serif!important;color:#303433!important;background:#fff!important;width:780px!important;max-width:780px!important;margin:0!important;padding:44px 48px!important;box-shadow:none!important} .ata-pdf-export *{box-sizing:border-box} .ata-pdf-export .top{display:flex;justify-content:space-between;align-items:flex-start;border-bottom:2px solid #ddc69e;padding-bottom:24px} .ata-pdf-export .brand{font-weight:750;font-size:23px;letter-spacing:2px} .ata-pdf-export .muted{color:#777c78} .ata-pdf-export .eyebrow{font-size:11px;letter-spacing:2px;color:#9a8056;text-transform:uppercase} .ata-pdf-export h1{font-size:30px;line-height:1.2;margin:24px 0 8px} .ata-pdf-export h2{font-size:16px;margin-top:30px} .ata-pdf-export p{white-space:pre-wrap;overflow-wrap:anywhere} .ata-pdf-export .grid{display:grid;grid-template-columns:1fr 1fr;gap:16px 24px;margin-top:25px} .ata-pdf-export .field{border-bottom:1px solid #eee8de;padding-bottom:12px} .ata-pdf-export .field b{display:block;margin-top:3px} .ata-pdf-export .price{background:#f5f0e6;border-radius:10px;padding:19px 22px;margin-top:26px;display:flex;justify-content:space-between;align-items:center} .ata-pdf-export .price strong{font-size:25px} .ata-pdf-export .signatures{display:grid;grid-template-columns:1fr 1fr;gap:36px;margin-top:75px} .ata-pdf-export .signature{border-top:1px solid #bbb;padding-top:9px;font-size:12px;color:#666} .ata-pdf-export .notice{font-size:12px;color:#777;margin-top:26px}`; sheet.classList.add('report','ata-pdf-export'); document.head.appendChild(style); try{ return await tidlettExportPdf(sheet.outerHTML,'ATA_'+ataLabel(a)+'_'+a.title); }finally{style.remove()} } function printATA(id){ const a=ataItems().find(x=>String(x.id)===String(id));if(!a)return alert('ÄTA saknas.'); const w=window.open('','_blank');if(!w)return alert('Tillåt popupfönster för att öppna utskriftsvyn.'); w.document.open();w.document.write(ataDocument(a));w.document.close(); } function previewNewATA(button){ try{ const form=button.closest('form'); if(!form.reportValidity())return; const f=new FormData(form),projectId=Number(f.get('projectId')); const title=String(f.get('title')||'').trim(),amount=Number(f.get('amount')); if(!title||!project(projectId)||project(projectId).quoteOnly||project(projectId).archived||project(projectId).completedAt||!Number.isFinite(amount)||amount<0)return alert('Fyll i projekt, rubrik och giltigt belopp.'); const vatMode=String(f.get('vatMode')||'ex'),vatRate=25,vatFactor=vatMode==='inc'?1+vatRate/100:1;const a={vatMode,vatRate,id:Date.now()+Math.floor(Math.random()*1000),projectId,ataNumber:nextATANumber(projectId),title,amount:String(f.get('priceType'))==='running'?0:amount/vatFactor,priceType:String(f.get('priceType')||'fixed'),hourlyRate:Number(f.get('hourlyRate')||0)/vatFactor,runningTerms:String(f.get('runningTerms')||''),status:'Utkast',description:String(f.get('description')||'').trim(),created:new Date().toISOString().slice(0,10)}; ataItems().push(a); try{save()}catch(err){console.warn('Kunde inte spara ÄTA lokalt:',err)} previewATA(a.id); }catch(err){console.error('ÄTA-förhandsgranskning:',err);alert('Kunde inte öppna förhandsgranskningen: '+err.message)} } function safePreviewATA(id){try{previewATA(id)}catch(err){console.error(err);alert('Förhandsgranskningen kunde inte öppnas: '+err.message)}} function previewATA(id){ const a=ataItems().find(x=>String(x.id)===String(id));if(!a)return alert('ÄTA saknas.'); document.getElementById('ataPreviewOverlay')?.remove(); const p=project(a.projectId),overlay=document.createElement('div');overlay.id='ataPreviewOverlay'; overlay.style.cssText='position:fixed;inset:0;background:#202522b8;z-index:99999;overflow:auto;padding:20px;'; overlay.innerHTML=`

Förhandsgranska ${esc(ataLabel(a))}

Redigera uppgifterna och se PDF-underlaget innan du skriver ut eller skickar.

Förhandsvisning av A4-dokumentet
`; document.body.appendChild(overlay); const style=document.createElement('style');style.id='ataPreviewResponsive';style.textContent='@media(max-width:850px){.ata-preview-grid{grid-template-columns:1fr!important}}';document.head.appendChild(style); try{toggleATAPreviewPrice();toggleATAPreviewVat();refreshATAPreview()}catch(err){console.error(err);alert('Dokumentet kunde inte visas: '+err.message)} } function closeATAPreview(){document.getElementById('ataPreviewOverlay')?.remove();document.getElementById('ataPreviewResponsive')?.remove()} function toggleATAPreviewPrice(){const f=document.getElementById('ataPreviewForm');if(!f)return;const running=f.elements.priceType.value==='running';document.getElementById('ataPreviewFixed').style.display=running?'none':'';document.getElementById('ataPreviewRunning').style.display=running?'':'none';refreshATAPreview()} function toggleATAPreviewVat(){const f=document.getElementById('ataPreviewForm');if(!f)return;const incl=f.elements.vatMode.value==='inc';document.querySelector('#ataPreviewFixed label').textContent='Fast pris '+(incl?'inkl.':'exkl.')+' moms (kr)';document.querySelector('#ataPreviewRunning label').textContent='Timpris '+(incl?'inkl.':'exkl.')+' moms (kr/tim)';refreshATAPreview()} function ataPreviewValues(){ const f=document.getElementById('ataPreviewForm');if(!f)return null; const a=ataItems().find(x=>String(x.id)===String(f.elements.id.value));if(!a)return null; const v={...a};for(const key of ['title','company','customer','contact','description','scope','schedule','terms'])v[key]=f.elements[key].value; v.vatMode=f.elements.vatMode.value;v.vatRate=25;const factor=v.vatMode==='inc'?1+ataVatRate(v):1;v.priceType=f.elements.priceType.value;v.amount=v.priceType==='running'?0:Number(f.elements.amount.value)/factor;v.hourlyRate=Number(f.elements.hourlyRate.value||0)/factor;v.runningTerms=f.elements.runningTerms.value;return v; } function refreshATAPreview(){const a=ataPreviewValues(),frame=document.getElementById('ataPreviewFrame');if(!a||!frame)return;frame.srcdoc=ataDocument(a).replace('',' .toolbar{display:none!important}.sheet{margin:0 auto;box-shadow:none}')} function saveATAPreview(openPDF){ const v=ataPreviewValues();if(!v||!v.title.trim()||!Number.isFinite(v.amount)||v.amount<0)return alert('Fyll i rubrik och giltigt pris.'); const a=ataItems().find(x=>String(x.id)===String(v.id));if(!a)return alert('ÄTA saknas.'); Object.assign(a,v);save();closeATAPreview();render();if(openPDF)downloadATAPDF(a.id);else showToast('ÄTA sparad'); } // Separate confirmation dialog: deletion only occurs after an explicit second click. window.openATADeleteConfirm=function(id){ const a=ataItems().find(x=>String(x.id)===String(id)); if(!a){alert('ÄTA hittades inte.');return;} document.getElementById('ataDeleteConfirmOverlay')?.remove(); const overlay=document.createElement('div'); overlay.id='ataDeleteConfirmOverlay'; overlay.style.cssText='position:fixed;inset:0;z-index:100010;background:rgba(24,30,27,.68);display:flex;align-items:center;justify-content:center;padding:20px;'; overlay.innerHTML=`

Är du säker på att du vill ta bort?

Du är på väg att ta bort ${esc(ataLabel(a))} – ${esc(a.title)}.

Det går inte att ångra. ÄTA-numret återanvänds inte.

`; document.body.appendChild(overlay); overlay.querySelector('#ataCancelDelete').addEventListener('click',()=>overlay.remove()); overlay.addEventListener('click',ev=>{if(ev.target===overlay)overlay.remove()}); overlay.querySelector('#ataConfirmDelete').addEventListener('click',()=>{ const existing=ataItems().find(x=>String(x.id)===String(id)); if(!existing){overlay.remove();alert('ÄTA finns inte längre.');return;} data.ataItems=ataItems().filter(x=>String(x.id)!==String(id)); save();overlay.remove();render();showToast(ataLabel(existing)+' borttagen'); }); overlay.querySelector('#ataCancelDelete').focus(); }; function deleteATA(id){window.openATADeleteConfirm(id)} function openProjectFromOverview(id){selectedProject=id;projectListMode=false;page='Projekt';render()} function overview(){ let workProjects=data.projects.filter(p=>!p.archived&&!p.quoteOnly&&!p.completedAt); const ongoingATA=ataItems().filter(a=>!['Färdig','Avslutad','Avvisad','Makulerad'].includes(a.status||'Utkast') && workProjects.some(p=>Number(p.id)===Number(a.projectId))); const recent=workProjects.slice(0,5); return `${tasksPanel()}${waitingQuotesPanel()}
Aktiva projekt
${workProjects.length}
${ongoingATA.length?`
Pågående ÄTA
${ongoingATA.length}
`:''}
Medarbetare
${data.people.length}
${recent.length?`

Aktuella projekt

Öppna ett projekt direkt från startsidan.

${recent.map(p=>``).join('')}
`:''}`; } function toggleProjectCreate(){creatingProject=!creatingProject;render()}var projectListMode=true,creatingProject=false; function openProjectDetail(id){selectedProject=id;projectListMode=false;render()} function toggleProjectAccess(id,allowed){data.projectAccess??=[];data.projectAccess=allowed?[...new Set([...data.projectAccess,id])]:data.projectAccess.filter(x=>x!==id);save();render();showToast('Projektbehörighet uppdaterad')} function projectCanManage(){return role()==='admin'||(role()==='employee'&&data.projectAccess?.includes(employeeId))} function projectToDiary(id){if(project(id)?.quoteOnly){alert('Denna offertpärm har inga arbetstimmar.');return}reportProject=id;from='2000-01-01';to='2099-12-31';page='Arbetsdagbok';render()} let projectFolderTab='active'; function setProjectFolderTab(tab){projectFolderTab=tab==='archive'?'archive':'active';render()} function projectFolderQuote(p){const qs=ensureQuotes().quotes.filter(q=>String(q.projectId)===String(p.id));return qs.find(q=>!q.archived)||qs.at(-1)||null} function projects(){ if(!projectListMode)return projectDetail(); const folders=data.projects.filter(p=>p.quoteOnly),work=data.projects.filter(p=>!p.quoteOnly); const active=work.filter(p=>!p.completedAt&&!p.archived),finished=work.filter(p=>!!p.completedAt||p.archived); const openFolders=folders.filter(p=>{const q=projectFolderQuote(p);return !p.archived&&q&&!q.archived&"eIsWaiting(q)}); const archivedFolders=folders.filter(p=>{const q=projectFolderQuote(p);return p.archived||!!q?.archived}); const activeCount=active.length; const archiveCount=finished.length; const tabs=`
`; const search=`
`; const folderRow=p=>{const q=projectFolderQuote(p),label=q?(quoteIsWaiting(q)?'Skickad – inväntar svar':quoteStatus(q)==='Avböjd'?'Avböjd':'Offert i utkast'):'Offertpärm – ännu ingen offert';return `
${projectFolderTab==='archive'?'Arkiverad · ':''}${esc(label)}
${esc(p.name)}
${q?.title&&q.title!==p.name?`
${esc(q.title)}
`:''}
${esc(q?.customer||'Offertpärm · Inte aktivt arbetsprojekt')}
`}; const content=projectFolderTab==='active'?`

Aktiva projekt (${active.length})

${active.map(p=>projectListRow(p)).join('')||'

Inga aktiva projekt.

'}
Kommande projekt (${openFolders.length})

Gula offertpärmar som ännu inte blivit aktiva projekt.

${openFolders.map(folderRow).join('')||'

Inga kommande projekt.

'}
`:`

Avslutade projekt (${finished.length})

${finished.map(p=>projectListRow(p)).join('')||'

Inga avslutade projekt.

'}`; return `

Projektpärmar

Gröna aktiva projekt visas först. Väntande offertpärmar finns i fliken längre ner.

${tabs}${search}${content}
`; } function projectListRow(p){const es=data.entries.filter(e=>e.projectId===p.id),done=!!p.completedAt||!!p.archived;return `
${done?'Avslutat / arkiverat':'✓ Aktivt projekt'}
${esc(p.name)}
${(p.team||[]).length} tilldelade · ${fmt(sum(es))} h registrerat ${p.completedAt?'· Avslutat '+esc(p.completedAt):''}
`} let projectATAExpanded=false; function ataCostSummary(a){ const p=project(a.projectId)||{}; const entries=data.entries.filter(e=>String(e.ataId||'')===String(a.id)&&Number(e.projectId)===Number(a.projectId)); const own=entries.filter(e=>!e.ueId),ue=entries.filter(e=>e.ueId); const ownCost=own.reduce((v,e)=>v+Number(e.hours||0)*Number(p.ownHourlyCost??data.hourlyCost??600),0); const ueCost=ue.reduce((v,e)=>v+Number(e.hours||0)*Number(p.uePrices?.[e.ueId]?.cost??ueInit().find(u=>u.id===e.ueId)?.hourlyCost??data.hourlyUECost??600),0); const purchases=(data.projectInvoices||[]).filter(x=>Number(x.projectId)===Number(a.projectId)&&String(x.ataId||'')===String(a.id)); const materials=purchases.reduce((v,x)=>v+Number(x.amount||0),0); const hours=entries.reduce((v,e)=>v+Number(e.hours||0),0); const revenue=ataRevenue(a); return {ownCost,ueCost,materials,hours,revenue,total:ownCost+ueCost+materials,purchases,entries}; } function projectATAList(p){ const atas=ataItems().filter(a=>Number(a.projectId)===Number(p.id)).sort((a,b)=>Number(a.ataNumber||0)-Number(b.ataNumber||0)); if(!atas.length)return '

Inga ÄTA på projektet ännu.

'; return atas.map(a=>{const c=ataCostSummary(a),finished=a.status==='Färdig';return `
${esc(ataLabel(a))} – ${esc(a.title)}${fmt(c.hours)} h registrerade · ${a.priceType==='fixed'?'Fastpris':'Löpande'}${finished?'✓ Färdig':esc(a.status||'Utkast')}

${esc(a.description||'Ingen beskrivning')}

${a.priceType==='running'?'Upparbetad intäkt exkl. moms (tid + materialpåslag)':'Avtalat fastpris exkl. moms'}
${kr(c.revenue)}
Arbetskostnad
${kr(c.ownCost+c.ueCost)}
Material / inköp kopplade till ÄTA
${kr(c.materials)}
Preliminärt resultat
${kr(c.revenue-c.total)}

${fmt(c.hours)} timmar · Egen personal ${kr(c.ownCost)} · UE ${kr(c.ueCost)}. Endast kostnader som kopplats till denna ÄTA ingår. Ej bokförd vinst.

${c.purchases.length?`

Inköp: ${c.purchases.map(x=>esc(x.supplier)+' ('+kr(x.amount)+')').join(', ')}

`:''}
`}).join(''); } function setProjectBillingType(id,value){const p=data.projects.find(x=>String(x.id)===String(id));if(!p||!['fixed','running'].includes(value))return;if((data.entries||[]).some(e=>String(e.projectId)===String(id)&&!e.ataId)&&!confirm('Det finns registrerad tid på grundprojektet. Ändringen påverkar ekonomin. Fortsätta?')){render();return}p.billingType=value;save();render()} function setProjectBillingRate(id,key,value){const p=data.projects.find(x=>String(x.id)===String(id)),n=Number(value);if(!p||!['customerHourlyRate','ueCustomerHourlyRate'].includes(key)||!Number.isFinite(n)||n<0){alert('Ange ett giltigt timpris');render();return}p[key]=n;save();render()} function projectDetail(){let available=data.projects.filter(p=>!p.quoteOnly),p=available.find(x=>x.id===selectedProject)||available[0];if(!p)return `
Inga arbetsprojekt ännu. ${creatingProject?`
`:''}
`;selectedProject=p.id;p.team??=[];p.rates??={Arbetare:550,Arbetsledare:750};let es=data.entries.filter(e=>e.projectId===p.id&&(filter==='Alla'||e.role===filter)),cost=es.reduce((s,e)=>s+e.hours*(p.rates[e.role]||0),0);return `

${esc(p.name)}

Projektinformation, personal och timmar.

${creatingProject?`
`:''}
${['admin','owner'].includes(role())?``:''}
Projektuppgifter · ${p.billingType==='running'?'Löpande':'Fastpris'}

Fastpris: timmar är kostnader utan ny intäkt. Löpande: debiterbara timmar skapar upparbetad intäkt. ÄTA har egen debiteringsform.

Timpriser ändras under Ekonomi → Timpriser & debitering.

Projektvy
${[['overview','Översikt'],['economy','Ekonomi'],['ata','ÄTA'],['time','Tid'],['invoices','Fakturor'],['team','Personal & UE']].map(([id,label])=>``).join('')}
${projectFinance(p)}
${projectOverviewNotes(p)}

Projektets timmar och debitering

Timmar
${fmt(sum(es))} h
Beräknad debitering
${kr(cost)}
Tidposter
${es.length}
Visa timmar, personer och belopp
${es.map(e=>``).join('')||''}
DatumPersonRegistrerad kategoriArbetet gällerTimmarBelopp
${e.date}${esc(entryPerson(e))}${e.role}${e.ataId?esc(ataLabel(ataItems().find(a=>String(a.id)===String(e.ataId))||{})+' – '+(ataItems().find(a=>String(a.id)===String(e.ataId))?.title||'Borttagen ÄTA')):'Vanligt projektarbete'}${fmt(e.hours)} h${kr(e.hours*(p.rates[e.role]||0))}
Inga timmar i urvalet
${tidlettEconomySubtab==='prices'?projectRatesAndUE(p):projectCostCategories(p)}
${projectUnderlagPanel(p)}
ÄTA på projektet · ${ataItems().filter(a=>Number(a.projectId)===Number(p.id)).length} stVisa / dölj

Alla ÄTA kopplade till projektet. Klicka på en ÄTA för kostnader, timmar och resultat.

${projectATAList(p)}
${projectInvoicesPanel(p)}
Timdebitering – Visa / ändra timpriser

Timdebitering på projektet

Varje tidpost behåller sin registrerade roll även om personens projektroll senare ändras.

Personalens roller på projektet · ${(p.team||[]).length} registrerade – Visa / hantera

Personal på ${esc(p.name)}

${(p.team||[]).length} tilldelade
${(p.team||[]).map(t=>``).join('')}
MedarbetareRoll på projektetÅtgärd
${esc(person(t.id))}
UE-fakturakontroll – Visa / dölj

UE-tid för fakturakontroll

Separat underlag med UE-företag, medarbetare, datum och timmar.

`} let selectedStaff=null; function prettyDate(date){if(!date)return 'Datum saknas';const p=String(date).split('-');if(p.length!==3)return String(date);const months=['januari','februari','mars','april','maj','juni','juli','augusti','september','oktober','november','december'];return Number(p[2])+' '+(months[Number(p[1])-1]||p[1])+' '+p[0]} function ownerAccounts(){ data.owners??=[]; return `

Delägarkonton

Delägare har administratörens översikter och projekt-, offert- och ekonomifunktioner, men kan inte skapa anställda eller UE-företag.

${role()==='admin'?``:''}
${data.owners.map(o=>`
${esc(o.name)}Delägare
`).join('')||'

Inga delägare upplagda ännu.

'}

Detta är demokonton utan lösenord. Säker inloggning och åtkomstkontroll kräver en server och databas.

`; } function addOwnerAccount(){ if(role()!=='admin')return alert('Endast administratören kan lägga till delägare.'); const name=prompt('Namn på delägare:');if(!name?.trim())return; data.owners??=[];data.owners.push({id:Date.now(),name:name.trim(),role:'owner'});save();render();showToast('Delägare tillagd'); } function refreshOwnerSelector(){ const select=document.getElementById('ownerProfile');if(!select)return; select.style.display=role()==='owner'?'':'none'; if(role()==='owner'){select.innerHTML=(data.owners||[]).map(o=>``).join('')||''} } function people(){let x=(data.people||[]).find(p=>String(p.id)===String(selectedStaff));if(x){let es=data.entries.filter(e=>e.personId===x.id&&!e.ueId).sort((a,b)=>b.date.localeCompare(a.date)||b.id-a.id);let assigned=data.projects.filter(p=>(p.team||[]).some(t=>t.id===x.id));let projectIds=[...new Set([...assigned.map(p=>p.id),...es.map(e=>e.projectId)])];return `

${esc(x.name)}

Översikt över medarbetarens registrerade arbetstid

Totalt registrerat
${fmt(sum(es))} h
Tidrapporter
${es.length}
Projekt
${projectIds.length}

Behörigheter

Demobehörighet. Riktig åtkomstkontroll kräver inloggning och databasregler.

Timmar per projekt

${projectIds.map(id=>`
${esc(project(id)?.name||'Tidigare projekt')}${fmt(sum(es.filter(e=>e.projectId===id)))} h
`).join('')||'

Inga projekt ännu.

'}

Registrerade tidrapporter

${es.map(e=>`
${esc(prettyDate(e.date))}
${esc(project(e.projectId)?.name||'Tidigare projekt')}
${fmt(e.hours)} h
UTFÖRT ARBETE
${esc(e.description||'Ingen arbetsbeskrivning')}
`).join('')||'

Inga tidrapporter ännu.

'}
`}return `

Personal

Tryck på en medarbetare för att se timmar, projekt och arbetsbeskrivningar.

${role()==='admin'?``:''}
${data.people.map(x=>``).join('')}
NamnProjektTotala timmar
${data.projects.filter(p=>(p.team||[]).some(t=>t.id===x.id)).map(p=>esc(p.name)).join(', ')||'—'}${fmt(sum(data.entries.filter(e=>e.personId===x.id&&!e.ueId)))} h
`} function timeForm(emp=false){ let projectsList=data.projects.filter(p=>!p.archived&&!p.quoteOnly&&!p.completedAt&&true); if(!projectsList.length)return `
Inget aktivt arbetsprojekt är tillgängligt för tidrapportering. ${emp?'Inga aktiva arbetsprojekt finns just nu.':'Skapa ett arbetsprojekt under Projekt.'}
`; let first=projectsList[0],peopleList=emp?[]:data.people; // Admin och delägare kan rapportera alla anställda på alla aktiva projekt return `
${emp?'':`
`}

Standard: Projektarbete

`} function times(){return `

Registrera tid

Som administratör eller delägare kan du registrera tid för alla anställda på alla pågående projekt, även om personen inte tidigare varit kopplad till projektet.

${timeForm()}
Alla tidsrapporter (${data.entries.length})
${entryTable(data.entries)}
`}function entryTable(es){return `
${es.slice().sort((a,b)=>b.date.localeCompare(a.date)).map(e=>``).join('')||''}
DatumProjektPersonRoll vid registreringTimmarArbete
${e.date}${esc(project(e.projectId)?.name||'Arkiverat projekt')}${esc(entryPerson(e))}${e.role}${fmt(e.hours)}${esc(e.description)}${e.ataId?' · '+esc(ataLabel(ataItems().find(a=>a.id===e.ataId)||{})):''}${role()==='admin'?``:''}
Inga tidrapporter
`} function diary(){let p=project(reportProject)||data.projects.find(x=>!x.quoteOnly);if(!p)return '
Inga projekt finns.
';let es=data.entries.filter(e=>e.projectId===p.id&&e.date>=from&&e.date<=to).sort((a,b)=>a.date.localeCompare(b.date));let dates=[...new Set(es.map(e=>e.date))];let leaders=p.team.filter(t=>t.role==='Arbetsledare').map(t=>person(t.id));return `

Skapa arbetsdagbok

PDF laddas ner utan utskriftsdialog. Om Chrome inte kan återge förhandsgranskningen används en enklare PDF-layout.

${data.companyLogo?`Företagslogotyp`:esc(data.companyName||'Företagsnamn')}

ARBETSDAGBOK

${esc(p.name)}
${from} – ${to}
Företag
${esc(data.companyName||'Exempelföretaget AB')}
Projekt
${esc(p.name)}
Period
${from} – ${to}
${showLeaders?`
Arbetsledare
${esc(leaders.join(', ')||'Ingen utsedd')}
`:''}

Daglig sammanställning

${dates.length?`
Personal${showCompanies?'Företag':''}ArbetsbeskrivningTimmar
`:''}${dates.map(date=>{let day=es.filter(e=>e.date===date);return `
${date}${day.length} tidposter · ${fmt(sum(day))} h
${day.map(e=>`
${esc(entryPerson(e))}${showCompanies?`${esc(companyForEntry(e))}`:''}${esc(e.description)}${fmt(e.hours)}
`).join('')}
`}).join('')||'
Inga registrerade timmar för vald period.
'}
Totalt: ${fmt(sum(es))} timmar
`} let staffSearch='',projectSearch=''; function filterListSearch(kind,value){if(kind==='staff')staffSearch=value;else projectSearch=value;const rows=document.querySelectorAll(kind==='staff'?'[data-staff-row]':'[data-project-row]');let shown=0;rows.forEach(r=>{const match=r.getAttribute(kind==='staff'?'data-staff-row':'data-project-row').toLocaleLowerCase('sv').includes(value.trim().toLocaleLowerCase('sv'));r.style.display=match?'':'none';if(match)shown++});const empty=document.getElementById(kind+'SearchEmpty');if(empty)empty.style.display=shown?'none':'block';} let statsYear=new Date().getFullYear(); function setStatsYear(v){statsYear=Number(v)||new Date().getFullYear();render()} function projectClosingWarnings(id){ const ata=ataItems().filter(a=>Number(a.projectId)===Number(id)&&!['Färdig','Avslutad'].includes(a.status)); const checks=(data.ueInvoiceChecks||[]).filter(c=>Number(c.projectId)===Number(id)); const unchecked=checks.filter(c=>!['Kontrollerad','Godkänd','Matchar','Klar'].includes(String(c.status||''))); const ueHours=data.entries.filter(e=>Number(e.projectId)===Number(id)&&e.ueId).length; return {ata,unchecked,ueHours,checks}; } function setProjectCompletion(id){ const p=project(id);if(!p)return; const finishing=!p.completedAt; const modal=document.getElementById('modal');if(!modal)return; const w=projectClosingWarnings(id); const warning=finishing?`
Kontroll innan avslut
${w.ata.length?'⚠ '+w.ata.length+' ÄTA är ännu inte markerade som färdiga.':'✓ Inga öppna ÄTA.'}
${w.unchecked.length?'⚠ '+w.unchecked.length+' UE-fakturakontroller behöver ses över.':'✓ Inga öppna UE-fakturakontroller registrerade.'}
${w.ueHours&&!w.checks.length?'
⚠ UE-tid finns registrerad men ingen UE-fakturakontroll finns sparad för projektet.
':''}

Du kan avsluta ändå. Kontrollera gärna kostnader och fakturor först.

`:''; modal.innerHTML=``; modal.classList.remove('hidden'); } function confirmProjectCompletion(id,finishing){ const p=project(id);if(!p)return; if(finishing){ if(p.completedAt){closeModal();render();return;} const now=new Date(); p.completedAt=[now.getFullYear(),String(now.getMonth()+1).padStart(2,'0'),String(now.getDate()).padStart(2,'0')].join('-'); }else delete p.completedAt; save(); closeModal(); page='Projekt';projectListMode=true;selectedProject=id; render(); showToast(finishing?'Projektet är avslutat och visas under Avslutade projekt':'Projektet är nu pågående igen'); } function stats(){ const year=Number(statsYear)||new Date().getFullYear(); const years=new Set([year,new Date().getFullYear()]); data.projects.forEach(p=>{if(p.completedAt)years.add(Number(String(p.completedAt).slice(0,4)))}); data.entries.forEach(e=>{if(e.date)years.add(Number(String(e.date).slice(0,4)))}); const yearEntries=data.entries.filter(e=>String(e.date||'').startsWith(String(year))); const ownHours=sum(yearEntries.filter(e=>!e.ueId)); const ueHours=sum(yearEntries.filter(e=>e.ueId)); const rows=data.projects.filter(p=>!p.quoteOnly).map(p=>{ const es=data.entries.filter(e=>Number(e.projectId)===Number(p.id)); const inv=(data.projectInvoices||[]).filter(e=>Number(e.projectId)===Number(p.id)); const own=es.filter(e=>!e.ueId),ue=es.filter(e=>e.ueId); const cost=sum(own)*Number(data.hourlyCost??600)+ue.reduce((v,e)=>v+Number(e.hours||0)*Number(ueInit().find(u=>u.id===e.ueId)?.hourlyCost??data.hourlyUECost??600),0)+inv.reduce((v,x)=>v+Number(x.amount||0),0); const revenue=(Number(p.budgetAmount)||0)+ataTotal(p.id); return {name:p.name,hours:sum(es),cost,revenue,profit:revenue-cost,completedAt:p.completedAt||'',active:es.length>0||inv.length>0||revenue>0}; }); const finished=rows.filter(x=>x.completedAt&&String(x.completedAt).startsWith(String(year))); const ongoing=rows.filter(x=>!x.completedAt&&x.active); const total=finished.reduce((a,x)=>a+x.profit,0); const bars=(items,empty,completed=false)=>{const max=Math.max(1,...items.map(x=>Math.abs(x.profit)));return items.map(x=>`
${esc(x.name)}${kr(x.profit)}

Projektvärde ${kr(x.revenue)} · Registrerade kostnader ${kr(x.cost)} · ${fmt(x.hours)} h totalt${x.completedAt?' · Avslutat '+esc(x.completedAt):''}

`).join('')||`

${empty}

`}; return `${companyCostStatistics()}

Arbetade timmar · ${year}

Timmar från tidrapporter under valt år, uppdelat på egen personal och underentreprenörer.

Egen personal
${fmt(ownHours)} h
Underentreprenörer (UE)
${fmt(ueHours)} h
Totalt
${fmt(ownHours+ueHours)} h

Projektresultat

Projektresultat för avslutade projekt visas separat från ekonomisk status för pågående projekt.

Vinst på avslutade projekt ${year}
${kr(total)}
Avslutade projekt ${year}
${finished.length}
Pågående projekt
${ongoing.length}

Ett projekt räknas som avslutat när du markerar det som ”Avsluta projekt” i projektvyn. Året baseras på avslutsdatum. Resultatet är en kalkyl utifrån projektvärde och registrerade kostnader – inte bokförd nettovinst. Kontrollera att alla kostnader är registrerade innan avslut.

AVSLUTADE PROJEKT · ${year}

Samlat projektresultat

${kr(total)}

Beräknat resultat för avslutade projekt under valt år.

Avslutade projekt – detaljer ${finished.length} projekt
${bars(finished,'Inga projekt markerade som avslutade under valt år.',true)}
Pågående projekt – ekonomiskt läge ${ongoing.length} projekt

Projektvärde minus registrerade kostnader är inte slutlig vinst. Pågående projekt ingår inte i resultatet för avslutade projekt.

${bars(ongoing,'Inga pågående projekt med registrerat projektvärde eller kostnader.')}
`; } function saveHourlyRates(){ let sell=Number(document.getElementById('hourSell')?.value),cost=Number(document.getElementById('hourCost')?.value); if(!Number.isFinite(sell)||!Number.isFinite(cost)||sell<0||cost<0)return alert('Ange giltiga belopp.'); data.hourlySell=sell;data.hourlyCost=cost;let ueCost=Number(document.getElementById("hourUECost")?.value);if(!Number.isFinite(ueCost)||ueCost<0)return alert("Ange giltig UE-kostnad.");data.hourlyUECost=ueCost;save();render();showToast('Timpriser sparade'); } function settings(){return `

Timpriser och kostnader

Standardvärden exklusive moms. Används för att uppskatta intäkt, faktisk timkostnad och täckningsbidrag på projekt. Kostnaden ska inkludera lön, sociala avgifter och övriga personalkostnader om du vill få en mer rättvisande kalkyl.

Exempel: 650 − 600 = 50 kr per arbetad timme i täckningsbidrag, före andra projektkostnader.

Företagsuppgifter

${data.companyLogo?`

`:''}

Logotypen används i både arbetsdagboken och kundofferten. Sparas endast i denna webbläsare.

Rubriker för standardtexter

Ange vad listan med standardtexter ska heta och vilken rubrik som ska visas i kundofferten.

Demoversion: Ingen riktig serverinloggning eller behörighetskontroll. Dela inte riktiga personuppgifter i denna prototyp.
`} function uploadLogo(input){let file=input.files[0];if(!file)return;if(!['image/png','image/jpeg'].includes(file.type)||file.size>600000){alert('Välj PNG/JPG på högst 600 KB.');return}let r=new FileReader();r.onload=()=>{data.companyLogo=r.result;try{save();render()}catch{alert('Bilden är för stor för webbläsarens lokala lagring.')}};r.readAsDataURL(file)} function employeeProjects(){const ps=data.projects.filter(p=>!p.archived&&!p.quoteOnly&&!p.completedAt);return `

Alla aktiva projekt

Alla anställda kan se och tidrapportera på samtliga aktiva projekt. Ekonomi och interna kalkyler visas inte här.

${ps.map(p=>`
${esc(p.name)}
`).join('')||'

Inga aktiva projekt.

'}
`} function employeeOverview(){ let es=data.entries.filter(e=>e.personId===employeeId&&!e.ueId).sort((a,b)=>b.date.localeCompare(a.date)||b.id-a.id); let recent=last30Days(es),days=new Map(); for(let e of es){if(!days.has(e.date))days.set(e.date,{hours:0,entries:0,projects:new Set()});let d=days.get(e.date);d.hours+=Number(e.hours)||0;d.entries++;d.projects.add(project(e.projectId)?.name||'Tidigare projekt')} let rows=[...days.entries()].sort((a,b)=>b[0].localeCompare(a[0])); return `

Min översikt

Din registrerade arbetstid

Senaste 30 dagarna
${fmt(sum(recent))} h
${recent.length} rapporter
Totalt registrerat
${fmt(sum(es))} h
Arbetsdagar registrerade
${days.size}

Alla registrerade dagar

Kompakt lista med datum och dagens totala timmar. Flera projekt samma dag summeras.

${rows.map(([date,d])=>`
${esc(prettyDate(date))}
${esc([...d.projects].join(', '))}
${fmt(d.hours)} h
`).join('')||'

Du har inte registrerat någon tid ännu.

'}
`} let editingEmployeeEntry=null; function prettyDate(d){if(!d)return 'Datum saknas';let parts=d.split('-');if(parts.length!==3)return d;return `${Number(parts[2])} ${['januari','februari','mars','april','maj','juni','juli','augusti','september','oktober','november','december'][Number(parts[1])-1]||parts[1]} ${parts[0]}`} function employeeTime(){let es=data.entries.filter(e=>e.personId===employeeId&&!e.ueId).sort((a,b)=>b.date.localeCompare(a.date)||b.id-a.id);return `

Min tidrapport

${timeForm(true)}

Senaste registrerade tid

Dina tre senaste rapporter. Tryck på Ändra om något blev fel.

${employeeEntryCards(es.slice(0,3))}
`} function employeeEntryCards(es){return es.map(e=>{let locked=e.approved||e.locked;let editing=editingEmployeeEntry===e.id;let available=data.projects.filter(p=>!p.quoteOnly&&(!p.archived&&!p.completedAt||p.id===e.projectId));return `
${esc(prettyDate(e.date))}
${esc(project(e.projectId)?.name||'Arkiverat projekt')}
${fmt(e.hours)} timmar
UTFÖRT ARBETE
${esc(e.description||'Ingen arbetsbeskrivning angiven')}
${locked?`

Godkänd/låst rapport – kontakta administratören för ändringar.

`:editing?`

Ändra tidrapport

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

Du har ännu inte registrerat någon tid.

'} function saveEmployeeEdit(ev,id){ev.preventDefault();let e=data.entries.find(x=>x.id===id&&x.personId===employeeId&&!x.ueId);if(!e){alert('Rapporten hittades inte.');return}if(e.approved||e.locked){alert('Rapporten är godkänd eller låst. Kontakta administratören.');return}let f=new FormData(ev.target),pid=Number(f.get('projectId')),p=project(pid),team=p?.team?.find(t=>t.id===employeeId),hours=Number(f.get('hours')),description=String(f.get('description')||'').trim(),date=String(f.get('date')||'');if(!p||p.quoteOnly||!(hours>0&&hours<=24)||!date){alert('Kontrollera datum, projekt och timmar.');return}e.projectId=pid;e.date=date;e.hours=hours;e.description=description;e.role=team?.role||e.role||'Arbetare';const editATAId=Number(f.get('ataId')||0);e.ataId=editATAId&&ataItems().some(a=>a.id===editATAId&&a.projectId===pid)?editATAId:null;e.updatedAt=new Date().toISOString();editingEmployeeEntry=null;save();render();showToast('Tidrapporten har uppdaterats.')} var myHoursFrom=null,myHoursTo=null,myHoursExpanded=null; function myHoursDate(daysAgo){let d=new Date();d.setDate(d.getDate()-daysAgo);return [d.getFullYear(),String(d.getMonth()+1).padStart(2,'0'),String(d.getDate()).padStart(2,'0')].join('-')} function myHoursSetRange(){let f=document.getElementById('myHoursFrom'),t=document.getElementById('myHoursTo');myHoursFrom=f?.value||'';myHoursTo=t?.value||'';myHoursExpanded=null;render()} function myHoursReset(){myHoursFrom=myHoursDate(29);myHoursTo=myHoursDate(0);myHoursExpanded=null;render()} function myTime(){ if(myHoursFrom===null){myHoursFrom=myHoursDate(29);myHoursTo=myHoursDate(0)} let es=data.entries.filter(e=>e.personId===employeeId&&!e.ueId&&(!myHoursFrom||e.date>=myHoursFrom)&&(!myHoursTo||e.date<=myHoursTo)).sort((a,b)=>b.date.localeCompare(a.date)||b.id-a.id); let grouped=new Map();for(let e of es){if(!grouped.has(e.date))grouped.set(e.date,[]);grouped.get(e.date).push(e)} return `

Mina timmar

Senaste 30 dagarna visas automatiskt. Välj egna datum för att se en annan period.

${fmt(sum(es))} timmar · ${es.length} rapporter · ${grouped.size} dagar
${[...grouped].map(([date,entries])=>`
${esc(prettyDate(date))}
${esc([...new Set(entries.map(e=>project(e.projectId)?.name||'Arkiverat projekt'))].join(', '))}
${fmt(sum(entries))} h
${myHoursExpanded===date?`
${entries.map(e=>`
${esc(project(e.projectId)?.name||'Arkiverat projekt')} · ${fmt(e.hours)} h${e.approved||e.locked?'Låst':``}
${esc(e.description||'')}
${editingEmployeeEntry===e.id?employeeEntryCards([e]):''}
`).join('')}
`:''}
`).join('')||'

Inga tidrapporter under den valda perioden.

'}
`} function syncTimePeople(sel){let f=sel.closest('form'),personSel=f?.querySelector('[name="personId"]'),p=project(+sel.value);if(!personSel||!p)return;let selected=personSel.value;personSel.innerHTML=options(data.people,selected);if(!data.people.some(x=>String(x.id)===selected))personSel.selectedIndex=0} function submitTimeFromButton(btn,emp){let form=btn.closest('form');if(!form){alert('Tidrapportformuläret hittades inte.');return}try{submitTime({preventDefault(){},target:form},emp)}catch(error){console.error('Tidrapportfel',error);let status=form.querySelector('#timeSaveStatus');if(status)status.textContent='Kunde inte spara: '+error.message;alert('Kunde inte spara tidrapporten: '+error.message)}} function submitTime(ev,emp){ev.preventDefault();let form=ev.target,status=form.querySelector('#timeSaveStatus');let fail=msg=>{if(status){status.textContent=msg;status.style.color='#a33'}alert(msg);return false}; let pid=Number(form.querySelector('[name="projectId"]')?.value),personId=emp?employeeId:Number(form.querySelector('[name="personId"]')?.value),p=project(pid),hours=Number(form.querySelector('[name="hours"]')?.value),date=String(form.querySelector('[name="date"]')?.value||''),description=String(form.querySelector('[name="description"]')?.value||'').trim(); if(!p||p.archived||p.quoteOnly||p.completedAt)return fail('Välj ett pågående arbetsprojekt. Avslutade projekt är låsta för ny tidrapportering.'); if(!data.people.some(x=>x.id===personId))return fail('Välj en medarbetare.'); if(!date)return fail('Ange datum.'); if(!Number.isFinite(hours)||hours<=0||hours>24||!Number.isInteger(hours*2))return fail('Ange hela eller halva timmar (t.ex. 7,5 eller 8).'); p.team??=[];let team=p.team.find(t=>t.id===personId); if(!team){team={id:personId,role:'Arbetare'};p.team.push(team)} data.entries??=[];let id=Math.max(Date.now(),...data.entries.map(e=>Number(e.id)||0).slice(-100))+1; let ataId=Number(form.querySelector('[name="ataId"]')?.value||0);if(ataId&&!ataItems().some(a=>a.id===ataId&&a.projectId===pid))return fail('Välj en ÄTA som hör till projektet.');data.entries.push({id,date,projectId:pid,ataId:ataId||null,personId,hours,description,role:team.role}); try{localStorage.setItem('tidlett-clean-v159',JSON.stringify(data))}catch(error){data.entries.pop();return fail('Webbläsaren kunde inte lagra tidrapporten: '+error.message)} try{render();showToast('Tidrapporten sparades!')}catch(error){console.error(error);alert('Tidrapporten sparades, men sidan kunde inte uppdateras: '+error.message)} return false} function createProjectInline(ev){ev.preventDefault();let name=String(new FormData(ev.target).get('projectName')||'').trim();if(!name){alert('Ange projektnamn');return}const existing=data.projects.find(p=>!p.quoteOnly&&p.name.trim().replace(/\s+/g,' ').toLocaleLowerCase('sv')===name.replace(/\s+/g,' ').toLocaleLowerCase('sv'));if(existing){if(confirm('Projektet finns redan. Vill du öppna det befintliga projektet?')){selectedProject=existing.id;projectListMode=false;creatingProject=false;render()}return}let id=Math.max(0,...data.projects.map(p=>Number(p.id)||0))+1;data.projects.push({id,name,billingType:'fixed',customerHourlyRate:600,ueCustomerHourlyRate:600,archived:false,team:[],rates:{Arbetare:550,Arbetsledare:750},quoteCompany:{...qCompany()}});selectedProject=id;projectListMode=false;creatingProject=false;save();render();showToast('Projektet '+name+' har skapats. Alla anställda kan nu tidrapportera på projektet.')} function showToast(message){let el=document.getElementById('tidlettToast');if(!el){el=document.createElement('div');el.id='tidlettToast';el.style.cssText='position:fixed;bottom:18px;right:18px;background:#252a2d;color:white;padding:15px 22px;border-radius:12px;z-index:9999;max-width:90vw';document.body.appendChild(el)}el.textContent=message;setTimeout(()=>{if(el.isConnected)el.remove()},5000)} function openNewProject(){ const modal=document.getElementById('modal'); modal.classList.remove('hidden'); modal.innerHTML=`

Skapa nytt projekt

`; modal.querySelector('#newProjectName').focus(); } function createProjectDirect(){ const field=document.getElementById('newProjectName'),status=document.getElementById('projectCreateError'); const fail=msg=>{if(status)status.textContent=msg;else alert(msg)}; try{ const name=(field?.value||'').trim(); if(!name)return fail('Skriv ett projektnamn.'); if(!Array.isArray(data.projects))data.projects=[]; const duplicate=data.projects.find(p=>!p.quoteOnly&&p.name.trim().replace(/\s+/g,' ').toLocaleLowerCase('sv')===name.replace(/\s+/g,' ').toLocaleLowerCase('sv'));if(duplicate){if(confirm('Projektet finns redan. Vill du öppna det befintliga projektet?')){closeModal();selectedProject=duplicate.id;projectListMode=false;page='Projekt';render()}return} const id=Math.max(0,...data.projects.map(p=>Number(p.id)||0))+1; const newItem={id,name,archived:false,team:[],rates:{Arbetare:550,Arbetsledare:750}}; data.projects.push(newItem); let storageWarning=''; try{localStorage.setItem('tidlett-clean-v159',JSON.stringify(data))} catch(err){storageWarning=' Obs: webbläsaren tillåter inte lokal lagring i den här förhandsvisningen. Projektet finns kvar tills sidan laddas om.';console.warn(err)} selectedProject=id;projectListMode=true;creatingProject=false; closeModal();page='Projekt';render();showToast('Projektet '+name+' har skapats'+storageWarning); }catch(err){console.error('Projektskapning',err);fail('Fel vid projektskapning: '+err.message)} } function createProjectForm(ev){ev?.preventDefault();createProjectDirect()} function newProject(){let name=prompt('Namn på nytt projekt:');if(!name?.trim())return;if(data.projects.some(p=>!p.quoteOnly&&p.name.trim().replace(/\s+/g,' ').toLocaleLowerCase('sv')===name.trim().replace(/\s+/g,' ').toLocaleLowerCase('sv')))return alert('Projektet finns redan.');let id=Date.now();data.projects.push({id,name:name.trim(),archived:false,team:[],rates:{Arbetare:550,Arbetsledare:750}});selectedProject=id;save();render()}function renameProject(id){let p=project(id),name=prompt('Nytt projektnamn:',p.name);if(!name?.trim())return;if(data.projects.some(x=>x.id!==id&&!x.quoteOnly&&x.name.trim().replace(/\s+/g,' ').toLocaleLowerCase('sv')===name.trim().replace(/\s+/g,' ').toLocaleLowerCase('sv')))return alert('Det finns redan ett projekt med det namnet.');p.name=name.trim();save();render()}function archiveProject(id){let p=project(id);p.archived=!p.archived;save();render()}function newPerson(){if(role()!=='admin')return alert('Endast administratören kan skapa anställdas konton.');let name=prompt('Namn på anställd:');if(!name?.trim())return;data.people.push({id:Date.now(),name:name.trim()});save();render()}function setRole(pid,id,r){let t=project(pid).team.find(x=>x.id===id);t.role=r;save();render()}function setRate(pid,r,v){project(pid).rates[r]=Math.max(0,+v||0);save();render()}function addToProject(pid){let sel=document.getElementById('addPerson');if(!sel||!sel.value)return;project(pid).team.push({id:+sel.value,role:'Arbetare'});save();render()}function removeFromProject(pid,id){project(pid).team=project(pid).team.filter(t=>t.id!==id);save();render()} function deleteProject(id){ if(!['admin','owner'].includes(role()))return alert('Endast administratör och delägare kan ta bort projekt.'); const p=project(id);if(!p)return; const historic=data.entries.some(e=>Number(e.projectId)===Number(id))||(data.projectInvoices||[]).some(e=>Number(e.projectId)===Number(id))||(data.ataItems||[]).some(e=>Number(e.projectId)===Number(id)); document.getElementById('modal').classList.remove('hidden'); document.getElementById('modal').innerHTML=`

Ta bort projekt?

Projekt: ${esc(p.name)}

${historic?'Projektet innehåller registrerade timmar, fakturor eller ÄTA och kan därför inte raderas. Använd Avsluta projekt för att flytta det till Avslutade projekt.':'Projektet saknar registrerade timmar, fakturor och ÄTA och kan raderas permanent.'}
${historic?'':``}
`; } function confirmDelete(id){if(!['admin','owner'].includes(role()))return;const p=project(id);if(!p)return;const historic=data.entries.some(e=>Number(e.projectId)===Number(id))||(data.projectInvoices||[]).some(e=>Number(e.projectId)===Number(id))||(data.ataItems||[]).some(e=>Number(e.projectId)===Number(id));if(historic){closeModal();return alert('Projektet har registrerat underlag och kan inte tas bort.');}data.projects=data.projects.filter(p=>Number(p.id)!==Number(id));(data.ues||[]).forEach(u=>{u.projects=(u.projects||[]).filter(pid=>Number(pid)!==Number(id))});selectedProject=data.projects.find(p=>!p.quoteOnly)?.id;projectListMode=true;save();closeModal();render();showToast('Projektet har tagits bort.')}function closeModal(){document.getElementById('modal').classList.add('hidden');document.getElementById('modal').innerHTML=''}function editEntry(id){let e=data.entries.find(x=>x.id===id),h=prompt('Korrigera antal timmar:',e.hours);if(h===null)return;let n=+h;if(!(n>0&&n<=24)){alert('Ange 0–24 timmar.');return}let d=prompt('Arbetsbeskrivning:',e.description);if(d===null)return;e.hours=n;e.description=d;save();render()}function resetDemo(){if(!confirm('Rensa all information som sparats i denna rena version av TIDLETT? Detta kan inte ångras.'))return;data=initial();save();selectedProject=null;reportProject=null;render()} function ueInit(){if(!data.ues)data.ues=[{id:1,name:'Stockholms Elmontage AB',projects:[1],people:[{id:1001,name:'Ali Hassan',active:true},{id:1002,name:'Daniel Berg',active:true}]}];return data.ues} function currentUE(){return ueInit().find(x=>x.id===ueCompanyId)||ueInit()[0]} function companyForEntry(e){if(e.ueId)return ueInit().find(u=>u.id===e.ueId)?.name||'UE';return data.companyName||'Exempelföretaget AB'} function ueInvoiceEntries(ueId,projectId,from,to){ return data.entries.filter(e=>String(e.ueId)===String(ueId)&&(String(projectId)==='all'||String(e.projectId)===String(projectId))&&e.date>=from&&e.date<=to); } function ueInvoicePreview(){ const form=document.getElementById('ueInvoiceForm'),target=document.getElementById('ueInvoicePreview');if(!form||!target)return; const f=new FormData(form),from=String(f.get('from')||''),to=String(f.get('to')||''); if(!from||!to||from>to){target.innerHTML='

Välj en giltig period.

';return} const entries=ueInvoiceEntries(f.get('ueId'),f.get('projectId'),from,to),hours=sum(entries); const ue=ueInit().find(u=>String(u.id)===String(f.get('ueId'))); const rate=Number(ue?.hourlyCost??data.hourlyUECost??0); const grouped=[...new Set(entries.map(e=>e.projectId))].map(id=>({name:project(id)?.name||'Okänt projekt',hours:sum(entries.filter(e=>String(e.projectId)===String(id)))})); target.innerHTML=`
Rapporterad UE-tid
${fmt(hours)} h
Beräknad kostnad
${kr(hours*rate)}

Baserat på UE:s registrerade timmar och inställd timkostnad ${kr(rate)}/h. Detta är ett underlag, inte en verifierad faktura.

${grouped.length?`
Visa projektfördelning
${grouped.map(g=>``).join('')}
${esc(g.name)}${fmt(g.hours)} h
`:'

Inga UE-timmar hittades för perioden.

'}`; } function ueInvoicePage(){ data.ueInvoiceChecks??=[]; const checks=data.ueInvoiceChecks.slice().reverse(); const projects=data.projects.filter(p=>!p.quoteOnly); const now=new Date(),start=new Date(now.getFullYear(),now.getMonth(),1),local=d=>`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; return `

UE-fakturakontroll

Välj UE och period. TIDLETT hämtar timmarna automatiskt. Ange fakturans uppgifter bara om du vill jämföra dem.

Rapporterad tid för perioden

Tryck på Uppdatera för att hämta timmar.

Jämför med en UE-faktura (valfritt)

För att hitta en avvikelse behöver vi fakturans timmar och arbetsbelopp. Fyll i fälten nedan om du vill spara en fakturakontroll.

Datum på fakturan / tidsspecifikation

Valfritt. En rad per datum: 2026-10-05;8. Då jämförs även dag för dag.

${checks.map(c=>ueInvoiceResult(c)).join('')||'

Inga fakturakontroller sparade ännu.

'}`; } function ueInvoiceResult(c){ const ue=ueInit().find(u=>String(u.id)===String(c.ueId)); const entries=ueInvoiceEntries(c.ueId,c.projectId,c.from,c.to); const actual=sum(entries),diff=c.hours-actual,expected=actual*c.rate,amountDiff=c.amount-expected; const days={};entries.forEach(e=>days[e.date]=(days[e.date]||0)+Number(e.hours||0)); const specified=c.dateLines||[]; const daily=specified.length?specified.map(d=>`${esc(d.date)}${fmt(days[d.date]||0)} h${fmt(d.hours)} h${fmt(d.hours-(days[d.date]||0))} h`).join('')+Object.entries(days).filter(([date])=>!specified.some(d=>d.date===date)).map(([date,h])=>`${esc(date)}${fmt(h)} h0,0 h${fmt(-h)} h`).join(''):''; const dateMismatch=specified.length&&specified.some(d=>Math.abs(d.hours-(days[d.date]||0))>.001)||specified.length&&Object.keys(days).some(date=>!specified.some(d=>d.date===date)); const matches=Math.abs(diff)<.001&&Math.abs(amountDiff)<1&&!dateMismatch; const overlap=(data.ueInvoiceChecks||[]).some(x=>x.id!==c.id&&String(x.ueId)===String(c.ueId)&&(String(x.projectId)===String(c.projectId)||String(x.projectId)==='all'||String(c.projectId)==='all')&&x.from<=c.to&&x.to>=c.from); return `

${esc(ue?.name||'UE')} · ${esc(c.number)}

${esc(String(c.projectId)==='all'?'Alla projekt':project(c.projectId)?.name||'Projekt')} · ${esc(c.from)} – ${esc(c.to)}

${matches?'Timmar och belopp stämmer':'Avvikelse / kontrollera'}
Rapporterat
${fmt(actual)} h
Fakturerat
${fmt(c.hours)} h
Skillnad
${fmt(diff)} h
Avtalat timpris${kr(c.rate)}/h
Förväntat arbetsbelopp${kr(expected)}
Fakturerat arbetsbelopp${kr(c.amount)}
Beloppsdifferens${kr(amountDiff)}
${overlap?'

Observera: en annan sparad UE-faktura har överlappande period för samma projekt. Kontrollera att timmarna inte fakturerats dubbelt.

':''} ${specified.length?`
Visa datumkontroll (${specified.length} fakturarader)
${daily}
DatumTIDLETTFakturaSkillnad
`:'

Ingen datumspecifikation angiven. Endast periodens totalsummor jämförs.

'}

Detta är ett separat kontrollunderlag och läggs inte till som extra projektkostnad.

`; } function saveUEInvoiceCheck(ev){ ev.preventDefault();const f=new FormData(ev.target); const from=String(f.get('from')),to=String(f.get('to')),hours=Number(f.get('hours')),rate=Number(f.get('rate')),amount=Number(f.get('amount')); if(from>to||[hours,rate,amount].some(n=>!Number.isFinite(n)||n<0))return alert('Kontrollera period, timmar, pris och belopp.'); const lines=String(f.get('dateLines')||'').split(/\r?\n/).map(x=>x.trim()).filter(Boolean); const dateLines=[]; for(const line of lines){const m=line.match(/^(\d{4}-\d{2}-\d{2})\s*[;,]\s*(\d+(?:[.,]\d+)?)$/);if(!m)return alert('Fel i datumraden: '+line+'. Använd ÅÅÅÅ-MM-DD;8');const h=Number(m[2].replace(',','.'));if(!Number.isFinite(h)||h<0||m[1]to)return alert('Kontrollera datum och timmar: '+line);dateLines.push({date:m[1],hours:h})} if(new Set(dateLines.map(x=>x.date)).size!==dateLines.length)return alert('Samma datum finns flera gånger. Summera timmarna på en rad per datum.'); const ueId=Number(f.get('ueId')),projectId=f.get('projectId')==='all'?'all':Number(f.get('projectId')),number=String(f.get('number')||'').trim(); if(!number||!String(f.get('hours')||'').trim()||!String(f.get('rate')||'').trim()||!String(f.get('amount')||'').trim())return alert('Ange fakturanummer, fakturerade timmar, timpris och arbetsbelopp för att spara en jämförelse.'); if((data.ueInvoiceChecks||[]).some(c=>c.ueId===ueId&&c.number===number))return alert('Fakturanumret är redan registrerat för denna UE.'); data.ueInvoiceChecks??=[];data.ueInvoiceChecks.push({id:Date.now()+Math.floor(Math.random()*10000),ueId,projectId,number,from,to,hours,rate,amount,dateLines,reviewed:false}); save();render();showToast('Fakturan har jämförts med UE-tiden'); } function setUEInvoiceStatus(id){const c=(data.ueInvoiceChecks||[]).find(x=>x.id===id);if(!c)return;c.reviewed=!c.reviewed;save();render()} function removeUEInvoiceCheck(id){if(!confirm('Ta bort fakturakontrollen?'))return;data.ueInvoiceChecks=(data.ueInvoiceChecks||[]).filter(x=>x.id!==id);save();render()} function uePruneProjects(){const valid=new Set(data.projects.filter(p=>!p.quoteOnly&&!p.archived&&!p.completedAt).map(p=>String(p.id)));let changed=false;for(const u of ueInit()){const next=(u.projects||[]).filter(id=>valid.has(String(id)));if(next.length!==(u.projects||[]).length){u.projects=next;changed=true}}if(changed)save()} function ueAdmin(){let us=ueInit();uePruneProjects();return `

Underentreprenörer

Skapa ett UE-företag och tilldela projekt. Välj sedan UE-företag (demo) högst upp för att testa deras vy.

${role()==='admin'?``:''}
${us.map(u=>`

${esc(u.name)}

${u.people.length} registrerade personer

${data.projects.filter(p=>!p.quoteOnly&&!p.archived&&!p.completedAt).map(p=>`
`).join('')}
`).join('')}`} function setUECost(uid,value){let u=ueInit().find(x=>x.id===uid);if(!u)return;let n=Number(value);if(!Number.isFinite(n)||n<0)return alert("Ange en giltig timkostnad.");u.hourlyCost=n;save();render();showToast("UE-timkostnad sparad")} function newUE(){if(role()!=='admin')return alert('Endast administratören kan skapa UE-företag.');let name=prompt('Namn på UE-företaget:');if(!name?.trim())return;let id=Date.now();ueInit().push({id,name:name.trim(),projects:[],people:[]});ueCompanyId=id;save();render()} function assignUE(uid,pid,on){let valid=data.projects.some(p=>Number(p.id)===Number(pid)&&!p.quoteOnly&&!p.archived&&!p.completedAt);if(on&&!valid)return alert('Projektet finns inte längre eller är inte aktivt.');let u=ueInit().find(x=>x.id===uid);if(!u)return;u.projects=on?[...new Set([...u.projects,pid])]:u.projects.filter(x=>x!==pid);save();render()} function ueOverview(){let u=currentUE(),all=data.entries.filter(e=>e.ueId===u.id),recent=last30Days(all),byDate=new Map();for(let e of recent){let row=byDate.get(e.date)||{hours:0,count:0,people:new Set(),projects:new Set()};row.hours+=Number(e.hours)||0;row.count++;row.people.add(String(entryPerson(e)||"UE-medarbetare"));row.projects.add(project(e.projectId)?.name||'Arkiverat projekt');byDate.set(e.date,row)}let dates=[...byDate.entries()].sort((a,b)=>b[0].localeCompare(a[0]));return `

${esc(u.name)} – översikt

Registrerad tid för ert UE-företag.

Senaste 30 dagarna
${fmt(sum(recent))} h
${recent.length} rapporter
Totalt registrerat
${fmt(sum(all))} h
Aktiva UE-medarbetare
${u.people.filter(x=>x.active).length}

Registrerade dagar – senaste 30 dagarna

Kompakt lista med alla dagar, medarbetare och timmar. Flera rapporter samma dag summeras.

${dates.map(([date,d])=>`
${esc(prettyDate(date))}
${esc([...d.people].join(', '))}
${esc([...d.projects].join(', '))}
${fmt(d.hours)} h
`).join('')||'

Inga registrerade timmar under de senaste 30 dagarna.

'}
`} function uePeople(){let u=currentUE();return `

Personal hos ${esc(u.name)}

Lägg till personer en gång. Namnen sparas för kommande tidrapporter.

${u.people.map(p=>`
${esc(p.name)} ${p.active?'':'(inaktiv)'}
`).join('')}
`} function addUEPerson(){let n=document.getElementById('uePersonName').value.trim();if(!n)return;currentUE().people.push({id:Date.now(),name:n,active:true});save();render()} function toggleUEPerson(id){let p=currentUE().people.find(x=>x.id===id);p.active=!p.active;save();render()} function entryPerson(e){return e.ueId?(ueInit().find(u=>u.id===e.ueId)?.people.find(p=>p.id===e.personId)?.name||'UE-personal'):person(e.personId)} function ueTime(){uePruneProjects();let u=currentUE(),ps=data.projects.filter(p=>u.projects.some(id=>Number(id)===Number(p.id))&&!p.archived&&!p.quoteOnly&&!p.completedAt),people=u.people.filter(p=>p.active),es=data.entries.filter(e=>e.ueId===u.id).sort((a,b)=>b.date.localeCompare(a.date));return `

Rapportera flera medarbetare samtidigt

Välj datum och projekt en gång. Fyll i timmar för varje person. Utfört arbete är valfritt. Lämna timmar tomma för personer som inte arbetat.

${ps.length&&people.length?`
${people.map((p,i)=>`
${esc(p.name)}Person ${i+1}
`).join('')}
`:'

Lägg till UE-personal och se till att företaget har ett tilldelat projekt.

'}

Våra registreringar

${es.map(e=>`
${esc(u.people.find(x=>x.id===e.personId)?.name||'Tidigare medarbetare')}${fmt(e.hours)} h
${esc(e.date)} · ${esc(project(e.projectId)?.name||'Arkiverat projekt')}
${e.description?`

${esc(e.description)}

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

Inga registreringar ännu.

'}
`} function saveUEBatchFromButton(btn){ let form=btn.closest('form');if(!form){alert('Kunde inte hitta UE-formuläret.');return} try{return saveUEBatch({preventDefault(){},target:form})} catch(error){console.error('UE-sparfel',error);let status=form.querySelector('#ueSaveStatus');if(status)status.textContent='Fel: '+error.message;alert('Kunde inte spara UE-tid: '+error.message);return false}} function saveUEBatch(ev){ ev.preventDefault();let form=ev.target,u=currentUE(),status=form.querySelector('#ueSaveStatus'); const fail=message=>{if(status){status.textContent=message;status.style.color='#a33'}alert(message);return false}; if(!u)return fail('Välj ett UE-företag.'); let pid=Number(form.querySelector('[name="project"]')?.value),date=String(form.querySelector('[name="date"]')?.value||''); if(!u.projects.some(id=>Number(id)===Number(pid))||!date||!project(pid)||project(pid).archived||project(pid).quoteOnly||project(pid).completedAt)return fail('Välj ett giltigt projekt och datum.'); let ataId=Number(form.querySelector('[name="ataId"]')?.value||0);if(ataId&&!ataItems().some(a=>a.id===ataId&&a.projectId===pid))return fail('ÄTA hör inte till projektet.');let rows=[],invalid=[]; for(let p of u.people.filter(x=>x.active)){ let raw=String(form.querySelector('[name="hours_'+p.id+'"]')?.value||'').trim(); let description=String(form.querySelector('[name="description_'+p.id+'"]')?.value||'').trim(); if(!raw)continue; let hours=Number(raw.replace(',','.')); if(!raw||!Number.isFinite(hours)||hours<0.5||hours>24||!Number.isInteger(hours*2)){invalid.push(p.name);continue} rows.push({id:Date.now()+rows.length+1,date,projectId:pid,ataId:ataId||null,personId:p.id,ueId:u.id,hours,description,role:'UE'}); } if(invalid.length)return fail('Ange hela eller halva timmar för: '+invalid.join(', ')); if(!rows.length)return fail('Fyll i timmar för minst en medarbetare.'); data.entries??=[];data.entries.push(...rows); try{localStorage.setItem('tidlett-clean-v159',JSON.stringify(data))} catch(error){data.entries.splice(-rows.length);return fail('Kunde inte spara i webbläsaren: '+error.message)} try{render();showToast(rows.length+' UE-tidrapporter sparade!')} catch(error){console.error('UE-visningsfel',error);alert('Timmarna sparades, men vyn kunde inte uppdateras: '+error.message)} return false} // Offertmodul: all data in local browser only. const quoteDefaults=()=>({markup:10,materials:[{id:1,name:'Regelvirke 45×95',unit:'m',price:48},{id:2,name:'Betong C25/30',unit:'m³',price:1450},{id:3,name:'Isolering',unit:'m²',price:92},{id:4,name:'Skruv',unit:'st',price:2.5},{id:5,name:'Armeringsjärn',unit:'kg',price:16}],snippets:['Etablering och avetablering ingår.','Bortforsling av byggavfall ingår enligt överenskommelse.','Offerten gäller i 30 dagar från offertdatum.','Ändrings- och tilläggsarbeten offereras separat.'],quotes:[],calculations:[]}); function ensureQuotes(){if(!data.quoteData)data.quoteData=quoteDefaults();if(!data.quoteAccess)data.quoteAccess=[];data.quoteData.customHeadings??=[];data.quoteData.snippetHeadings??={};return data.quoteData} function qStandardHeading(kind){const q=ensureQuotes();return (kind==='list'?q.snippetsHeading:q.quoteTermsHeading)|| (kind==='list'?'Återanvändbara offerttexter':'Omfattning och villkor')} function qSaveStandardHeadings(){const list=document.getElementById('qSnippetsHeading'),terms=document.getElementById('qTermsHeading');if(!list||!terms)return;const q=ensureQuotes();q.snippetsHeading=list.value.trim()||'Återanvändbara offerttexter';q.quoteTermsHeading=terms.value.trim()||'Omfattning och villkor';save();render();showToast('Rubrikerna är sparade.')} let offerHeadingSnapshot=null;let qStandardCreatePanel='',offerTab='kalkyl',offerProject=null,offerDraft=[],offerText=[],offerTitle='',offerCustomer='',offerDescription='',offerVat='ex',offerTotalOverride='',offerLaborHours='0',offerLaborRate='650',offerQuoteId=null,offerContact='',offerContactPhone='',offerContactEmail=''; const qmoney=n=>Number(n||0).toLocaleString('sv-SE',{style:'currency',currency:'SEK',maximumFractionDigits:2}); const qnum=n=>Math.max(0,Number(n)||0); const qproject=()=>{let p=project(offerProject)||data.projects[0];if(p)offerProject=p.id;return p}; const qcost=r=>qnum(r.qty)*qnum(r.price); const qprice=r=>qcost(r)*(1+qnum(r.markup)/100); const qtotal=()=>offerDraft.reduce((a,r)=>a+qprice(r),0)+qnum(offerLaborHours)*qnum(offerLaborRate); const qget=id=>ensureQuotes().materials.find(m=>m.id===Number(id)); // Material navigation: compact list and searchable selector. let qMaterialsExpanded=false; function qMaterialPopularity(material){ const q=ensureQuotes(); const saved=(q.calculations||[]).flatMap(c=>c.rows||[]); return saved.filter(r=>r.name===material.name).length + offerDraft.filter(r=>r.name===material.name).length; } function qSortedMaterials(){return [...ensureQuotes().materials].sort((a,b)=>qMaterialPopularity(b)-qMaterialPopularity(a)||String(a.name).localeCompare(String(b.name),'sv'));} function qToggleMaterialList(){qMaterialsExpanded=!qMaterialsExpanded;render()} function qFilterMaterialList(value){ const needle=value.toLocaleLowerCase('sv').trim(); const rows=[...document.querySelectorAll('#qMaterialTable tbody tr[data-material-name]')]; const expanded=qMaterialsExpanded||!!needle; rows.forEach((row,i)=>{row.hidden=(!expanded&&i>=8)||(!row.dataset.materialName.includes(needle))}); const count=document.getElementById('qMaterialCount'); if(count)count.textContent=needle?`${rows.filter(r=>!r.hidden).length} träffar`:`${Math.min(rows.length,expanded?rows.length:8)} av ${rows.length} material visas`; } function qFilterCalcMaterials(value){ const select=document.getElementById('qMaterialSelect');if(!select)return; const needle=value.toLocaleLowerCase('sv').trim(); let first='';let count=0; [...select.options].forEach(opt=>{ const match=opt.textContent.toLocaleLowerCase('sv').includes(needle); opt.hidden=!match;opt.disabled=!match; if(match){count++;if(!first)first=opt.value} }); select.value=first||''; const add=document.getElementById('qAddMaterialBtn');if(add)add.disabled=!count; const info=document.getElementById('qCalcMaterialCount');if(info)info.textContent=count?`${count} material hittades`:'Inga material hittades'; } function qAddMaterial(id){let m=qget(id);if(!m)return;offerDraft.push({id:Date.now()+Math.random(),name:m.name,unit:m.unit,qty:1,price:m.price,markup:ensureQuotes().markup});render()} function qRemove(id){offerDraft=offerDraft.filter(x=>String(x.id)!==String(id));render()} function qEdit(id,key,value){let r=offerDraft.find(x=>String(x.id)===String(id));if(!r)return;r[key]=key==='name'?value:qnum(value);qRefreshTotals()} function qRefreshTotals(){let c=document.getElementById('qtotals');if(c)c.innerHTML=qTotalsHtml();document.querySelectorAll('[data-q-row-total]').forEach(el=>{let r=offerDraft.find(x=>String(x.id)===el.dataset.qRowTotal);if(r)el.textContent=qmoney(qprice(r))})} function qTotalsHtml(){let cost=offerDraft.reduce((a,r)=>a+qcost(r),0),material=offerDraft.reduce((a,r)=>a+qprice(r),0),labor=qnum(offerLaborHours)*qnum(offerLaborRate);return `
Materialinköp
${qmoney(cost)}
Material inkl. påslag
${qmoney(material)}
Kalkylsumma exkl. moms
${qmoney(material+labor)}

Arbete: ${qmoney(labor)}. Dessa uppgifter är interna och visas inte i kundofferten.

`} function qSaveCalc(){let p=qproject();if(!p){alert('Skapa ett projekt först.');return}let q=ensureQuotes();q.calculations.push({id:Date.now(),projectId:p.id,rows:JSON.parse(JSON.stringify(offerDraft)),laborHours:qnum(offerLaborHours),laborRate:qnum(offerLaborRate),date:new Date().toISOString(),quoteId:offerQuoteId||null});if(offerQuoteId){const quote=q.quotes.find(x=>String(x.id)===String(offerQuoteId));if(quote)quote.calcSnapshot={rows:JSON.parse(JSON.stringify(offerDraft)),laborHours:qnum(offerLaborHours),laborRate:qnum(offerLaborRate)}}save();showToast('Kalkylen sparad i projektpärmen.');render()} function qLoadCalc(id){quoteFolderView='folder';let c=ensureQuotes().calculations.find(x=>x.id===id);if(!c)return;offerProject=c.projectId;offerDraft=JSON.parse(JSON.stringify(c.rows));offerLaborHours=String(c.laborHours);offerLaborRate=String(c.laborRate);offerTab='kalkyl';render()} function qSaveMaterial(ev){ev.preventDefault();let f=new FormData(ev.target),name=String(f.get('name')||'').trim();if(!name)return;ensureQuotes().materials.push({id:Date.now(),name,unit:String(f.get('unit')),price:qnum(f.get('price'))});save();render();showToast('Material tillagt i prislistan.')} function qSetMarkup(v){ensureQuotes().markup=qnum(v);save();render()} function qSaveSnippet(ev){ev.preventDefault();let t=String(new FormData(ev.target).get('text')||'').trim();if(!t)return;ensureQuotes().snippets.push(t);save();render()} function qAddSnippet(id){qSyncDraft();let t=ensureQuotes().snippets[Number(id)];if(!t)return;offerText=offerText.includes(t)?offerText.filter(x=>x!==t):[...offerText,t];render()} function qSyncDraft(){let ids=['offerTitle','offerCustomer','offerDescription','offerTotalOverride','offerContact','offerContactPhone','offerContactEmail','offerCustomerContact','offerCustomerAddress','offerExtraTitle','offerExtraText'];for(let id of ids){let el=document.getElementById(id);if(el){if(id==='offerTitle')offerTitle=el.value;if(id==='offerCustomer')offerCustomer=el.value;if(id==='offerDescription')offerDescription=el.value;if(id==='offerTotalOverride')offerTotalOverride=el.value;if(id==='offerContact')offerContact=el.value;if(id==='offerContactPhone')offerContactPhone=el.value;if(id==='offerContactEmail')offerContactEmail=el.value;if(id==='offerCustomerContact')offerCustomerContact=el.value;if(id==='offerCustomerAddress')offerCustomerAddress=el.value;if(id==='offerExtraTitle')offerExtraTitle=el.value;if(id==='offerExtraText')offerExtraText=el.value}}} function qSwitch(tab){qSyncDraft();offerTab=tab;render()} function qCreateQuote(){ const button=document.getElementById('saveQuoteButton'); if(button?.disabled)return; if(button){button.disabled=true;button.textContent='Sparar…'} try{ qSyncDraft(); let p=qproject(); if(!p)throw Error('Välj en projektpärm först.'); if(!offerCustomer.trim()||!offerTitle.trim())throw Error('Fyll i kund och offertens rubrik.'); let amount=offerTotalOverride.trim()===''?qtotal():qnum(offerTotalOverride); let q=ensureQuotes(),id=offerQuoteId||Date.now(); let old=q.quotes.find(x=>x.id===id); let entry={id,projectId:p.id,customer:offerCustomer,title:offerTitle,description:offerDescription,contact:offerContact,contactPhone:offerContactPhone,contactEmail:offerContactEmail,customerContact:offerCustomerContact,customerAddress:offerCustomerAddress,extraTitle:offerExtraTitle,extraText:offerExtraText,amount,vat:offerVat,texts:[...offerText],headingMap:Object.fromEntries(offerText.map(t=>[t,offerHeadingSnapshot?.[t]||ensureQuotes().snippetHeadings?.[t]||qStandardHeading('terms')])),date:new Date().toISOString(),status:old?.status||'Påbörjad',calcSnapshot:{rows:JSON.parse(JSON.stringify(offerDraft)),laborHours:qnum(offerLaborHours),laborRate:qnum(offerLaborRate)},priceOverride:offerTotalOverride}; let i=q.quotes.findIndex(x=>x.id===id); if(i>=0)q.quotes[i]=entry;else q.quotes.push(entry); try{localStorage.setItem('tidlett-clean-v159',JSON.stringify(data))} catch(err){if(i>=0)q.quotes[i]=old;else q.quotes.pop();throw Error('Webbläsaren tillåter inte att offerten sparas permanent: '+err.message)} offerQuoteId=id; if(button){button.textContent='✓ Sparad';button.disabled=false} let msg=document.getElementById('quoteSaveFeedback'); if(msg){msg.textContent='✓ Offerten är sparad. Du hittar den under Sparade offerter.';msg.style.color='#26724b'} showToast('Offerten sparades!'); }catch(err){ if(button){button.disabled=false;button.textContent='Spara offertutkast'} let msg=document.getElementById('quoteSaveFeedback'); if(msg){msg.textContent=err.message;msg.style.color='#a33'} alert(err.message) } } function qOpenQuote(id){quoteFolderView='folder';let e=ensureQuotes().quotes.find(x=>x.id===id);if(!e)return;offerQuoteId=e.id;offerProject=e.projectId;offerTitle=e.title;offerCustomer=e.customer;offerDescription=e.description;offerTotalOverride=e.priceOverride!==undefined?String(e.priceOverride):String(e.amount);offerVat=e.vat;offerText=[...(e.texts||[])];offerHeadingSnapshot=e.headingMap||null;offerCustomerContact=e.customerContact||"";offerCustomerAddress=e.customerAddress||"";offerExtraTitle=e.extraTitle||"";offerExtraText=e.extraText||"";offerContact=e.contact??qCompany().contact??'';offerContactPhone=e.contactPhone??qCompany().contactPhone??'';offerContactEmail=e.contactEmail??qCompany().contactEmail??'';const snap=e.calcSnapshot||ensureQuotes().calculations.filter(c=>String(c.quoteId)===String(e.id)).at(-1)||ensureQuotes().calculations.filter(c=>String(c.projectId)===String(e.projectId)).at(-1);offerDraft=JSON.parse(JSON.stringify(snap?.rows||[]));offerLaborHours=String(snap?.laborHours??0);offerLaborRate=String(snap?.laborRate??data.hourlySell??650);offerTab='offert';render()} function qNewQuote(){quoteFolderView='folder';offerQuoteId=null;offerContact=qProjectCompany().contact||'';offerContactPhone=qProjectCompany().contactPhone||'';offerContactEmail=qProjectCompany().contactEmail||'';offerTitle='';offerCustomer='';offerCustomerContact='';offerCustomerAddress='';offerExtraTitle='';offerExtraText='';offerDescription='';offerTotalOverride='';offerText=[...ensureQuotes().snippets];offerHeadingSnapshot=null;offerVat='ex';offerDraft=[];offerLaborHours='0';offerLaborRate=String(data.hourlySell??650);offerTab='offert';render()} function qToggleAccess(id,checked){ensureQuotes();if(checked&&!data.quoteAccess.includes(id))data.quoteAccess.push(id);if(!checked)data.quoteAccess=data.quoteAccess.filter(x=>x!==id);save();render()} function qRefreshQuotePreview(){qSyncDraft();const current=document.querySelector('.quote-document');if(!current)return;const holder=document.createElement('div');holder.innerHTML=qQuotePreview();const updated=holder.firstElementChild;if(updated)current.replaceWith(updated);const feedback=document.getElementById('quotePreviewFeedback');if(feedback)feedback.textContent='Förhandsgranskningen är uppdaterad';} function qGroupedTexts(){const grouped=new Map();const q=ensureQuotes();for(const t of offerText){const heading=offerHeadingSnapshot?.[t]||q.snippetHeadings?.[t]||qStandardHeading('terms');if(!grouped.has(heading))grouped.set(heading,[]);grouped.get(heading).push(t)}return [...grouped]} function qQuoteGroupedHtml(){return qGroupedTexts().map(([heading,texts])=>`

${esc(heading)}

`).join('')} function qRenameAnyHeading(name){ const q=ensureQuotes(),next=prompt('Ändra rubrik:',name)?.trim();if(!next||next===name)return; if([qStandardHeading('terms'),...q.customHeadings].some(h=>h!==name&&h.toLocaleLowerCase('sv')===next.toLocaleLowerCase('sv')))return alert('Rubriken finns redan.'); if(name===qStandardHeading('terms'))q.quoteTermsHeading=next; else q.customHeadings=q.customHeadings.map(h=>h===name?next:h); for(const t of Object.keys(q.snippetHeadings))if(q.snippetHeadings[t]===name)q.snippetHeadings[t]=next; save();render();showToast('Rubriken är uppdaterad.'); } function qDeleteAnyHeading(name){ const q=ensureQuotes(),base=qStandardHeading('terms'); if(name===base){alert('Den första rubriken kan byta namn men inte tas bort.');return;} if(!confirm('Ta bort rubriken? Dess standardtexter flyttas till '+base+'.'))return; q.customHeadings=q.customHeadings.filter(h=>h!==name); for(const t of Object.keys(q.snippetHeadings))if(q.snippetHeadings[t]===name)q.snippetHeadings[t]=base; save();render(); } function qNewSnippetUnderHeading(heading){qStandardCreatePanel='snippet';qNewSnippetHeading=heading;qEditingSnippet=-1;render()} let qNewSnippetHeading=''; function qAddCustomHeading(){const input=document.getElementById('qNewHeadingName');const name=input?.value.trim();if(!name)return alert('Ange rubrik.');const q=ensureQuotes();if([qStandardHeading('terms'),...q.customHeadings].some(x=>x.toLocaleLowerCase('sv')===name.toLocaleLowerCase('sv')))return alert('Rubriken finns redan.');q.customHeadings.push(name);save();render();showToast('Rubriken är tillagd.')} function qRenameCustomHeading(old){const next=prompt('Nytt namn på rubriken:',old)?.trim();if(!next||next===old)return;const q=ensureQuotes();if([qStandardHeading('terms'),...q.customHeadings].some(x=>x!==old&&x.toLocaleLowerCase('sv')===next.toLocaleLowerCase('sv')))return alert('Rubriken finns redan.');q.customHeadings=q.customHeadings.map(x=>x===old?next:x);for(const t of Object.keys(q.snippetHeadings))if(q.snippetHeadings[t]===old)q.snippetHeadings[t]=next;save();render()} function qDeleteCustomHeading(name){if(!confirm('Ta bort rubriken? Texterna flyttas till Omfattning och villkor.'))return;const q=ensureQuotes();q.customHeadings=q.customHeadings.filter(x=>x!==name);for(const t of Object.keys(q.snippetHeadings))if(q.snippetHeadings[t]===name)q.snippetHeadings[t]=qStandardHeading('terms');save();render()} function qSetSnippetHeading(i,value){const q=ensureQuotes(),t=q.snippets[i];if(!t)return;q.snippetHeadings[t]=value;save();render()} function qHeadingOptions(selected){return [qStandardHeading('terms'),...ensureQuotes().customHeadings].map(h=>``).join('')} function qQuotePreview(){qSyncDraft();let amount=offerTotalOverride.trim()===''?qtotal():qnum(offerTotalOverride),vat=amount*.25;return `
${data.companyLogo?`Företagslogotyp`:esc(data.companyName||'Företagsnamn')}
${(!data.companyLogo||qProjectCompany().showNameUnderLogo)?`
${esc(qProjectCompany().name||data.companyName||'')}
`:''}

OFFERT

${new Date().toLocaleDateString('sv-SE')}
${esc(qProjectCompany().name||data.companyName||'')}
${esc(qProjectCompany().org?'Org.nr: '+qProjectCompany().org:'')}
${esc([qProjectCompany().address,qProjectCompany().postal,qProjectCompany().city].filter(Boolean).join(', '))}
${esc([qProjectCompany().phone,qProjectCompany().email,qProjectCompany().website].filter(Boolean).join(' · '))}
${(offerContact||qProjectCompany().contact)?`
Kontaktperson: ${esc(offerContact||qProjectCompany().contact)}${(offerContactPhone||qProjectCompany().contactPhone)?' · '+esc(offerContactPhone||qProjectCompany().contactPhone):''}${(offerContactEmail||qProjectCompany().contactEmail)?' · '+esc(offerContactEmail||qProjectCompany().contactEmail):''}
`:''}
Beställare
${esc(offerCustomer||'Beställarens namn')}
${offerCustomerContact?`
Kontaktperson: ${esc(offerCustomerContact)}
`:''}${offerCustomerAddress?`
Plats för arbetet
${esc(offerCustomerAddress)}
`:''}
Projekt
${esc(qproject()?.name||'')}

${esc(offerTitle||'Offertens rubrik')}

${esc(offerDescription||'Beskrivning av arbetets omfattning.')}

${qQuoteGroupedHtml()}${offerExtraTitle||offerExtraText?`

${esc(offerExtraTitle||'Reservationer')}

    ${offerExtraText.split(/\n/).map(x=>x.trim().replace(/^[•*\-]\s*/, '')).filter(Boolean).map(x=>`
  • ${esc(x)}
  • `).join('')}
`:''}
${offerVat==='ex'||offerVat==='both'?`
Pris exkl. moms${qmoney(amount)}
`:''}${offerVat==='both'?`
Moms 25 %${qmoney(vat)}
`:''}${offerVat==='inc'||offerVat==='both'?`
Pris inkl. moms${qmoney(amount+vat)}
`:''}
`} // Offline PDF export: self-contained PDF writer, no popup or external library. function pdfWinAnsi(text){ const map={'€':128,'‚':130,'ƒ':131,'„':132,'…':133,'†':134,'‡':135,'ˆ':136,'‰':137,'Š':138,'‹':139,'Œ':140,'Ž':142,'‘':145,'’':146,'“':147,'”':148,'•':149,'–':150,'—':151,'˜':152,'™':153,'š':154,'›':155,'œ':156,'ž':158,'Ÿ':159}; let out='';for(const ch of String(text??'')){const code=ch.codePointAt(0);out+=String.fromCharCode(map[ch]??(code<=255?code:63))}return out; } function pdfEscape(text){return pdfWinAnsi(text).replace(/\\/g,'\\\\').replace(/\(/g,'\\(').replace(/\)/g,'\\)').replace(/[\r\n]/g,' ')} function pdfTextWidth(text,size){return Array.from(String(text)).reduce((w,c)=>w+(c===' '?0.28:'il.,:;!|'.includes(c)?0.25:'MW@'.includes(c)?0.85:0.52)*size,0)} function pdfWrap(text,maxWidth,size){ const result=[];for(const para of String(text??'').split(/\r?\n/)){ if(!para.trim()){result.push('');continue} let line='';for(const word of para.trim().split(/\s+/)){ if(line&&pdfTextWidth(line+' '+word,size)>maxWidth){result.push(line);line=''} if(pdfTextWidth(word,size)>maxWidth){let part='';for(const ch of word){if(part&&pdfTextWidth(part+ch,size)>maxWidth){result.push(part);part=''}part+=ch}line=part} else line=line?line+' '+word:word; }if(line)result.push(line) }return result; } function buildOfferPDF(){ const company=qProjectCompany(), amount=offerTotalOverride.trim()===''?qtotal():qnum(offerTotalOverride), vat=amount*.25; const lines=[];const add=(text,size=10,bold=false,space=0)=>lines.push({text:String(text??''),size,bold,space}); add(company.name||data.companyName||'Företag',17,true,5); add([company.address,company.postal,company.city].filter(Boolean).join(', '),9); add([company.org?'Org.nr: '+company.org:'',company.phone,company.email,company.website].filter(Boolean).join(' | '),9, false,12); add('OFFERT',22,true,5);add('Datum: '+new Date().toLocaleDateString('sv-SE'),9,false,10); add('KUND',11,true,3);add(offerCustomer,11,true);if(offerCustomerContact)add('Kontaktperson: '+offerCustomerContact,9); if(offerCustomerAddress)add(offerCustomerAddress,9);add('',9,false,7); add(offerTitle,15,true,8);if(qproject()?.name)add('Projekt: '+qproject().name,9,false,8); add('ARBETETS OMFATTNING',11,true,4);add(offerDescription||'',10,false,9); if(offerText.length){for(const [heading,texts] of qGroupedTexts()){add(heading.toLocaleUpperCase('sv-SE'),11,true,5);for(const t of texts)add('• '+t,10,false,4);add('',9,false,5)}} if(offerExtraTitle||offerExtraText){add(offerExtraTitle||'Reservationer',11,true,5);for(const t of offerExtraText.split(/\r?\n/).filter(x=>x.trim()))add('• '+t.trim().replace(/^[•*\-]\s*/,''),10,false,4);add('',9,false,5)} add('PRIS',11,true,5);if(offerVat!=='inc')add('Pris exkl. moms: '+qmoney(amount),12,true,4); if(offerVat==='both')add('Moms 25 %: '+qmoney(vat),10,false,4); if(offerVat!=='ex')add('Pris inkl. moms: '+qmoney(amount+vat),12,true,4); add('',9,false,10);if(offerContact||company.contact)add('Kontaktperson: '+(offerContact||company.contact),9); if(offerContactPhone||company.contactPhone)add('Telefon: '+(offerContactPhone||company.contactPhone),9); if(offerContactEmail||company.contactEmail)add('E-post: '+(offerContactEmail||company.contactEmail),9); const pages=[];let ops=[],y=785;const flush=()=>{pages.push(ops.join('\n'));ops=[];y=785}; // Champagne accent and pale price highlight match the on-screen quote preview. // Draw the champagne divider only after the complete company address/contact block. for(const item of lines){if(item.text==='OFFERT'){y-=12;}y-=item.space||0;const width=495;const parts=pdfWrap(item.text,width,item.size);for(const part of parts){const height=item.size*1.5;if(y-height<55)flush();if(part){const isPrice=item.text==='PRIS'||item.text.startsWith('Pris exkl.')||item.text.startsWith('Pris inkl.')||item.text.startsWith('Moms 25');const isHeading=item.text==='OFFERT'||item.text==='KUND'||item.text==='ARBETETS OMFATTNING'||qGroupedTexts().some(([heading])=>item.text===heading.toLocaleUpperCase('sv-SE'))||item.text==='PRIS'||item.text===offerExtraTitle; if(isPrice){ops.push('0.961 0.941 0.902 rg 42 '+(y-height*0.45).toFixed(2)+' 511 '+(height+5).toFixed(2)+' re f')}ops.push((isHeading?'0.59 0.48 0.31 rg':'0.19 0.21 0.20 rg')+' BT /'+(item.bold?'F2':'F1')+' '+item.size+' Tf 48 '+y.toFixed(2)+' Td ('+pdfEscape(part)+') Tj ET')} y-=height}} if(ops.length||!pages.length)flush(); const objs=[null];const obj=(str)=>{objs.push(str);return objs.length-1}; const font1=obj('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>'); const font2=obj('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>'); const pageRefs=[],pageData=[];for(const stream of pages){const content=obj('<< /Length '+pdfWinAnsi(stream).length+' >>\nstream\n'+stream+'\nendstream');const page=obj('');pageRefs.push(page);pageData.push({page,content})} const pagesRef=obj('<< /Type /Pages /Kids ['+pageRefs.map(n=>n+' 0 R').join(' ')+'] /Count '+pageRefs.length+' >>'); for(const x of pageData)objs[x.page]='<< /Type /Page /Parent '+pagesRef+' 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 '+font1+' 0 R /F2 '+font2+' 0 R >> >> /Contents '+x.content+' 0 R >>'; const root=obj('<< /Type /Catalog /Pages '+pagesRef+' 0 R >>'); let output='%PDF-1.4\n%âãÏÓ\n', offsets=[0];for(let i=1;i>\nstartxref\n'+xref+'\n%%EOF'; const bytes=Uint8Array.from(pdfWinAnsi(output),ch=>ch.charCodeAt(0));return new Blob([bytes],{type:'application/pdf'}); } function buildDiaryPDF(){ const p=project(reportProject)||data.projects.find(x=>!x.quoteOnly); if(!p)throw Error('Välj ett projekt först.'); const es=data.entries.filter(e=>e.projectId===p.id&&e.date>=from&&e.date<=to).sort((a,b)=>a.date.localeCompare(b.date)); const leaders=(p.team||[]).filter(t=>t.role==='Arbetsledare').map(t=>person(t.id)); const rows=[{t:data.companyName||'TIDLETT',size:17,bold:true,gap:12},{t:'ARBETSDAGBOK',size:20,bold:true,gap:10},{t:'Projekt: '+p.name,size:12,bold:true,gap:6},{t:'Period: '+from+' – '+to,size:10,gap:8}]; if(showLeaders)rows.push({t:'Arbetsledare: '+(leaders.join(', ')||'Ingen utsedd'),size:10,gap:7}); let lastDate='';for(const e of es){if(e.date!==lastDate){lastDate=e.date;rows.push({t:e.date,size:12,bold:true,gap:12,band:true})}rows.push({t:entryPerson(e)+(showCompanies?' | '+companyForEntry(e):'')+' | '+fmt(e.hours)+' h',size:10,bold:true,gap:4});rows.push({t:e.description||'Ingen arbetsbeskrivning',size:9,gap:7})} if(!es.length)rows.push({t:'Inga registrerade timmar för vald period.',size:10,gap:10}); rows.push({t:'TOTAL ARBETSTID: '+fmt(sum(es))+' timmar',size:13,bold:true,gap:20,band:true}); return buildTextPDF(rows); } function buildTextPDF(rows){ const pages=[];let ops=[],y=784; const pageStart=()=>{ops=[];y=784}; const flush=()=>{pages.push(ops.join('\n'));pageStart()};pageStart(); for(const row of rows){y-=row.gap||0;const parts=pdfWrap(row.t,493,row.size||10);for(const part of parts){const h=(row.size||10)*1.5;if(y-h<52)flush();if(row.band)ops.push('0.961 0.941 0.902 rg 43 '+(y-h*0.45).toFixed(2)+' 510 '+(h+5).toFixed(2)+' re f');if(part)ops.push((row.bold?'0.38 0.32 0.23 rg':'0.19 0.21 0.20 rg')+' BT /'+(row.bold?'F2':'F1')+' '+(row.size||10)+' Tf 48 '+y.toFixed(2)+' Td ('+pdfEscape(part)+') Tj ET');y-=h}} pages.push(ops.join('\n')); const objs=[null],obj=v=>(objs.push(v),objs.length-1); const f1=obj('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>'),f2=obj('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>'); const refs=[],contents=[];for(const stream of pages){const c=obj('<< /Length '+pdfWinAnsi(stream).length+' >>\nstream\n'+stream+'\nendstream'),pg=obj('');refs.push(pg);contents.push(c)} const parent=obj('<< /Type /Pages /Kids ['+refs.map(n=>n+' 0 R').join(' ')+'] /Count '+refs.length+' >>'); refs.forEach((pg,i)=>objs[pg]='<< /Type /Page /Parent '+parent+' 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 '+f1+' 0 R /F2 '+f2+' 0 R >> >> /Contents '+contents[i]+' 0 R >>'); const root=obj('<< /Type /Catalog /Pages '+parent+' 0 R >>');let out='%PDF-1.4\n%âãÏÓ\n',offs=[0];for(let i=1;i>\nstartxref\n'+xref+'\n%%EOF';return new Blob([Uint8Array.from(pdfWinAnsi(out),c=>c.charCodeAt(0))],{type:'application/pdf'}); } // Version 94: Print the SAME report in the current tab, avoiding blocked popups. // Explicit user action opens the browser print/save-to-PDF dialog. let tidlettPrintTitle=''; function closePDFPreview(){ const overlay=document.getElementById('tidlett-pdf-overlay'); if(overlay)overlay.remove(); document.body.classList.remove('tidlett-pdf-active'); if(tidlettPrintTitle){document.title=tidlettPrintTitle;tidlettPrintTitle=''} } function printPreviewAsPDF(reportHTML,title){ closePDFPreview(); const report=document.createElement('div'); report.innerHTML=reportHTML; const content=report.querySelector('.report'); if(!content){alert('Kunde inte visa dokumentet.');return} const overlay=document.createElement('div'); overlay.id='tidlett-pdf-overlay'; const toolbar=document.createElement('div'); toolbar.className='tidlett-pdf-actions'; const printButton=document.createElement('button'); printButton.type='button';printButton.textContent='Skriv ut / Spara som PDF'; printButton.onclick=()=>{ // Chrome may ignore print() when invoked from an overlay in a local HTML file. // Use an isolated print document, created synchronously from this user click. try{ const printFrame=document.createElement('iframe'); printFrame.setAttribute('title','PDF-utskrift'); printFrame.style.cssText='position:fixed;right:0;bottom:0;width:1px;height:1px;border:0;opacity:0;pointer-events:none'; document.body.appendChild(printFrame); const doc=printFrame.contentDocument; const css=Array.from(document.querySelectorAll('style')).map(x=>x.textContent).join('\n'); const paper=document.querySelector('#tidlett-pdf-overlay .tidlett-pdf-paper'); doc.open(); doc.write(''+document.title.replace(/&/g,'&').replace(/</g,'<')+'
'+paper.innerHTML+'
'); doc.close(); const images=Array.from(doc.images); Promise.all(images.map(img=>img.complete?Promise.resolve():new Promise(resolve=>{img.onload=resolve;img.onerror=resolve;setTimeout(resolve,2500)}))).then(()=>{ printFrame.contentWindow.focus(); printFrame.contentWindow.print(); setTimeout(()=>printFrame.remove(),120000); }); }catch(err){ alert('Utskrift kunde inte startas. Tryck Ctrl+P i Chrome och välj Spara som PDF. Fel: '+err.message); } }; const closeButton=document.createElement('button'); closeButton.type='button';closeButton.textContent='Tillbaka till TIDLETT';closeButton.onclick=closePDFPreview; const hint=document.createElement('span');hint.textContent='Välj ”Spara som PDF” i utskriftsdialogen.'; toolbar.append(printButton,closeButton,hint); const paper=document.createElement('div');paper.className='tidlett-pdf-paper';paper.append(content); overlay.append(toolbar,paper);document.body.append(overlay); document.body.classList.add('tidlett-pdf-active'); tidlettPrintTitle=document.title; document.title=title.replace(/[\\/:*?"<>|]/g,'_'); overlay.scrollTop=0; } // Direct PDF download. Render the visible report with Chrome's SVG foreignObject // and embed page images into a PDF; no print dialog or external dependencies. function tidlettDownloadBlob(blob,filename){ const url=URL.createObjectURL(blob),a=document.createElement('a'); a.href=url;a.download=filename.replace(/[\\/:*?"<>|]/g,'_')+'.pdf'; document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),60000); } function tidlettPdfFromJpegs(jpegs){ const enc=new TextEncoder(),parts=[],offsets=[0];let length=0; const push=x=>{const b=typeof x==='string'?enc.encode(x):x;parts.push(b);length+=b.length}; const obj=(id,body)=>{offsets[id]=length;push(id+' 0 obj\n');push(body);push('\nendobj\n')}; const n=jpegs.length, pagesId=2, fontId=3, rootId=1; push('%PDF-1.4\n');obj(rootId,'<< /Type /Catalog /Pages 2 0 R >>'); obj(pagesId,'<< /Type /Pages /Kids ['+jpegs.map((_,i)=>(4+i*3)+' 0 R').join(' ')+'] /Count '+n+' >>'); obj(fontId,'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>'); jpegs.forEach((jpg,i)=>{ const page=4+i*3,img=page+1,content=page+2; obj(page,'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595.28 841.89] /Resources << /XObject << /Im0 '+img+' 0 R >> >> /Contents '+content+' 0 R >>'); const bytes=Uint8Array.from(atob(jpg.data.split(',')[1]),c=>c.charCodeAt(0)); offsets[img]=length;push(img+' 0 obj\n<< /Type /XObject /Subtype /Image /Width '+jpg.width+' /Height '+jpg.height+' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length '+bytes.length+' >>\nstream\n');push(bytes);push('\nendstream\nendobj\n'); const stream='q\n595.28 0 0 841.89 0 0 cm\n/Im0 Do\nQ\n'; obj(content,'<< /Length '+enc.encode(stream).length+' >>\nstream\n'+stream+'endstream'); }); const xref=length,count=4+n*3;push('xref\n0 '+count+'\n0000000000 65535 f \n'); for(let i=1;i>\nstartxref\n'+xref+'\n%%EOF'); return new Blob(parts,{type:'application/pdf'}); } // TIDLETT v100: direct vector PDF export from the *actual rendered report*. // No SVG foreignObject, no print dialog, and no simplified text-only fallback. async function tidlettDirectPDF(reportHTML,filename){ const holder=document.createElement('div'); holder.style.cssText='position:fixed;left:0;top:0;width:780px;z-index:-9999;pointer-events:none;background:#fff;opacity:0'; holder.innerHTML=reportHTML; const report=holder.querySelector('.report')||holder.firstElementChild; if(!report)throw Error('Förhandsgranskningen saknas'); report.style.width='780px';report.style.maxWidth='780px';report.style.margin='0'; document.body.appendChild(holder); try{ if(document.fonts?.ready)await document.fonts.ready; const imgs=[...report.querySelectorAll('img')]; await Promise.all(imgs.map(im=>im.decode().catch(()=>{}))); const root=report.getBoundingClientRect(), scale=595.28/root.width, pageH=841.89/scale; const height=Math.max(report.scrollHeight,root.height), pageCount=Math.max(1,Math.ceil(height/pageH)); const pages=Array.from({length:pageCount},()=>[]), images=[]; const rgb=(str)=>{let m=(str||'').match(/rgba?\((\d+)[, ]+\s*(\d+)[, ]+\s*(\d+)(?:[, /]+([\d.]+))?/);return m&&m[4]!=='0'?[+m[1]/255,+m[2]/255,+m[3]/255]:null}; const color=(str,stroke=false)=>{let c=rgb(str);return c?c.map(n=>n.toFixed(3)).join(' ')+(stroke?' RG\n':' rg\n'):''}; const escPDF=s=>String(s).replace(/\\/g,'\\\\').replace(/\(/g,'\\(').replace(/\)/g,'\\)').replace(/[\u0000-\u001f]/g,' '); const win=s=>String(s).replace(/[\u2018\u2019]/g,"'").replace(/[\u201c\u201d]/g,'"').replace(/[\u2013\u2014]/g,'-').replace(/\u2022/g,'*').replace(/\u00a0/g,' ').replace(/[^\x20-\x7e\xA0-\xFF\n\r]/g,'?'); const rect=(x,y,w,h,fill,stroke,sw)=>{ if(w<=0||h<=0)return; const first=Math.max(0,Math.floor(y/pageH)),last=Math.min(pageCount-1,Math.floor((y+h-0.01)/pageH)); for(let p=first;p<=last;p++){ const yy=Math.max(y,p*pageH),bottom=Math.min(y+h,(p+1)*pageH),hh=bottom-yy; const xx=x*scale,py=841.89-(yy-p*pageH+hh)*scale; let cmd='';if(fill)cmd+=color(fill);if(stroke)cmd+=color(stroke,true)+(sw||1)*scale+' w\n'; cmd+=xx.toFixed(2)+' '+py.toFixed(2)+' '+(w*scale).toFixed(2)+' '+(hh*scale).toFixed(2)+' re '+(fill&&stroke?'B':fill?'f':'S')+'\n';pages[p].push(cmd); } }; const elements=[report,...report.querySelectorAll('*')]; // Paint boxes first, then text, so a later container never obscures earlier text. for(const el of elements){ const st=getComputedStyle(el),r=el.getBoundingClientRect(); if(st.display==='none'||st.visibility==='hidden'||r.width<1||r.height<1)continue; const x=r.left-root.left,y=r.top-root.top; if(st.backgroundColor&&rgb(st.backgroundColor))rect(x,y,r.width,r.height,st.backgroundColor); for(const side of ['top','right','bottom','left']){ const bw=parseFloat(st.getPropertyValue('border-'+side+'-width'))||0,bc=st.getPropertyValue('border-'+side+'-color'); if(!bw||st.getPropertyValue('border-'+side+'-style')==='none'||!rgb(bc))continue; if(side==='top')rect(x,y,r.width,bw,bc); if(side==='bottom')rect(x,y+r.height-bw,r.width,bw,bc); if(side==='left')rect(x,y,bw,r.height,bc); if(side==='right')rect(x+r.width-bw,y,bw,r.height,bc); } } for(const el of elements){ if(el.tagName==='IMG'&&el.complete&&el.naturalWidth){ try{ const r=el.getBoundingClientRect(),canvas=document.createElement('canvas'); canvas.width=Math.min(2400,Math.max(1,Math.round(Math.max(el.naturalWidth,r.width*3))));canvas.height=Math.min(1800,Math.max(1,Math.round(Math.max(el.naturalHeight,r.height*3)))); const ctx=canvas.getContext('2d');ctx.fillStyle='#fff';ctx.fillRect(0,0,canvas.width,canvas.height);ctx.imageSmoothingEnabled=true;ctx.imageSmoothingQuality='high';ctx.drawImage(el,0,0,canvas.width,canvas.height); const b64=canvas.toDataURL('image/jpeg',1.0).split(',')[1]; const id=images.length;images.push({b64,width:canvas.width,height:canvas.height}); const x=(r.left-root.left)*scale,y=(r.top-root.top)*scale,w=r.width*scale,h=r.height*scale,p=Math.floor((r.top-root.top)/pageH); if(p>=0&&p=600||st.fontWeight==='bold'; while((match=re.exec(text))){ if(!match[0].trim())continue; const range=document.createRange();range.setStart(node,match.index);range.setEnd(node,match.index+match[0].length); const boxes=[...range.getClientRects()]; for(const b of boxes){ const y=b.top-root.top,x=b.left-root.left,p=Math.floor(y/pageH); if(p<0||p>=pageCount)continue; const baseline=841.89-(y-p*pageH+b.height*.79)*scale; pages[p].push(color(st.color||'rgb(30,30,30)')+'BT /'+(bold?'F2':'F1')+' '+fontSize.toFixed(2)+' Tf 1 0 0 1 '+(x*scale).toFixed(2)+' '+baseline.toFixed(2)+' Tm ('+escPDF(win(match[0]))+') Tj ET\n'); } } } } const enc=new TextEncoder(),chunks=[],offsets=[0];let size=0; const push=v=>{const b=typeof v==='string'?enc.encode(v):v;chunks.push(b);size+=b.length}; const obj=(id,s)=>{offsets[id]=size;push(id+' 0 obj\n'+s+'\nendobj\n')}; const stream=(id,dict,bytes)=>{offsets[id]=size;push(id+' 0 obj\n<< '+dict+' /Length '+bytes.length+' >>\nstream\n');push(bytes);push('\nendstream\nendobj\n')}; const imgBase=5+pageCount*2; push('%PDF-1.4\n');obj(1,'<< /Type /Catalog /Pages 2 0 R >>'); obj(2,'<< /Type /Pages /Kids ['+pages.map((_,i)=>(5+i*2)+' 0 R').join(' ')+'] /Count '+pageCount+' >>'); obj(3,'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>'); obj(4,'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>'); pages.forEach((commands,i)=>{ const pid=5+i*2,cid=pid+1; const xobjects=images.map((_,j)=>'/Im'+j+' '+(imgBase+j)+' 0 R').join(' '); obj(pid,'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> /XObject << '+xobjects+' >> >> /Contents '+cid+' 0 R >>'); const content=win(commands.join(''));stream(cid,'',Uint8Array.from(content,c=>c.charCodeAt(0))); }); images.forEach((im,i)=>{const bytes=Uint8Array.from(atob(im.b64),c=>c.charCodeAt(0));stream(imgBase+i,'/Type /XObject /Subtype /Image /Width '+im.width+' /Height '+im.height+' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode',bytes)}); const xref=size,count=imgBase+images.length;push('xref\n0 '+count+'\n0000000000 65535 f \n'); for(let i=1;i>\nstartxref\n'+xref+'\n%%EOF'); tidlettDownloadBlob(new Blob(chunks,{type:'application/pdf'}),filename); }finally{holder.remove()} } // Version 98: Always produce a downloadable PDF, with a visible status and a // self-contained PDF fallback if Chrome cannot render the preview as an image. let tidlettPdfBusy=false; function tidlettPdfStatus(message,isError=false){ let el=document.getElementById('tidlett-pdf-status'); if(!el){el=document.createElement('div');el.id='tidlett-pdf-status';el.setAttribute('role','status'); el.style.cssText='position:fixed;right:18px;bottom:18px;z-index:999999;max-width:390px;padding:15px 20px;background:#fff;border:1px solid #d4c5a7;border-radius:12px;box-shadow:0 8px 30px #0002;font:14px/1.5 Arial,sans-serif;color:#34312b';document.body.appendChild(el)} el.style.borderColor=isError?'#bd7777':'#d4c5a7';el.textContent=message; } async function tidlettExportPdf(reportHTML,filename,fallback){ if(tidlettPdfBusy){tidlettPdfStatus('En PDF skapas redan. Vänta en stund.');return} tidlettPdfBusy=true; tidlettPdfStatus('Skapar PDF – vänta ett ögonblick …'); try{ try{ await Promise.race([tidlettDirectPDF(reportHTML,filename),new Promise((_,reject)=>setTimeout(()=>reject(Error('Förhandsgranskningens PDF-rendering tog för lång tid')),12000))]); tidlettPdfStatus('PDF-nedladdningen har startats. Kontrollera Hämtade filer i Chrome (Ctrl+J).');return true; }catch(previewError){ console.error('PDF-rendering misslyckades:',previewError); throw previewError; } }catch(e){console.error('PDF-fel',e);tidlettPdfStatus('PDF kunde inte laddas ner: '+e.message,true);alert('PDF kunde inte laddas ner: '+e.message);return false} finally{tidlettPdfBusy=false} } function downloadDiaryPDF(){ const report=document.querySelector('.report');if(!report){tidlettPdfStatus('Öppna arbetsdagboken först.',true);return} const p=project(reportProject)||data.projects.find(x=>!x.quoteOnly); return tidlettExportPdf(report.outerHTML,'Arbetsdagbok_'+(p?.name||'projekt')+'_'+from+'_'+to,buildDiaryPDF); } function qPrint(){ try{ qSyncDraft(); if(!offerCustomer.trim()||!offerTitle.trim()){tidlettPdfStatus('Fyll i kund och offertens rubrik först.',true);return} return Promise.resolve(tidlettExportPdf(qQuotePreview(),'Offert_'+offerCustomer+'_'+offerTitle,buildOfferPDF)).then(result=>{if(result!==false&&confirm('Vill du markera offerten som skickad?\n\nJa = Skickad – inväntar svar\nAvbryt = Behåll som påbörjad')){if(offerQuoteId){qSetQuoteStatus(offerQuoteId,'Skickad')}else{showToast('Spara offerten först för att registrera status som skickad.')}}return result}); }catch(e){console.error(e);tidlettPdfStatus('Kunde inte förbereda offerten: '+e.message,true)} } let quoteFolderView='home'; function qSaveMaterialDirect(){const f=document.getElementById('qMaterialForm');if(!f){alert('Öppna materialprislistan igen.');return}let name=f.querySelector('[name="name"]').value.trim(),unit=f.querySelector('[name="unit"]').value,price=Number(f.querySelector('[name="price"]').value);if(!name||!Number.isFinite(price)||price<0){alert('Fyll i materialnamn och ett giltigt inköpspris.');return}ensureQuotes().materials.push({id:Date.now()+Math.random(),name,unit,price});save();render();showToast('Materialet '+name+' är sparat i prislistan.')} let qEditingSnippet=-1,qEditingTemplate=null,qDeletePendingSnippet=-1,qDeletePendingTemplate=null; function qEditSnippet(id){qEditingSnippet=qEditingSnippet===Number(id)?-1:Number(id);qDeletePendingSnippet=-1;qStandardCreatePanel='';render()} function qSaveEditedSnippet(id){const el=document.getElementById('qSnippetEdit'+id);if(!el)return;const next=el.value.trim();if(!next){alert('Standardtexten får inte vara tom.');return}const q=ensureQuotes(),old=q.snippets[id];if(old===undefined)return;q.snippets[id]=next;q.snippetHeadings[next]=q.snippetHeadings[old]||qStandardHeading('terms');delete q.snippetHeadings[old];offerText=offerText.map(t=>t===old?next:t);qEditingSnippet=-1;save();render();showToast('Standardtexten är uppdaterad.')} function qDeleteSnippet(id){const q=ensureQuotes(),i=Number(id),old=q.snippets[i];if(old===undefined)return;if(qDeletePendingSnippet!==i){qDeletePendingSnippet=i;render();return;}q.snippets.splice(i,1);delete q.snippetHeadings[old];offerText=offerText.filter(t=>t!==old);qEditingSnippet=-1;qDeletePendingSnippet=-1;save();render();showToast('Standardtexten är borttagen.')} function qToggleStandardCreate(which){qStandardCreatePanel=qStandardCreatePanel===which?'':which;qEditingSnippet=-1;qEditingTemplate=null;render()} function qSaveSnippetDirect(){const f=document.getElementById('qSnippetForm');if(!f){alert('Öppna standardtexter igen.');return}const el=f.querySelector('textarea[name="text"]');const t=el?el.value.trim():'';if(!t){alert('Skriv en standardtext först.');return}const q=ensureQuotes();q.snippets??=[];q.snippets.push(t);q.snippetHeadings[t]=document.getElementById('qSnippetHeadingSelect')?.value||qStandardHeading('terms');qStandardCreatePanel='';save();render();showToast('Standardtexten är sparad.')} function qEditExtraTemplate(id){qEditingTemplate=qEditingTemplate===String(id)?null:String(id);qDeletePendingTemplate=null;qStandardCreatePanel='';render()} function qSaveEditedExtraTemplate(id){const title=document.getElementById('qTemplateEditTitle'+id)?.value.trim(),body=document.getElementById('qTemplateEditText'+id)?.value.trim();if(!title||!body){alert('Ange både rubrik och text.');return}const x=(ensureQuotes().extraTemplates||[]).find(t=>String(t.id)===String(id));if(!x)return;x.title=title;x.text=body;qEditingTemplate=null;save();render();showToast('Textmallen är uppdaterad.')} function qDeleteExtraTemplate(id){if(!confirm('Vill du ta bort den sparade textmallen? Befintliga offerter påverkas inte.'))return;const q=ensureQuotes();q.extraTemplates=(q.extraTemplates||[]).filter(x=>String(x.id)!==String(id));qEditingTemplate=null;qDeletePendingTemplate=null;save();render();showToast('Textmallen är borttagen.')} function qOpenFolder(id){offerProject=Number(id);quoteFolderView='folder';offerTab='sammanstallning';offerQuoteId=null;offerTitle='';offerCustomer='';offerCustomerContact='';offerCustomerAddress='';offerDescription='';offerExtraTitle='';offerExtraText='';offerText=[...ensureQuotes().snippets];offerTotalOverride='';let c=ensureQuotes().calculations.filter(x=>String(x.projectId)===String(id)).at(-1);if(c){offerDraft=JSON.parse(JSON.stringify(c.rows));offerLaborHours=String(c.laborHours);offerLaborRate=String(c.laborRate)}else{offerDraft=[];offerLaborHours='0';offerLaborRate=String(data.hourlySell??650)}render()} function qHome(){quoteFolderView='home';render()} let creatingQuoteFolder=false; function qNewFolder(){creatingQuoteFolder=true;render();let el=document.getElementById('newQuoteFolderName');if(el)el.focus()} function qCancelNewFolder(){creatingQuoteFolder=false;render()} function qSaveNewFolder(){let el=document.getElementById('newQuoteFolderName');let name=el?el.value.trim():'';if(!name){alert('Ange ett projektnamn för offerten.');if(el)el.focus();return}let id=Math.max(0,...data.projects.map(p=>Number(p.id)||0))+1;data.projects.push({id,name,archived:false,team:[],rates:{Arbetare:550,Arbetsledare:750},quoteOnly:true,quoteCompany:{...qCompany()}});selectedProject=id;creatingQuoteFolder=false;const quotes=ensureQuotes().quotes;quotes.push({id:Date.now()+Math.random(),projectId:id,title:name,customer:'',description:'',amount:0,vat:'ex',texts:[...ensureQuotes().snippets],status:'Påbörjad',date:new Date().toISOString()});save();qOpenFolder(id);qNewQuote();offerQuoteId=quotes[quotes.length-1].id;showToast('Offerten '+name+' har skapats. Spara den som påbörjad när du har fyllt i uppgifterna.')} function qFolderSummary(){let p=qproject(),q=ensureQuotes(),cs=q.calculations.filter(x=>String(x.projectId)===String(p?.id)),qs=q.quotes.filter(x=>String(x.projectId)===String(p?.id)),c=cs.at(-1),calcAmount=c?c.rows.reduce((a,r)=>a+qprice(r),0)+qnum(c.laborHours)*qnum(c.laborRate):0;return `

Projektpärm: ${esc(p?.name||'')}

Sammanställning av intern kalkyl och kundofferter.

Senaste kalkyl exkl. moms
${qmoney(calcAmount)}
Sparade kalkyler
${cs.length}
Offerter
${qs.length}

Intern kalkyl

${c?`

Senast sparad ${new Date(c.date).toLocaleDateString('sv-SE')}

${c.rows.map(r=>``).join('')}
MaterialMängdInköpsprisPåslagFörsäljningspris
${esc(r.name)}${r.qty} ${esc(r.unit)}${qmoney(r.price)} / ${esc(r.unit)}${r.markup} %${qmoney(qprice(r))}

Arbete: ${c.laborHours} timmar × ${qmoney(c.laborRate)} = ${qmoney(c.laborHours*c.laborRate)}

`:'

Ingen kalkyl sparad ännu. Välj Ändra kalkyl för att lägga till material och mängder.

'}

Kundofferter

${qs.filter(x=>!x.archived&&!quoteProject(x)).map(x=>quoteRow(x)).join('')||'

Inga offerter skapade ännu.

'}
`} function openAcceptedProject(id){let p=data.projects.find(x=>!x.quoteOnly&&(String(x.sourceQuoteId)===String(id)||String(x.acceptedQuoteId)===String(id)));if(!p)return alert('Projektet hittades inte.');selectedProject=p.id;projectListMode=false;page='Projekt';render()} document.addEventListener('click',function(ev){ const button=ev.target.closest('[data-accept-quote]'); if(!button)return; ev.preventDefault();ev.stopPropagation(); if(button.disabled)return; button.disabled=true;button.textContent='Skapar projekt…'; acceptQuote(button.getAttribute('data-accept-quote'),button); },true); function acceptQuote(id,button){ const feedback=message=>{if(button){button.disabled=false;button.textContent='✓ Godkänn & skapa projekt'}alert(message)}; try{ const quotes=ensureQuotes(),quote=quotes.quotes.find(x=>String(x.id)===String(id)); if(!quote)throw Error('Offerten hittades inte. Spara offerten först.'); const folder=project(quote.projectId); if(!folder)throw Error('Offertpärmen hittades inte.'); const prior=data.projects.find(x=>!x.quoteOnly&&(String(x.sourceQuoteId)===String(id)||String(x.acceptedQuoteId)===String(id))); if(prior){quote.status='Godkänd';save();offerTab='sammanstallning';render();showToast('Projektet finns redan.');return} const copy=x=>JSON.parse(JSON.stringify(x)); const calc=(quotes.calculations||[]).filter(c=>String(c.projectId)===String(folder.id)).at(-1); const work={ id:Math.max(0,...data.projects.map(x=>Number(x.id)||0))+1, name:folder.name,archived:false,quoteOnly:false,team:[], rates:{Arbetare:550,Arbetsledare:750},sourceQuoteId:id,sourceFolderId:folder.id, acceptedQuoteId:id,budgetAmount:Number(quote.amount)||0, budgetSource:'Godkänd offert',budgetQuoteTitle:quote.title||'', customer:quote.customer||'',customerContact:quote.customerContact||'', customerAddress:quote.customerAddress||'',description:quote.description||'', quoteCompany:copy(folder.quoteCompany||{}), acceptedQuoteSnapshot:copy(quote),acceptedCalculation:calc?copy(calc):null }; quote.status='Godkänd';data.projects.push(work); let persisted=true; try{localStorage.setItem('tidlett-clean-v159',JSON.stringify(data))} catch(error){persisted=false;console.warn(error)} offerTab='sammanstallning'; try{render()}catch(error){console.error('Kunde inte uppdatera sidan',error);feedback('Projektet skapades men sidan kunde inte uppdateras: '+error.message);return} if(persisted)showToast('✓ Godkänd! Projektet är skapat med budget och offertuppgifter.'); else alert('Projektet skapades men webbläsaren blockerar lagring. Projektet kan försvinna vid omladdning.'); }catch(error){console.error(error);feedback('Kunde inte skapa projekt: '+error.message)} } function exportUEProjectTime(pid){ const p=project(pid);if(!p)return alert('Projektet saknas.'); const ueEntries=data.entries.filter(e=>e.projectId===pid&&e.ueId).sort((a,b)=>a.date.localeCompare(b.date)); const ues=ueInit(),name=id=>ues.find(u=>u.id===id)?.name||'UE'; const grouped=[...new Set(ueEntries.map(e=>e.ueId))]; const body=grouped.map(uid=>{ const rows=ueEntries.filter(e=>e.ueId===uid),total=sum(rows); return `

${esc(name(uid))} – ${fmt(total)} timmar

${rows.map(e=>``).join('')}
DatumMedarbetareArbetatTimmar
${esc(e.date)}${esc(entryPerson(e))}${esc(e.description||'')}${fmt(e.hours)}
`; }).join(''); const html=`UE-tid – ${esc(p.name)}

UE-tid – ${esc(p.name)}

Underlag för avstämning mot UE-fakturor · ${new Date().toLocaleDateString('sv-SE')}

Totalt: ${fmt(sum(ueEntries))} timmar · ${ueEntries.length} tidrapporter

${body||'

Ingen UE-tid registrerad på projektet.

'}`; const win=window.open('','_blank'); if(!win)return alert('Tillåt popupfönster för att öppna UE-underlaget.'); win.document.open();win.document.write(html);win.document.close(); } function openMaterialEdit(id){ const m=ensureQuotes().materials.find(x=>String(x.id)===String(id)); if(!m)return alert('Materialet hittades inte.'); const modal=document.getElementById('modal'); modal.classList.remove('hidden'); modal.innerHTML=`

Ändra material

Uppdatera priset för kommande kalkyler. Redan sparade kalkylrader ändras inte.

`; const form=document.getElementById('editMaterialForm'); form.elements.name.value=m.name||''; form.elements.unit.value=m.unit||''; form.elements.price.value=m.price??0; form.elements.name.focus(); } function saveMaterialEdit(id){ const form=document.getElementById('editMaterialForm'); if(!form)return; const m=ensureQuotes().materials.find(x=>String(x.id)===String(id)); if(!m)return alert('Materialet hittades inte.'); const name=form.elements.name.value.trim(),unit=form.elements.unit.value.trim(),price=Number(form.elements.price.value); const error=document.getElementById('materialEditError'); if(!name||!unit||form.elements.price.value===''||!Number.isFinite(price)||price<0){if(error)error.textContent='Fyll i namn, enhet och ett giltigt pris.';return} const old={name:m.name,unit:m.unit,price:m.price}; Object.assign(m,{name,unit,price}); try{save()}catch(e){Object.assign(m,old);if(error)error.textContent='Kunde inte spara: '+e.message;return} closeModal();render();showToast('Material och pris uppdaterade!'); } function deleteMaterialPrice(id){ if(!confirm('Ta bort materialet från prislistan? Befintliga kalkyler påverkas inte.'))return; const q=ensureQuotes();q.materials=q.materials.filter(m=>String(m.id)!==String(id));save();render();showToast('Material borttaget'); } function updateMaterialPrice(id,key,value){ const m=ensureQuotes().materials.find(x=>String(x.id)===String(id));if(!m)return; if(key==='price'){const n=Number(value);if(!Number.isFinite(n)||n<0)return alert('Ange ett giltigt pris.');m.price=n} else m[key]=String(value).trim(); save();render(); } function customerInvoiceList(){data.customerInvoices??=[];return data.customerInvoices} function customerInvoicePanel(p,baseBudget,atas){ const list=customerInvoiceList().filter(i=>String(i.projectId)===String(p.id)); const lines=[{key:'base',title:'Grundprojekt',budget:baseBudget},...atas.map(a=>({key:String(a.id),title:ataLabel(a)+' – '+a.title+' ('+(a.priceType==='running'?'löpande':'fastpris')+')',budget:ataRevenue(a)}))]; const totalBudget=lines.reduce((n,l)=>n+l.budget,0); const invoiced=list.reduce((n,i)=>n+Number(i.amount||0),0); const row=lines.map(l=>{const paid=list.filter(i=>String(i.target)===l.key).reduce((n,i)=>n+Number(i.amount||0),0);return `${esc(l.title)}${kr(l.budget)}${kr(paid)}${kr(l.budget-paid)}`}).join(''); const opts=lines.map(l=>``).join(''); return `
Fakturering till kund · ${kr(invoiced)} fakturerat · ${kr(totalBudget-invoiced)} kvar

Registrera skickade kundfakturor separat för grundprojekt och respektive ÄTA. Detta påverkar inte budget eller vinst.

Fakturerat hittills
${kr(invoiced)}
Kvar att fakturera
${kr(totalBudget-invoiced)}
${row}
DelBudgetFaktureratKvar
Totalt${kr(totalBudget)}${kr(invoiced)}${kr(totalBudget-invoiced)}
+ Registrera kundfaktura
${list.map(i=>``).join('')||''}
DatumFakturanummerGällerBelopp exkl. momsÅtgärd
${esc(i.date)}${esc(i.number)}${esc(lines.find(l=>l.key===String(i.target))?.title||'Tidigare ÄTA')}${kr(i.amount)}
Inga kundfakturor registrerade.
`; } function addCustomerInvoice(event,projectId){ event.preventDefault(); const f=new FormData(event.target),target=String(f.get('target')),number=String(f.get('number')||'').trim(),date=String(f.get('date')),amount=Number(f.get('amount')); const validTargets=['base',...ataItems().filter(a=>String(a.projectId)===String(projectId)&&['Godkänd','Färdig'].includes(a.status)).map(a=>String(a.id))]; if(!validTargets.includes(target)||!number||!date||!Number.isFinite(amount)||amount<=0)return alert('Kontrollera fakturans uppgifter.'); customerInvoiceList().push({id:Date.now()+Math.floor(Math.random()*100000),projectId,target,number,date,amount}); save();render();showToast('Kundfaktura registrerad'); } function editCustomerInvoice(id){ const i=customerInvoiceList().find(i=>Number(i.id)===Number(id));if(!i)return; const number=prompt('Fakturanummer:',i.number);if(number===null)return; const date=prompt('Fakturadatum (ÅÅÅÅ-MM-DD):',i.date);if(date===null)return; const amountText=prompt('Belopp exkl. moms:',i.amount);if(amountText===null)return; const amount=Number(String(amountText).replace(',','.')); if(!number.trim()||!/^\d{4}-\d{2}-\d{2}$/.test(date)||!Number.isFinite(amount)||amount<=0)return alert('Ogiltiga fakturauppgifter.'); Object.assign(i,{number:number.trim(),date,amount});save();render(); } function deleteCustomerInvoice(id){ if(!confirm('Ta bort den registrerade kundfakturan?'))return; data.customerInvoices=customerInvoiceList().filter(i=>Number(i.id)!==Number(id));save();render(); } function projectFinance(p){ data.projectInvoices??=[]; const invoices=data.projectInvoices.filter(x=>Number(x.projectId)===Number(p.id)); const entries=data.entries.filter(e=>Number(e.projectId)===Number(p.id)); const own=entries.filter(e=>!e.ueId),ue=entries.filter(e=>e.ueId); const ownH=sum(own),ueH=sum(ue),hours=ownH+ueH; const ownCost=own.reduce((v,e)=>v+Number(e.hours||0)*Number(p.ownHourlyCost??data.hourlyCost??600),0); const ueCost=ue.reduce((v,e)=>v+Number(e.hours||0)*Number(p.uePrices?.[e.ueId]?.cost??ueInit().find(u=>u.id===e.ueId)?.hourlyCost??data.hourlyUECost??600),0); const invoiceCost=invoices.reduce((v,x)=>v+Number(x.amount||0),0); const ueBills=(data.ueBills||[]).filter(x=>String(x.projectId)===String(p.id)); const ueBillCost=ueBills.reduce((v,x)=>v+Number(x.cost||0),0); const ueBillRevenue=ueBills.filter(x=>x.allocation==='additional').reduce((v,x)=>v+Number(x.customerAmount||0),0); const atas=ataItems().filter(a=>Number(a.projectId)===Number(p.id)&&['Godkänd','Färdig'].includes(a.status)); const approvedFixed=atas.filter(a=>a.priceType!=='running').reduce((v,a)=>v+ataRevenue(a),0); const runningRevenue=atas.filter(a=>a.priceType==='running').reduce((v,a)=>v+ataRevenue(a),0); const baseEntries=entries.filter(e=>!e.ataId); const baseRunningRevenue=p.billingType==='running'?baseEntries.reduce((v,e)=>v+Number(e.hours||0)*Number(e.ueId?p.uePrices?.[e.ueId]?.sell??p.ueCustomerHourlyRate??600:p.customerHourlyRate??600),0):0; const baseBudget=p.billingType==='running'?0:Number(p.budgetAmount)||0; const budget=baseBudget+baseRunningRevenue+approvedFixed+runningRevenue+ueBillRevenue; const spent=ownCost+ueCost+invoiceCost+ueBillCost; const hasForecast=p.remainingCostEstimate!==undefined&&p.remainingCostEstimate!==null&&String(p.remainingCostEstimate)!==''; const remainingEstimate=hasForecast?Math.max(0,Number(p.remainingCostEstimate)||0):0; const balance=budget-spent; const forecast=balance-remainingEstimate; const margin=budget>0?forecast/budget*100:0; const pct=budget>0?Math.max(0,Math.min(100,spent/budget*100)):0; const costGroup=(ataId,kind)=>{const a=ataItems().find(a=>String(a.id)===String(ataId));return a?.priceType==='running'?'running':a?'fixed':'base'}; const laborCost=e=>Number(e.hours||0)*(e.ueId?Number(p.uePrices?.[e.ueId]?.cost??ueInit().find(u=>u.id===e.ueId)?.hourlyCost??data.hourlyUECost??600):Number(p.ownHourlyCost??data.hourlyCost??600)); const groupCost=kind=>entries.filter(e=>costGroup(e.ataId)===kind).reduce((v,e)=>v+laborCost(e),0)+invoices.filter(x=>costGroup(x.ataId)===kind).reduce((v,x)=>v+Number(x.amount||0),0); const fixedCost=groupCost('fixed')+ueBills.filter(x=>x.allocation==='ata').reduce((v,x)=>v+Number(x.cost||0),0),runningCost=groupCost('running'),baseCost=groupCost('base')+ueBills.filter(x=>x.allocation!=='ata').reduce((v,x)=>v+Number(x.cost||0),0); const stat=(label,value,color)=>`
${label}
${typeof value==='string'?esc(value):kr(value)}
`; return `

Projektets ekonomi

Alla belopp exkl. moms · uppdateras automatiskt

${stat('Total projektbudget',budget)}${stat('Kostnader hittills',spent)}${stat('Kvar efter kostnader',balance,'green')}

Kvar efter kostnader är inte slutlig vinst. Kommande kostnader är ännu inte avdragna.

Visa ekonomisk sammanställning
DelIntäktsbudgetKostnader hittillsKvar
Grundprojekt inkl. egna kostnader (${p.billingType==='running'?'löpande':'fastpris'})${kr(baseBudget+baseRunningRevenue)}${kr(baseCost)}${kr(baseBudget+baseRunningRevenue-baseCost)}
Fastpris-ÄTA${kr(approvedFixed)}${kr(fixedCost)}${kr(approvedFixed-fixedCost)}
Löpande ÄTA, timmar och material med påslag${kr(runningRevenue)}${kr(runningCost)}${kr(runningRevenue-runningCost)}
Tillkommande UE-arbeten utan ÄTA${kr(ueBillRevenue)}Ingår i grundprojektets kostnader${kr(ueBillRevenue)}
Totalt${kr(budget)}${kr(spent)}${kr(balance)}

Arbetstimmar och kostnader

${stat('Egen personal',fmt(ownH)+' h')}${stat('UE',fmt(ueH)+' h')}${stat('Totalt registrerad tid',fmt(hours)+' h')}
KostnadBelopp
Egen personal, beräknad kostnad${kr(ownCost)}
UE, beräknad kostnad${kr(ueCost)}
Registrerade leverantörsfakturor${kr(invoiceCost)}
Separat registrerade UE-fakturor${kr(ueBillCost)}
Totala kostnader${kr(spent)}

Registrera inte samma UE-kostnad både som separat UE-faktura, vanlig leverantörsfaktura och UE-tid. Automatisk avstämning finns ännu inte.

Registrerade timmar, material och UE-kostnader ingår. Fakturering till kund räknas inte som en ny intäkt.

Vinstprognos (valfritt) ${hasForecast?'· '+kr(forecast):''}

Ange hur mycket arbete, material och andra kostnader du tror återstår.

${hasForecast?`
${stat('Beräknad slutvinst',forecast,'green')}${stat('Prognostiserad vinstmarginal',margin.toFixed(1)+' %')}
`:'

Ingen vinstprognos angiven ännu.

'}

Beräknad slutvinst = total projektbudget − registrerade kostnader − uppskattade återstående kostnader.

${customerInvoicePanel(p,baseBudget,atas)}
Kostnadsfördelning och budgetinställningar
Egen personal (${fmt(ownH)} h)${kr(ownCost)}
UE (${fmt(ueH)} h)${kr(ueCost)}
Material och andra registrerade leverantörsfakturor${kr(invoiceCost)}
Separata UE-fakturor${kr(ueBillCost)}

Obs: UE-fakturor som redan motsvaras av UE-timmar kan dubbelräknas. Kontrollera att kostnaderna är registrerade korrekt.

`; } function setProjectRemainingCost(id,value){const p=project(id);if(!p)return;p.remainingCostEstimate=String(value).trim()===''?null:Math.max(0,Number(value)||0);save();render()} function closeProjectQuotePreview(){document.getElementById('projectQuoteOverlay')?.remove()} function showProjectQuotePreview(id){ const p=project(id);if(!p)return; const q=ensureQuotes().quotes.find(x=>String(x.id)===String(p.acceptedQuoteId||p.sourceQuoteId)); const snap=p.acceptedQuoteSnapshot||q; if(!snap){alert('Ingen godkänd offert finns kopplad.');return} const calc=(ensureQuotes().calculations||[]).filter(x=>String(x.projectId)===String(q?.projectId||p.sourceFolderId)).at(-1); const costs=calc?(calc.rows||[]).reduce((sum,r)=>sum+qcost(r),0):0; const sell=calc?(calc.rows||[]).reduce((sum,r)=>sum+qprice(r),0)+qnum(calc.laborHours)*qnum(calc.laborRate):0; const overlay=document.createElement('div');overlay.id='projectQuoteOverlay';overlay.style.cssText='position:fixed;inset:0;background:#0009;z-index:20000;overflow:auto;padding:24px'; overlay.onclick=e=>{if(e.target===overlay)closeProjectQuotePreview()}; overlay.innerHTML=`

Offert och intern kalkyl

OFFERT

${esc(p.name)}

${esc(snap.title||p.name)}

Beställare: ${esc(snap.customer||p.customer||'')}

Arbetsadress: ${esc(snap.customerAddress||p.customerAddress||'')}

Arbetets omfattning

${esc(snap.description||p.description||'')}

${(snap.texts||[]).length?`

Omfattning och villkor

    ${snap.texts.map(t=>`
  • ${esc(t)}
  • `).join('')}
`:''}${snap.extraTitle||snap.extraText?`

${esc(snap.extraTitle||'Extra avsnitt')}

${esc(snap.extraText||'')}

`:''}

Offertpris: ${qmoney(snap.amount||0)} exkl. moms

Intern kalkyl – endast för företaget

${calc?`
${(calc.rows||[]).map(r=>``).join('')}
MaterialAntalInköpFörsäljning
${esc(r.name)}${esc(r.qty)} ${esc(r.unit)}${qmoney(qcost(r))}${qmoney(qprice(r))}

Materialinköp: ${qmoney(costs)} · Arbetstid: ${fmt(calc.laborHours)} h × ${qmoney(calc.laborRate)}

Kalkylsumma: ${qmoney(sell)}

`:'

Ingen sparad kalkyl kopplad till offerten.

'}
`; document.body.appendChild(overlay); } function projectUnderlagPanel(p){return `
Projektunderlag – Visa / dölj
${p.acceptedQuoteSnapshot?`

Godkänd offert: ${esc(p.acceptedQuoteSnapshot.title||'')}

${p.acceptedQuoteId&&ensureQuotes().quotes.some(q=>String(q.id)===String(p.acceptedQuoteId))?``:''}

Kund: ${esc(p.customer||'')}

Kontakt: ${esc(p.customerContact||'')}

Adress: ${esc(p.customerAddress||'')}

Arbetsbeskrivning: ${esc(p.description||'')}

`:'

Ingen godkänd offert kopplad.

'}
`;} const DEFAULT_COST_CATEGORIES=['Betong','Virke','El','VVS','Markarbete','Maskiner','Transport','Hyra','Övrigt']; function costCategories(){data.costCategories??=[...DEFAULT_COST_CATEGORIES];return data.costCategories} function categoryForSupplier(supplier){ const norm=String(supplier||'').trim().toLocaleLowerCase('sv-SE'); const matches=(data.projectInvoices||[]).filter(i=>String(i.supplier||'').trim().toLocaleLowerCase('sv-SE')===norm&&i.category); const freq={};matches.forEach(i=>freq[i.category]=(freq[i.category]||0)+1); return Object.keys(freq).sort((a,b)=>freq[b]-freq[a])[0]||''; } function suggestInvoiceCategory(el){ const category=el.form?.elements?.namedItem('category');if(!category)return; const guess=categoryForSupplier(el.value); if(guess&&[...category.options].some(o=>o.value===guess))category.value=guess; } function addCostCategory(){tidlettInvoiceFormOpen=true; const name=prompt('Namn på ny kostnadskategori:');if(!name?.trim())return; const cleaned=name.trim(); if(costCategories().some(c=>c.toLocaleLowerCase('sv-SE')===cleaned.toLocaleLowerCase('sv-SE')))return alert('Kategorin finns redan.'); costCategories().push(cleaned);save();render(); } function renameCostCategory(){tidlettInvoiceFormOpen=true; const old=prompt('Vilken kategori vill du byta namn på?');if(!old)return; const found=costCategories().find(c=>c.toLocaleLowerCase('sv-SE')===old.trim().toLocaleLowerCase('sv-SE'));if(!found)return alert('Kategorin hittades inte.'); const next=prompt('Nytt namn:',found);if(!next?.trim())return; if(costCategories().some(c=>c!==found&&c.toLocaleLowerCase('sv-SE')===next.trim().toLocaleLowerCase('sv-SE')))return alert('Namnet finns redan.'); data.costCategories=data.costCategories.map(c=>c===found?next.trim():c); (data.projectInvoices||[]).forEach(i=>{if(i.category===found)i.category=next.trim()}); save();render(); } function deleteCostCategory(){tidlettInvoiceFormOpen=true; const name=prompt('Vilken kategori vill du ta bort?');if(!name)return; const found=costCategories().find(c=>c.toLocaleLowerCase('sv-SE')===name.trim().toLocaleLowerCase('sv-SE'));if(!found)return; if(!confirm('Ta bort '+found+'? Tidigare fakturor flyttas till Övrigt.'))return; data.costCategories=data.costCategories.filter(c=>c!==found); if(!data.costCategories.includes('Övrigt'))data.costCategories.push('Övrigt'); (data.projectInvoices||[]).forEach(i=>{if(i.category===found)i.category='Övrigt'}); save();render(); } function projectCostCategories(p){ const invoices=(data.projectInvoices||[]).filter(i=>Number(i.projectId)===Number(p.id)); const groups={};invoices.forEach(i=>{const cat=i.category||'Okategoriserat';(groups[cat]??=[]).push(i)}); const names=Object.keys(groups).sort((a,b)=>groups[b].reduce((v,i)=>v+Number(i.amount||0),0)-groups[a].reduce((v,i)=>v+Number(i.amount||0),0)); return `
Kostnader per kategori · ${kr(invoices.reduce((v,i)=>v+Number(i.amount||0),0))}

Leverantörsfakturor i detta projekt. Klicka på en kategori för att se fakturorna. Personaltimmar ingår inte i denna material- och leverantörsöversikt.

${names.map(name=>`
${esc(name)} · ${kr(groups[name].reduce((v,i)=>v+Number(i.amount||0),0))} (${groups[name].length} fakturor)
${groups[name].map(i=>``).join('')}
DatumLeverantörFakturanrBelopp
${esc(i.date)}${esc(i.supplier)}${esc(i.number||'–')}${kr(i.amount)}
`).join('')||'

Inga leverantörsfakturor registrerade ännu.

'}
`; } function companyCostStatistics(){ const invoices=data.projectInvoices||[]; const groups={};invoices.forEach(i=>{const cat=i.category||'Okategoriserat';(groups[cat]??=[]).push(i)}); return `
Kostnadsstatistik · alla projekt

Sammanställning av leverantörsfakturor exkl. moms. Välj projekt, år eller leverantör för att filtrera.

`; } function updateCostStats(){ const root=document.getElementById('costStatResult');if(!root)return; const pid=document.getElementById('costStatProject')?.value||'',year=document.getElementById('costStatYear')?.value||'',supplier=(document.getElementById('costStatSupplier')?.value||'').toLocaleLowerCase('sv-SE'); const invoices=(data.projectInvoices||[]).filter(i=>(!pid||String(i.projectId)===pid)&&(!year||String(i.date||'').startsWith(year))&&String(i.supplier||'').toLocaleLowerCase('sv-SE').includes(supplier)); const groups={};invoices.forEach(i=>{const cat=i.category||'Okategoriserat';(groups[cat]??=[]).push(i)}); root.innerHTML=`

Totalt: ${kr(invoices.reduce((v,i)=>v+Number(i.amount||0),0))} · ${invoices.length} fakturor

`+Object.keys(groups).sort((a,b)=>groups[b].reduce((v,i)=>v+Number(i.amount||0),0)-groups[a].reduce((v,i)=>v+Number(i.amount||0),0)).map(cat=>`
${esc(cat)} · ${kr(groups[cat].reduce((v,i)=>v+Number(i.amount||0),0))}
${groups[cat].map(i=>``).join('')}
ProjektLeverantörDatumBelopp
${esc(data.projects.find(p=>String(p.id)===String(i.projectId))?.name||'–')}${esc(i.supplier)}${esc(i.date)}${kr(i.amount)}
`).join('')||'

Inga fakturor matchar filtren.

'; } function projectOverviewNotes(p){ const pending=ataItems().filter(a=>String(a.projectId)===String(p.id)&&a.status==='Skickad').length; const inv=(data.customerInvoices||[]).filter(i=>String(i.projectId)===String(p.id)); const invoiced=inv.reduce((n,i)=>n+Number(i.amount||0),0); return `

Snabböversikt

Fakturerat till kund: ${kr(invoiced)}

${pending?`⏳ ${pending} ÄTA väntar på godkännande`:'Inga skickade ÄTA väntar på godkännande.'}

Fler detaljer hittar du under flikarna Ekonomi, ÄTA, Tid och Fakturor.

`; } let tidlettProjectTab='overview'; let tidlettEconomySubtab='summary',tidlettUEBillFormOpen=false; function setTidlettEconomySubtab(tab){tidlettEconomySubtab=tab;render()} function projectRatesAndUE(p){ const assigned=ueInit().filter(u=>(u.projects||[]).some(id=>String(id)===String(p.id))); const bills=(data.ueBills||[]).filter(b=>String(b.projectId)===String(p.id)); const moneyValue=n=>kr(Number(n)||0); return `

Timpriser & debitering

Alla priser exkl. moms. Här visas bara UE kopplade till projektet.

Egen personal

Projektets UE

${assigned.map(u=>{const cost=Number((p.uePrices||{})[u.id]?.cost??u.hourlyCost??data.hourlyUECost??600),sell=Number((p.uePrices||{})[u.id]?.sell??p.ueCustomerHourlyRate??600);return `
${esc(u.name)}

Täckningsbidrag: ${moneyValue(sell-cost)}/tim · ${fmt(sum(data.entries.filter(e=>Number(e.projectId)===Number(p.id)&&Number(e.ueId)===Number(u.id))))} h × ${kr(cost)} = ${kr(sum(data.entries.filter(e=>Number(e.projectId)===Number(p.id)&&Number(e.ueId)===Number(u.id)))*cost)} i kostnad

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

Inga UE kopplade till projektet. Koppla UE under Personal & UE.

'}

Separata UE-fakturor

För UE-fakturor som inte redan lagts in under vanliga leverantörsfakturor. Beloppen registreras en gång.

${tidlettUEBillFormOpen?'Registrera UE-faktura':'Visa formulär'}
${bills.map(b=>``).join('')||''}
UE / beskrivningTypKostnadÖkat projektvärde
${esc(b.supplier)}${esc(b.allocation==='base'?'Grundprojekt':b.allocation==='own'?'Egen kostnad':b.allocation==='ata'?'Befintlig ÄTA':'Tillkommande arbete')}${b.allocation==='ata'?' – '+esc(ataItems().find(a=>String(a.id)===String(b.ataId))?.title||'Borttagen ÄTA'):''}${kr(b.cost)}${kr(b.allocation==='additional'?b.customerAmount:0)}
Inga separata UE-fakturor registrerade.
`; } function setProjectPrice(id,key,value){const p=project(id),n=Number(value);if(!p||!['ownHourlyCost','customerHourlyRate'].includes(key)||!Number.isFinite(n)||n<0)return alert('Ogiltigt pris');p[key]=n;save();render()} function setUEProjectPrice(id,ueId,key,value){const p=project(id),n=Number(value);if(!p||!['cost','sell'].includes(key)||!Number.isFinite(n)||n<0)return alert('Ogiltigt pris');p.uePrices??={};p.uePrices[ueId]??={};p.uePrices[ueId][key]=n;save();render()} function toggleUEBillForm(){tidlettUEBillFormOpen=!tidlettUEBillFormOpen;render();if(tidlettUEBillFormOpen)document.getElementById('ueBillForm')?.scrollIntoView({block:'nearest'})} function updateUEBillAllocation(f){const t=f.elements.allocation.value;f.querySelector('.ue-bill-ata').style.display=t==='ata'?'block':'none';f.querySelector('.ue-bill-revenue').style.display=t==='additional'?'block':'none';f.elements.customerAmount.required=t==='additional'} function saveUEBill(ev,id){ev.preventDefault();const f=ev.target,fd=new FormData(f),allocation=String(fd.get('allocation')),cost=Number(fd.get('cost')),customerAmount=allocation==='additional'?Number(fd.get('customerAmount')):0,supplier=String(fd.get('supplier')||'').trim(),ataId=allocation==='ata'?String(fd.get('ataId')||''):'';if(!supplier||!Number.isFinite(cost)||cost<0||!Number.isFinite(customerAmount)||customerAmount<0)return alert('Kontrollera beloppen');if(allocation==='ata'&&!ataItems().some(a=>String(a.id)===ataId&&String(a.projectId)===String(id)))return alert('Välj en befintlig ÄTA på projektet');data.ueBills??=[];data.ueBills.push({id:Date.now(),projectId:id,supplier,allocation,ataId,cost,customerAmount});tidlettUEBillFormOpen=false;save();render();showToast('UE-fakturan har registrerats')} function editUEBill(id){const b=(data.ueBills||[]).find(x=>Number(x.id)===Number(id));if(!b)return;const supplier=prompt('UE / beskrivning',b.supplier);if(supplier===null)return;const costText=prompt('Kostnad för företaget (kr)',b.cost);if(costText===null)return;const cost=Number(costText.replace(',','.'));if(!supplier.trim()||!Number.isFinite(cost)||cost<0)return alert('Ogiltiga uppgifter');let customerAmount=b.customerAmount;if(b.allocation==='additional'){const text=prompt('Debiteras kund (kr)',b.customerAmount);if(text===null)return;customerAmount=Number(text.replace(',','.'));if(!Number.isFinite(customerAmount)||customerAmount<0)return alert('Ogiltigt kundbelopp')}Object.assign(b,{supplier:supplier.trim(),cost,customerAmount});save();render()} function deleteUEBill(id){if(!confirm('Ta bort UE-fakturan?'))return;data.ueBills=(data.ueBills||[]).filter(x=>Number(x.id)!==Number(id));save();render()} function setTidlettProjectTab(tab){tidlettProjectTab=tab;render()} function projectQuickNew(id){ const choice=prompt('Vad vill du lägga till?\n1. Leverantörsfaktura\n2. Kundfaktura\n3. ÄTA\n4. Tidsrapport\n\nSkriv 1–4:'); if(choice==='3'){selectedProject=id;go('ÄTA-hantering');return} if(choice==='4'){projectToDiary(id);return} const tab=choice==='1'?'invoices':choice==='2'?'economy':null; if(!tab)return; tidlettProjectTab=tab;render(); if(choice==='1')tidlettInvoiceFormOpen=true;render();const target=document.querySelector(choice==='1'?'#projectInvoiceEntry':'#customerInvoiceSection'); if(target){target.open=true;target.scrollIntoView({behavior:'smooth',block:'start'})} } function projectInvoicesPanel(p){ const invoices=(data.projectInvoices||[]).filter(x=>String(x.projectId)===String(p.id)); const total=invoices.reduce((v,x)=>v+Number(x.amount||0),0); const categories={};invoices.forEach(x=>(categories[x.category||'Okategoriserat']??=[]).push(x)); const groups=Object.entries(categories).sort((a,b)=>b[1].reduce((v,x)=>v+Number(x.amount||0),0)-a[1].reduce((v,x)=>v+Number(x.amount||0),0)); const catHtml=groups.map(([name,items])=>`
${esc(name)} — ${kr(items.reduce((v,x)=>v+Number(x.amount||0),0))}
${items.map(x=>``).join('')}
LeverantörDatumFakturanrBelopp
${esc(x.supplier)}${esc(x.date)}${esc(x.number||'–')}${kr(x.amount)}
`).join(''); return `

Leverantörsfakturor

Endast ${esc(p.name)} · belopp exkl. moms

Totala leverantörsfakturor · ${invoices.length} st
${kr(total)}
Visa sammanställning och fakturor ▾
${catHtml||'

Inga leverantörsfakturor ännu.

'}
+ Registrera leverantörsfaktura
Används bara för godkänd löpande ÄTA.
Alla registrerade fakturor (${invoices.length})
${invoices.map(x=>``).join('')||''}
DatumLeverantörFakturanrKategoriBeloppÅtgärd
${esc(x.date)}${esc(x.supplier)}${esc(x.number||'–')}${esc(x.category||'Okategoriserat')}${kr(x.amount)}
Inga fakturor registrerade.
`; } let tidlettInvoiceFormOpen=false,tidlettInvoiceLastSupplier='',tidlettInvoiceLastCategory='Betong'; function toggleProjectInvoiceForm(){tidlettInvoiceFormOpen=true;render();document.getElementById('projectInvoiceEntry')?.scrollIntoView({block:'nearest'})} function editProjectInvoice(id){ const i=(data.projectInvoices||[]).find(x=>Number(x.id)===Number(id));if(!i)return; const supplier=prompt('Leverantör:',i.supplier);if(supplier===null)return; const number=prompt('Fakturanummer:',i.number||'');if(number===null)return; const date=prompt('Datum (ÅÅÅÅ-MM-DD):',i.date);if(date===null)return; const category=prompt('Kostnadskategori:',i.category||'Övrigt');if(category===null)return; const amountText=prompt('Belopp exkl. moms:',i.amount);if(amountText===null)return; const amount=Number(amountText.replace(',','.')); if(!supplier.trim()||!category.trim()||!/^\d{4}-\d{2}-\d{2}$/.test(date)||!Number.isFinite(amount)||amount<=0)return alert('Kontrollera uppgifterna.'); if(!costCategories().includes(category.trim()))costCategories().push(category.trim()); Object.assign(i,{supplier:supplier.trim(),number,date,category:category.trim(),amount});save();render(); } function addProjectInvoice(ev,id){ ev.preventDefault(); const f=new FormData(ev.target),amount=Number(f.get('amount')),supplier=String(f.get('supplier')||'').trim(),number=String(f.get('number')||'').trim(); if(!supplier||!Number.isFinite(amount)||amount<=0)return alert('Ange leverantör och giltigt belopp.'); const duplicates=(data.projectInvoices||[]).some(x=>String(x.projectId)===String(id)&&number&&String(x.number||'').trim().toLocaleLowerCase('sv-SE')===number.toLocaleLowerCase('sv-SE')&&String(x.supplier||'').trim().toLocaleLowerCase('sv-SE')===supplier.toLocaleLowerCase('sv-SE')); if(duplicates&&!confirm('Samma fakturanummer från leverantören finns redan på projektet. Spara ändå?'))return; const category=String(f.get('category')||'Övrigt'),mode=ev.submitter?.value||'next'; data.projectInvoices??=[]; data.projectInvoices.push({id:Date.now()+Math.floor(Math.random()*10000),projectId:id,date:String(f.get('date')),supplier,number,amount,category,ataId:String(f.get('ataId')||''),markupPercent:Math.max(0,Number(f.get('markupPercent')??10))}); tidlettInvoiceLastSupplier=supplier;tidlettInvoiceLastCategory=category;tidlettInvoiceFormOpen=mode!=='close'; save();render();showToast('Fakturan har lagts till'); if(tidlettInvoiceFormOpen){const el=document.getElementById('projectInvoiceEntry');if(el){el.open=true;el.scrollIntoView({block:'nearest'})}} return false; } function deleteProjectInvoice(id){if(!confirm('Ta bort fakturan?'))return;data.projectInvoices=(data.projectInvoices||[]).filter(x=>x.id!==id);save();render()} function qDeleteQuoteFolder(id){const p=project(id);if(!p||!p.quoteOnly)return;const linked=data.projects.some(x=>!x.quoteOnly&&String(x.sourceFolderId)===String(id));const historic=data.entries.some(x=>String(x.projectId)===String(id))||(data.projectInvoices||[]).some(x=>String(x.projectId)===String(id))||(data.ataItems||[]).some(x=>String(x.projectId)===String(id));if(linked||historic){alert('Pärmen är kopplad till ett projekt eller har registrerad historik och kan inte tas bort. Arkivera offerterna i stället.');return}if(!confirm('Ta bort offertpärmen "'+p.name+'" och alla dess offerter och kalkyler permanent?'))return;const q=ensureQuotes();q.quotes=q.quotes.filter(x=>String(x.projectId)!==String(id));q.calculations=q.calculations.filter(x=>String(x.projectId)!==String(id));data.projects=data.projects.filter(x=>String(x.id)!==String(id));if(String(offerProject)===String(id))quoteFolderView='home';save();render();showToast('Offertpärmen är borttagen.')} function qFolderHome(){const q=ensureQuotes();const active=q.quotes.filter(x=>!quoteProject(x)&&!['Avböjd','Godkänd'].includes(quoteStatus(x))&&(!x.archived||['Utkast','Påbörjad'].includes(quoteStatus(x))));const history=q.quotes.filter(x=>!active.includes(x));return `

Offerter

Skapa och hantera offerter. Godkända projekt hittar du under Projekt.

${creatingQuoteFolder?`
`:''}

Offerter och status

Påbörjade offerter och offerter som inväntar svar.

${active.map(x=>quoteRow(x)).join('')||'

Inga offerter att hantera just nu.

'}
Alla offerter / Historik (${history.length})

Godkända, avböjda och arkiverade offerter. Här kan du öppna, återställa eller ta bort offerter utan projektkoppling.

${history.map(x=>`
${quoteRow(x)}
`).join('')||'

Ingen offerthistorik ännu.

'}
${quoteDeclinePending!==null?`

Markera offert som avböjd?

Är du säker på att kunden har tackat nej? Offerten flyttas till historiken men raderas inte.

`:''}

Gemensamma inställningar

`} function qFilterQuoteHistory(value){const term=String(value||'').toLocaleLowerCase('sv').trim();document.querySelectorAll('#quoteHistoryRows .quote-history-row').forEach(el=>el.hidden=!el.dataset.quoteSearch.includes(term))} function qCompany(){if(!data.quoteCompany)data.quoteCompany={name:data.companyName||'',org:'',address:'',postal:'',city:'',phone:'',email:'',website:'',contact:'',contactPhone:'',contactEmail:'',showNameUnderLogo:false};return data.quoteCompany} function qProjectCompany(){let p=qproject();if(!p)return qCompany();return p.quoteCompany||(p.quoteCompany={...qCompany()})} function qResetProjectCompany(){qproject().quoteCompany={...qCompany()};save();render()} var offerCustomerContact='',offerCustomerAddress='',offerExtraTitle='',offerExtraText=''; function qSetExtraTemplate(id){qSyncDraft();let x=(ensureQuotes().extraTemplates||[]).find(t=>String(t.id)===String(id));if(x){offerExtraTitle=x.title;offerExtraText=x.text;render()}} function qSaveExtraTemplate(){let title=document.getElementById('qExtraTemplateTitle')?.value.trim(),text=document.getElementById('qExtraTemplateText')?.value.trim();if(!title||!text){alert('Ange rubrik och text');return}let q=ensureQuotes();(q.extraTemplates??=[]).push({id:Date.now(),title,text});qStandardCreatePanel='';save();render();showToast('Reservationstext sparad. Välj den i kundofferten.')} function qCompanySave(){let f=document.getElementById('qCompanyForm');if(!f)return;let c=quoteFolderView==='shared'?qCompany():qProjectCompany();for(let el of f.querySelectorAll('[name]'))c[el.name]=el.type==='checkbox'?el.checked:el.value.trim();if(quoteFolderView==='shared')data.companyName=c.name||data.companyName;save();render();showToast('Företagsuppgifter och kontaktperson sparade.')} function qCompanyForm(){let c=quoteFolderView==='shared'?qCompany():qProjectCompany(),fields=[['name','Företagsnamn'],['org','Organisationsnummer'],['address','Gatuadress'],['postal','Postnummer'],['city','Ort'],['phone','Telefon'],['email','E-post'],['website','Webbplats'],['contact','Kontaktperson för offerter'],['contactPhone','Kontaktpersonens telefon'],['contactEmail','Kontaktpersonens e-post']];return `

Företagsuppgifter på offerten

${quoteFolderView==="shared"?"Standarduppgifter för nya projektpärmar.":"Förifyllt från standard. Ändringar gäller bara denna projektpärm."} Uppgifterna används på kundofferten och i utskriften. Företagets logotyp hämtas som tidigare från Inställningar.

${fields.map(([k,label])=>`
`).join('')}
${quoteFolderView!=='shared'?``:''}
`} function offerPage(){if(quoteFolderView==='home')return `
${qFolderHome()}
`;let q=ensureQuotes(),p=qproject(),tabs=[['sammanstallning','Sammanställning'],['kalkyl','Intern kalkyl'],['material','Materialprislista'],['standard','Standardtexter'],['offert','Kundoffert'],['historik','Sparat'],['foretag','Företagsuppgifter']];let head=`

Offert & kalkyl

Interna mängder och priser hålls separata från kundens offert.

${tabs.map(([id,name])=>``).join('')}
`;let body=''; if(offerTab==='foretag'){body=qCompanyForm()} if(offerTab==='sammanstallning'){body=qFolderSummary()} if(offerTab==='kalkyl'){body=`

Material och mängder

${offerDraft.map(r=>``).join('')||''}
MaterialMängdEnhetInköp/enhetPåslag %FörsäljningsprisTotalt
${esc(r.name)}${esc(r.unit)}${qmoney(qnum(r.price)*(1+qnum(r.markup)/100))}${qmoney(qprice(r))}
Välj ett material ovan för att börja kalkylera.

Arbetskostnad

Kalkylsammanställning

${qTotalsHtml()}
`} if(offerTab==='material'){body=`

Materialprislista

Standard 10 %. Enskilda kalkylrader kan ha egna påslag. Materialpriserna är interna.

${Math.min(q.materials.length,qMaterialsExpanded?q.materials.length:8)} av ${q.materials.length} material visas
${qSortedMaterials().map((m,i)=>`=8?'hidden':''}>`).join('')}
MaterialEnhetInköpsprisFörsäljningspris med ${q.markup} %Åtgärd
${esc(m.name)}${esc(m.unit)}${qmoney(m.price)}${qmoney(m.price*(1+q.markup/100))}

Lägg till material

`} if(offerTab==='standard'){ const q=ensureQuotes(),headings=[qStandardHeading('terms'),...q.customHeadings]; body=`

Standardtexter för offert

Redigera rubrikerna och texterna under varje rubrik. Valen i enskilda offerter påverkar inte mallarna.

`+ headings.map((h,hi)=>`

${esc(h)}

${hi?``:''}
${(q.snippets||[]).map((t,i)=>({t,i})).filter(({t})=>(q.snippetHeadings?.[t]||qStandardHeading('terms'))===h).map(({t,i})=>`
${esc(t)}
${qEditingSnippet===i?`
${qDeletePendingSnippet===i?`
Ta bort standardtexten?
`:''}
`:''}
`).join('')||'

Inga texter under rubriken ännu.

'}
${qStandardCreatePanel==='snippet'&&qNewSnippetHeading===h?`
`:''}
`).join('')+`

Extra textavsnitt – sparade mallar

${qStandardCreatePanel==='template'?`
`:''}

Sparade extra textavsnitt – mallar

${(q.extraTemplates||[]).map(x=>`
${esc(x.title)}
${esc(x.text)}
${qEditingTemplate===String(x.id)?`
${qDeletePendingTemplate===String(x.id)?``:``}
`:''}
`).join('')||'

Inga textmallar sparade ännu. Klicka på ”Extra textavsnitt – mallar” ovan för att skapa en.

'}
`; } if(offerTab==='offert'){body=`

Skapa kundoffert

Kontaktperson för denna offert

Förifylls från Företagsuppgifter, men kan ändras för just den här offerten.

Extra avsnitt (valfritt) – t.ex. reservationer

Fyll bara i om du behöver ett extra avsnitt. Tomma avsnitt visas inte i kundofferten.

${qQuotePreview()}`} if(offerTab==='historik'){let cs=q.calculations.filter(x=>x.projectId===p?.id),qs=q.quotes.filter(x=>x.projectId===p?.id);body=`

Sparade kalkyler – ${esc(p?.name||'')}

${cs.map(x=>`
${new Date(x.date).toLocaleDateString('sv-SE')} · ${x.rows.length} materialrader
`).join('')||'

Inga sparade kalkyler ännu.

'}

Sparade offerter

${qs.filter(x=>!x.archived&&!quoteProject(x)).map(x=>quoteRow(x)).join('')||'

Inga sparade offerter ännu.

'}
`} return `
${head+body}
`} // v147 – Löneperioder, frånvaro, utlägg och ritningslänk. Lokal demo, ingen serverautentisering. const todayISO=()=>{const d=new Date();return [d.getFullYear(),String(d.getMonth()+1).padStart(2,'0'),String(d.getDate()).padStart(2,'0')].join('-')}; function payPeriod(offset=0){const now=new Date(),start=new Date(now.getFullYear(),now.getMonth()+(now.getDate()<16?-1:0)+offset,16),end=new Date(start.getFullYear(),start.getMonth()+1,15);const iso=d=>[d.getFullYear(),String(d.getMonth()+1).padStart(2,'0'),String(d.getDate()).padStart(2,'0')].join('-');return [iso(start),iso(end)]} let payrollOffset=0,payrollPerson='',payrollStart='',payrollEnd=''; function payrollShift(delta){payrollOffset+=delta;[payrollStart,payrollEnd]=payPeriod(payrollOffset);render()} function payrollPage(){if(!payrollStart||!payrollEnd)[payrollStart,payrollEnd]=payPeriod();if(!payrollPerson)payrollPerson=String(data.people[0]?.id||'');const id=Number(payrollPerson),rows=data.entries.filter(e=>!e.ueId&&Number(e.personId)===id&&e.date>=payrollStart&&e.date<=payrollEnd).sort((a,b)=>a.date.localeCompare(b.date)),absence=(data.absences||[]).filter(x=>Number(x.personId)===id&&x.date>=payrollStart&&x.date<=payrollEnd).sort((a,b)=>a.date.localeCompare(b.date)),total=sum(rows),absTotal=absence.reduce((v,x)=>v+Number(x.hours||0),0),byProject=[...new Set(rows.map(e=>e.projectId))];return `

Löneunderlag · alla projekt

Registrerade timmar och frånvaro. Underlag för kontroll, inte en färdig löneberäkning.

Arbetstid
${fmt(total)} h
Frånvaro
${fmt(absTotal)} h
Totalt registrerat
${fmt(total+absTotal)} h

Timmar per projekt

${byProject.map(pid=>`
${esc(project(pid)?.name||'Tidigare projekt')}${fmt(sum(rows.filter(e=>e.projectId===pid)))} h
`).join('')||'

Ingen arbetstid under perioden.

'}

Arbetade dagar

${entryTable(rows)}

Frånvaro

${absence.map(x=>`
${esc(x.date)} · ${esc(x.type)}${fmt(x.hours)} h
`).join('')||'

Ingen frånvaro under perioden.

'}
`} function exportPayrollCSV(){const id=Number(payrollPerson),rows=[['Datum','Typ','Projekt','Beskrivning','Timmar']];data.entries.filter(e=>!e.ueId&&Number(e.personId)===id&&e.date>=payrollStart&&e.date<=payrollEnd).forEach(e=>rows.push([e.date,'Arbete',project(e.projectId)?.name||'',e.description||'',e.hours]));(data.absences||[]).filter(e=>Number(e.personId)===id&&e.date>=payrollStart&&e.date<=payrollEnd).forEach(e=>rows.push([e.date,e.type,'','Frånvaro',e.hours]));const csv='\ufeff'+rows.map(r=>r.map(x=>'"'+String(x??'').replaceAll('"','""')+'"').join(';')).join('\r\n');const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv;charset=utf-8'}));a.download='TIDLETT_lon_'+payrollStart+'_'+payrollEnd+'.csv';a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000)} const absenceTypes=['Sjuk','VAB','Semester','Arbetstidsförkortning','Kompledig','Tjänstledig']; function openAbsenceForm(id){const old=(data.absences||[]).find(x=>Number(x.id)===Number(id));const personId=old?.personId||(role()==='employee'?employeeId:Number(payrollPerson||data.people[0]?.id));const form=`

${old?'Ändra':'Registrera'} frånvaro

Vid flera dagar registreras måndag–fredag. Helger kan läggas till separat vid behov.

${old?``:''}
`;document.getElementById('content').innerHTML=form} function saveAbsence(ev,id){ev.preventDefault();const f=ev.target,personId=Number(f.personId.value),hours=Number(f.hours.value),start=f.start.value,end=f.end.value;if(end365)return alert('Kontrollera datum och timmar');data.absences??=[];if(id)data.absences=data.absences.filter(x=>Number(x.id)!==Number(id));const cursor=new Date(start+'T12:00:00'),stop=new Date(end+'T12:00:00');while(cursor<=stop){if(cursor.getDay()!==0&&cursor.getDay()!==6)data.absences.push({id:Date.now()+Math.random(),personId,type:f.type.value,date:[cursor.getFullYear(),String(cursor.getMonth()+1).padStart(2,'0'),String(cursor.getDate()).padStart(2,'0')].join('-'),hours});cursor.setDate(cursor.getDate()+1)}save();render();showToast('Frånvaron har sparats')} function editAbsence(id){openAbsenceForm(id)} function deleteAbsence(id){if(!confirm('Ta bort frånvaron?'))return;data.absences=(data.absences||[]).filter(x=>Number(x.id)!==Number(id));save();render()} function openExpenseForm(){document.getElementById('content').innerHTML=`

Registrera utlägg

Kvittot lagras lokalt i denna testversion. Godkännande och projektkostnad sker separat.

`} async function saveExpense(ev){ev.preventDefault();const f=ev.target,file=f.receipt.files[0];if(!file||file.size>1024*1024)return alert('Välj ett kvitto på högst 1 MB i testversionen');if(!['application/pdf','image/jpeg','image/png','image/webp','image/heic'].includes(file.type))return alert('Välj en bild eller PDF');const receipt=await new Promise((resolve,reject)=>{const r=new FileReader();r.onload=()=>resolve(r.result);r.onerror=reject;r.readAsDataURL(file)});data.expenses??=[];data.expenses.push({id:Date.now(),personId:Number(f.personId.value),date:f.date.value,amount:Number(f.amount.value),projectId:f.projectId.value?Number(f.projectId.value):null,description:f.description.value,receipt,receiptName:file.name,status:'pending',projectBooked:false});try{save();render();showToast('Utlägget skickat för godkännande')}catch(e){alert('Kunde inte spara kvittot. Försök med en mindre bild.')}} function expensesPage(){const rows=data.expenses||[];return `

Utlägg

Godkänn först. Registrera sedan projektkostnaden separat.

${rows.slice().reverse().map(x=>``).join('')||''}
Datum / personBeskrivningBeloppStatusÅtgärd
${esc(x.date)}
${esc(person(x.personId))}
${esc(x.description)}
${esc(project(x.projectId)?.name||'Ej projektrelaterat')}
${kr(x.amount)}${x.status==='pending'?'Väntar':x.status==='approved'?'Godkänt':'Nekat'}${x.projectBooked?' · Projektkostnad registrerad':''}
${x.status==='pending'?``:''}${x.status==='approved'&&!x.projectBooked?``:''}
Inga utlägg ännu
`} function viewExpenseReceipt(id){const x=(data.expenses||[]).find(x=>x.id===id);if(x?.receipt){const w=window.open('','_blank');if(!w)return alert('Tillåt popupfönster för att se kvittot');const url=x.receipt;w.document.write('Kvitto');w.document.close()}} function setExpenseStatus(id,status){if(role()!=='admin')return alert('Endast administratör kan godkänna utlägg');const x=(data.expenses||[]).find(x=>x.id===id);if(!x)return;x.status=status;save();render()} function bookExpense(id){if(role()!=='admin')return alert('Endast administratör kan lägga in projektkostnad');const x=(data.expenses||[]).find(x=>x.id===id);if(!x||x.status!=='approved'||x.projectBooked)return;const ps=data.projects.filter(p=>!p.quoteOnly);const answer=prompt('Ange projekt-ID för kostnaden:\n'+ps.map(p=>p.id+' – '+p.name).join('\n'),x.projectId||'');if(answer===null)return;const p=ps.find(p=>String(p.id)===answer.trim());if(!p)return alert('Välj ett giltigt projekt-ID');const category=prompt('Kostnadskategori','Utlägg personal');if(category===null)return;data.projectInvoices??=[];const invoiceId=Date.now();data.projectInvoices.push({id:invoiceId,projectId:p.id,supplier:'Utlägg – '+person(x.personId),amount:x.amount,category:category||'Utlägg personal',date:x.date,description:x.description,expenseId:x.id});x.projectBooked=true;x.bookedProjectId=p.id;x.bookedInvoiceId=invoiceId;save();render();showToast('Utlägget är registrerat som projektkostnad')} function drawingPage(){return `

Ritningar & mängdning

Ritningsverktyget är nu inbyggt i TIDLETT. Dina ritningsmappar och mätningar sparas lokalt i webbläsaren.

`} const tidlettDrawingHtml=decodeURIComponent(escape(atob('PCFkb2N0eXBlIGh0bWw+PGh0bWwgbGFuZz0ic3YiPjxoZWFkPjxtZXRhIGNoYXJzZXQ9InV0Zi04Ij48bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLGluaXRpYWwtc2NhbGU9MSI+PHRpdGxlPlRJRExFVFQg4oCTIFJpdG5pbmdhciAmIG3DpG5nZG5pbmc8L3RpdGxlPjxzdHlsZT4KOnJvb3R7LS1iZzojZjdmNWVmOy0tY2FyZDojZmZmOy0taW5rOiMyOTJjMmM7LS1nb2xkOiNiNzliNjU7LS1saW5lOiNlMWRkZDN9Kntib3gtc2l6aW5nOmJvcmRlci1ib3h9Ym9keXttYXJnaW46MDtiYWNrZ3JvdW5kOnZhcigtLWJnKTtmb250OjE0cHggc3lzdGVtLXVpLEFyaWFsO2NvbG9yOnZhcigtLWluayl9aGVhZGVye2JhY2tncm91bmQ6I2ZmZjtib3JkZXItYm90dG9tOjFweCBzb2xpZCB2YXIoLS1saW5lKTtwYWRkaW5nOjE0cHggMjJweDtkaXNwbGF5OmZsZXg7YWxpZ24taXRlbXM6Y2VudGVyO2p1c3RpZnktY29udGVudDpzcGFjZS1iZXR3ZWVuO2dhcDoxMHB4fWgxe2ZvbnQtc2l6ZToyMXB4O21hcmdpbjowfWgye2ZvbnQtc2l6ZToxNnB4O21hcmdpbjowIDAgMTJweH1tYWlue2Rpc3BsYXk6Z3JpZDtncmlkLXRlbXBsYXRlLWNvbHVtbnM6bWlubWF4KDAsMWZyKSAyNDVweDtnYXA6OXB4O3BhZGRpbmc6OXB4O21heC13aWR0aDpub25lO21hcmdpbjphdXRvfS5jYXJke2JhY2tncm91bmQ6I2ZmZjtib3JkZXI6MXB4IHNvbGlkIHZhcigtLWxpbmUpO2JvcmRlci1yYWRpdXM6MTBweDtwYWRkaW5nOjEwcHh9LnRvb2xze2Rpc3BsYXk6ZmxleDtnYXA6OHB4O2ZsZXgtd3JhcDp3cmFwO2FsaWduLWl0ZW1zOmNlbnRlcjttYXJnaW4tYm90dG9tOjEycHh9YnV0dG9uLC5idG57Ym9yZGVyOjFweCBzb2xpZCAjZDRjZGJkO2JhY2tncm91bmQ6I2ZmZjtwYWRkaW5nOjdweCA5cHg7Ym9yZGVyLXJhZGl1czo3cHg7Y3Vyc29yOnBvaW50ZXI7Zm9udDppbmhlcml0fWJ1dHRvbi5hY3RpdmUsLmdvbGR7YmFja2dyb3VuZDp2YXIoLS1nb2xkKTtjb2xvcjojZmZmO2JvcmRlci1jb2xvcjp2YXIoLS1nb2xkKX1idXR0b246ZGlzYWJsZWR7b3BhY2l0eTouNDU7Y3Vyc29yOmRlZmF1bHR9aW5wdXQsc2VsZWN0e3dpZHRoOjEwMCU7cGFkZGluZzo5cHg7Ym9yZGVyOjFweCBzb2xpZCAjZDRjZGJkO2JvcmRlci1yYWRpdXM6OHB4O2JhY2tncm91bmQ6d2hpdGU7Zm9udDppbmhlcml0fWxhYmVse2Rpc3BsYXk6YmxvY2s7Zm9udC1zaXplOjEycHg7Zm9udC13ZWlnaHQ6NjUwO21hcmdpbjoxMHB4IDAgNXB4fS5tdXRlZHtjb2xvcjojNzU3NTZmO2ZvbnQtc2l6ZToxMnB4O2xpbmUtaGVpZ2h0OjEuNX0uY2FudmFzV3JhcHtoZWlnaHQ6Y2FsYygxMDB2aCAtIDIzNXB4KTttaW4taGVpZ2h0OjUyMHB4O292ZXJmbG93OmF1dG87YmFja2dyb3VuZDojZTZlNmU0O2JvcmRlcjoxcHggc29saWQgdmFyKC0tbGluZSk7Ym9yZGVyLXJhZGl1czo5cHg7cG9zaXRpb246cmVsYXRpdmU7dGV4dC1hbGlnbjpjZW50ZXJ9LnNoZWV0e3Bvc2l0aW9uOnJlbGF0aXZlO2Rpc3BsYXk6aW5saW5lLWJsb2NrO21hcmdpbjoyMHB4O2JveC1zaGFkb3c6MCA0cHggMjJweCAjMDAwMjt2ZXJ0aWNhbC1hbGlnbjp0b3A7dG91Y2gtYWN0aW9uOm5vbmU7YmFja2dyb3VuZDojZmZmfWNhbnZhc3tkaXNwbGF5OmJsb2NrfXN2Z3twb3NpdGlvbjphYnNvbHV0ZTtpbnNldDowO3dpZHRoOjEwMCU7aGVpZ2h0OjEwMCU7b3ZlcmZsb3c6dmlzaWJsZTtjdXJzb3I6Y3Jvc3NoYWlyfS5ncm91cHtib3JkZXI6MXB4IHNvbGlkIHZhcigtLWxpbmUpO2JvcmRlci1yYWRpdXM6OXB4O3BhZGRpbmc6MTBweDttYXJnaW46OXB4IDB9LmJldHdlZW57ZGlzcGxheTpmbGV4O2p1c3RpZnktY29udGVudDpzcGFjZS1iZXR3ZWVuO2dhcDoxMHB4O2FsaWduLWl0ZW1zOmNlbnRlcn0ucm93e2Rpc3BsYXk6ZmxleDtnYXA6OHB4O2FsaWduLWl0ZW1zOmNlbnRlcn0ucm93Pip7bWluLXdpZHRoOjB9LnN1bXtmb250LXNpemU6MjJweDtmb250LXdlaWdodDo3NTB9LmxpbmV7Ym9yZGVyLXRvcDoxcHggc29saWQgdmFyKC0tbGluZSk7bWFyZ2luOjEycHggMH0ubWVhc3VyZXtwYWRkaW5nOjZweCAwO2Rpc3BsYXk6ZmxleDtqdXN0aWZ5LWNvbnRlbnQ6c3BhY2UtYmV0d2VlbjtnYXA6OHB4O2ZvbnQtc2l6ZToxMnB4fS5tZWFzdXJlIGJ1dHRvbntwYWRkaW5nOjNweCA3cHh9YXNpZGV7bWluLXdpZHRoOjB9LnRpbnl7Zm9udC1zaXplOjExcHh9LnN0YXR1c3tiYWNrZ3JvdW5kOiNmYWY3ZWM7cGFkZGluZzo5cHg7Ym9yZGVyLXJhZGl1czo4cHg7bWFyZ2luOjhweCAwO2xpbmUtaGVpZ2h0OjEuNX1ib2R5LmhpZGVBc2lkZSBtYWlue2dyaWQtdGVtcGxhdGUtY29sdW1uczptaW5tYXgoMCwxZnIpfWJvZHkuaGlkZUFzaWRlIGFzaWRle2Rpc3BsYXk6bm9uZX0uZWRpdEhhbmRsZXtjdXJzb3I6Z3JhYn0uZWRpdEhhbmRsZTphY3RpdmV7Y3Vyc29yOmdyYWJiaW5nfUBtZWRpYShtYXgtd2lkdGg6ODUwcHgpe21haW57Z3JpZC10ZW1wbGF0ZS1jb2x1bW5zOjFmcn0uY2FudmFzV3JhcHtoZWlnaHQ6NTV2aH1oZWFkZXJ7cGFkZGluZzoxMnB4fWFzaWRle29yZGVyOjJ9fQoKLyogdjk6IHR5ZGxpZ2EsIGtvbXBha3RhIGtsaWNreXRvciBvY2ggc3TDtnJyZSByaXRuaW5nc3l0YSAqLwo6cm9vdHstLWJ1dHRvbi15ZWxsb3c6I2Y0ZDY3ZTstLWJ1dHRvbi15ZWxsb3ctaG92ZXI6I2VhYzM1YzstLWJ1dHRvbi1ib3JkZXI6I2M1OWMzN30KYnV0dG9uLC5idG57YmFja2dyb3VuZDp2YXIoLS1idXR0b24teWVsbG93KTtib3JkZXI6MXB4IHNvbGlkIHZhcigtLWJ1dHRvbi1ib3JkZXIpO2NvbG9yOiMzNDJhMTI7Zm9udC13ZWlnaHQ6NjUwO3RyYW5zaXRpb246YmFja2dyb3VuZCAuMTJzIGVhc2U7cGFkZGluZzo2cHggOXB4fQpidXR0b246aG92ZXI6bm90KDpkaXNhYmxlZCksLmJ0bjpob3ZlcntiYWNrZ3JvdW5kOnZhcigtLWJ1dHRvbi15ZWxsb3ctaG92ZXIpfQpidXR0b24uYWN0aXZlLC5nb2xke2JhY2tncm91bmQ6I2Q3YWQ0Mztjb2xvcjojMmQyMzBkO2JvcmRlci1jb2xvcjojYWQ4MTI1fQpidXR0b24uYWN0aXZlOmhvdmVyLC5nb2xkOmhvdmVye2JhY2tncm91bmQ6I2M5OWMzMH0KYnV0dG9uOmZvY3VzLXZpc2libGUsLmJ0bjpmb2N1cy12aXNpYmxle291dGxpbmU6M3B4IHNvbGlkICM4YjY2MWU7b3V0bGluZS1vZmZzZXQ6MnB4fQphc2lkZSAuY2FyZHtwYWRkaW5nOjlweH1hc2lkZSBoMntmb250LXNpemU6MTRweDttYXJnaW4tYm90dG9tOjdweH1hc2lkZSBsYWJlbHtmb250LXNpemU6MTFweDttYXJnaW46N3B4IDAgM3B4fWFzaWRlIGJ1dHRvbixhc2lkZSAuYnRue3BhZGRpbmc6NXB4IDdweDtmb250LXNpemU6MTFweDtib3JkZXItcmFkaXVzOjZweH1hc2lkZSBpbnB1dCxhc2lkZSBzZWxlY3R7cGFkZGluZzo2cHg7Zm9udC1zaXplOjEycHh9YXNpZGUgLnJvd3tnYXA6NHB4fWFzaWRlIC5iZXR3ZWVue2dhcDo1cHg7ZmxleC13cmFwOndyYXB9YXNpZGUgLmdyb3Vwe3BhZGRpbmc6N3B4O21hcmdpbjo2cHggMH1hc2lkZSAuc3Vte2ZvbnQtc2l6ZToxN3B4fWFzaWRlIC5zdGF0dXN7cGFkZGluZzo2cHh9YXNpZGUgLm11dGVke2ZvbnQtc2l6ZToxMXB4fWFzaWRlIC5tZWFzdXJlIGJ1dHRvbntwYWRkaW5nOjNweCA1cHh9Lm1lYXN1cmV7ZmxleC13cmFwOndyYXA7YWxpZ24taXRlbXM6Y2VudGVyfS5tZWFzdXJlPnNwYW46Zmlyc3QtY2hpbGR7ZmxleDoxIDEgMTAwJX0KbWFpbntncmlkLXRlbXBsYXRlLWNvbHVtbnM6bWlubWF4KDAsMWZyKSAyMjBweH0udG9vbHN7Z2FwOjVweDttYXJnaW4tYm90dG9tOjdweH0udG9vbHMgYnV0dG9ue3BhZGRpbmc6NnB4IDhweH0uY2FudmFzV3JhcHtoZWlnaHQ6Y2FsYygxMDB2aCAtIDIwNXB4KTttaW4taGVpZ2h0OjU0MHB4fQouZXhwb3J0T3B0aW9uc3ttYXJnaW4tdG9wOjlweDtwYWRkaW5nOjhweDtiYWNrZ3JvdW5kOiNmYWY2ZTk7Ym9yZGVyOjFweCBzb2xpZCAjZTRkNmFkO2JvcmRlci1yYWRpdXM6OHB4fS5leHBvcnRPcHRpb25zIGgze2ZvbnQtc2l6ZToxMnB4O21hcmdpbjowIDAgNnB4fS5leHBvcnRPcHRpb25zIGxhYmVse2Rpc3BsYXk6ZmxleDthbGlnbi1pdGVtczpjZW50ZXI7Z2FwOjdweDtmb250LXNpemU6MTFweDtmb250LXdlaWdodDo1NTA7bWFyZ2luOjZweCAwO2N1cnNvcjpwb2ludGVyfS5leHBvcnRPcHRpb25zIGlucHV0e3dpZHRoOjE1cHg7aGVpZ2h0OjE1cHg7YWNjZW50LWNvbG9yOiNiZDhmMjc7ZmxleC1zaHJpbms6MH0KQG1lZGlhKG1heC13aWR0aDo4NTBweCl7bWFpbntncmlkLXRlbXBsYXRlLWNvbHVtbnM6MWZyfS5jYW52YXNXcmFwe2hlaWdodDo2MHZofX0KCi5saWJyYXJ5e2JhY2tncm91bmQ6I2ZmZjtib3JkZXI6MXB4IHNvbGlkIHZhcigtLWxpbmUpO2JvcmRlci1yYWRpdXM6MTBweDtwYWRkaW5nOjExcHg7bWFyZ2luOjAgOXB4IDNweH0ubGlicmFyeUhlYWR7ZGlzcGxheTpmbGV4O2dhcDo4cHg7YWxpZ24taXRlbXM6Y2VudGVyO2ZsZXgtd3JhcDp3cmFwO2p1c3RpZnktY29udGVudDpzcGFjZS1iZXR3ZWVufS5saWJyYXJ5SGVhZCBoMnttYXJnaW46MH0ubGlicmFyeUFjdGlvbnN7ZGlzcGxheTpmbGV4O2FsaWduLWl0ZW1zOmNlbnRlcjtnYXA6NnB4O2ZsZXgtd3JhcDp3cmFwfS5saWJyYXJ5QWN0aW9ucyBpbnB1dHt3aWR0aDoxNzVweDtwYWRkaW5nOjZweH0uZm9sZGVyQ2FyZHN7ZGlzcGxheTpmbGV4O2ZsZXgtd3JhcDp3cmFwO2dhcDo2cHg7bWFyZ2luLXRvcDo5cHh9LmZvbGRlckNhcmR7Ym9yZGVyLXJhZGl1czo3cHg7d2hpdGUtc3BhY2U6bm93cmFwfS5mb2xkZXJDYXJkLmN1cnJlbnR7YmFja2dyb3VuZDojZDdhZDQzfS5saWJyYXJ5RmlsZXN7ZGlzcGxheTpmbGV4O2dhcDo3cHg7ZmxleC13cmFwOndyYXA7bWFyZ2luLXRvcDo5cHh9LmZpbGVDYXJke2JhY2tncm91bmQ6I2ZiZmFmNTtib3JkZXI6MXB4IHNvbGlkICNkZGQ0YmQ7Ym9yZGVyLXJhZGl1czo4cHg7cGFkZGluZzo5cHg7bWluLXdpZHRoOjE5NXB4O21heC13aWR0aDozMDBweDtmbGV4OjF9LmZpbGVDYXJkIHN0cm9uZ3tkaXNwbGF5OmJsb2NrO292ZXJmbG93LXdyYXA6YW55d2hlcmV9LmZpbGVDYXJkIC5tdXRlZHttYXJnaW46NHB4IDB9LmZpbGVDYXJkIGJ1dHRvbnttYXJnaW46M3B4IDRweCAwIDB9LmxpYnJhcnlOb3Rle21hcmdpbjo3cHggMCAwfQoKLyogdjE2IOKAkyBwcm9mZXNzaW9uZWxsLCBsdWZ0aWcgZGVzaWduIHV0YW4gw6RuZHJpbmdhciBpIG3DpHQtIGVsbGVyIGxhZ3JpbmdzbG9naWsgKi8KOnJvb3R7LS1iZzojZjNmNWY3Oy0tY2FyZDojZmZmOy0taW5rOiMyMDJiMzY7LS1nb2xkOiNjYWE2NGU7LS1saW5lOiNlMmU3ZWM7LS1idXR0b24teWVsbG93OiNmNmRjOGY7LS1idXR0b24teWVsbG93LWhvdmVyOiNlZmNkNmM7LS1idXR0b24tYm9yZGVyOiNkNWI0NTl9CmJvZHl7Zm9udC1mYW1pbHk6SW50ZXIsdWktc2Fucy1zZXJpZixzeXN0ZW0tdWksLWFwcGxlLXN5c3RlbSwiU2Vnb2UgVUkiLHNhbnMtc2VyaWY7YmFja2dyb3VuZDp2YXIoLS1iZyk7Y29sb3I6dmFyKC0taW5rKTtmb250LXNpemU6MTNweH0KaGVhZGVye2JhY2tncm91bmQ6IzE3MmIzYTtjb2xvcjojZmZmO2JvcmRlci1ib3R0b206MDtwYWRkaW5nOjE3cHggMjRweDtib3gtc2hhZG93OjAgM3B4IDEycHggIzE1MjYzNTE4fQpoZWFkZXIgaDF7Zm9udC1zaXplOjIwcHg7Zm9udC13ZWlnaHQ6NzIwO2xldHRlci1zcGFjaW5nOi0uNDVweH0KaGVhZGVyIC5tdXRlZHtjb2xvcjojYmRjYmQ2fQpoZWFkZXI+ZGl2OmZpcnN0LWNoaWxkPi5tdXRlZHtmb250LXNpemU6MTBweDtmb250LXdlaWdodDo4MDA7bGV0dGVyLXNwYWNpbmc6Mi40cHg7Y29sb3I6I2YyZDI3YzttYXJnaW4tYm90dG9tOjNweH0KbWFpbntnYXA6MTJweDtwYWRkaW5nOjEycHg7Z3JpZC10ZW1wbGF0ZS1jb2x1bW5zOm1pbm1heCgwLDFmcikgMjM2cHh9Ci5jYXJkLC5saWJyYXJ5e2JvcmRlcjoxcHggc29saWQgI2UyZTdlYztib3JkZXItcmFkaXVzOjEycHg7Ym94LXNoYWRvdzowIDJweCAxMnB4ICMxNDJjM2IwOX0KLmNhcmR7cGFkZGluZzoxM3B4fS5saWJyYXJ5e21hcmdpbjoxMnB4IDEycHggMDtwYWRkaW5nOjEzcHggMTVweH0KaDJ7Zm9udC1zaXplOjE0cHg7Zm9udC13ZWlnaHQ6NzUwO2xldHRlci1zcGFjaW5nOi0uMnB4fQpidXR0b24sLmJ0bntiYWNrZ3JvdW5kOnZhcigtLWJ1dHRvbi15ZWxsb3cpO2NvbG9yOiMzOTJmMTc7Ym9yZGVyOjFweCBzb2xpZCB2YXIoLS1idXR0b24tYm9yZGVyKTtib3JkZXItcmFkaXVzOjdweDtmb250LXNpemU6MTJweDtmb250LXdlaWdodDo3MDA7cGFkZGluZzo3cHggMTBweDtib3gtc2hhZG93OjAgMXB4IDAgI2ZmZjcgaW5zZXQ7dHJhbnNpdGlvbjpiYWNrZ3JvdW5kIC4xMnMsYm9yZGVyLWNvbG9yIC4xMnMsdHJhbnNmb3JtIC4xMnN9CmJ1dHRvbjpob3Zlcjpub3QoOmRpc2FibGVkKSwuYnRuOmhvdmVye2JhY2tncm91bmQ6dmFyKC0tYnV0dG9uLXllbGxvdy1ob3Zlcik7Ym9yZGVyLWNvbG9yOiNiOTkyMzh9CmJ1dHRvbjphY3RpdmU6bm90KDpkaXNhYmxlZCl7dHJhbnNmb3JtOnRyYW5zbGF0ZVkoMXB4KX0KYnV0dG9uLmFjdGl2ZSwuZ29sZHtiYWNrZ3JvdW5kOiNkZmI4NGY7Y29sb3I6IzMwMjYwZjtib3JkZXItY29sb3I6I2MyOWEzN30KYnV0dG9uLmFjdGl2ZTpob3ZlciwuZ29sZDpob3ZlcntiYWNrZ3JvdW5kOiNkNWFiM2N9CmJ1dHRvbjpkaXNhYmxlZHtvcGFjaXR5Oi40NTtib3gtc2hhZG93Om5vbmV9CmlucHV0LHNlbGVjdHtib3JkZXI6MXB4IHNvbGlkICNkY2UzZTk7Ym9yZGVyLXJhZGl1czo3cHg7YmFja2dyb3VuZDojZmZmO2NvbG9yOiMyODM2NDI7bWluLWhlaWdodDozMnB4fQppbnB1dDpmb2N1cyxzZWxlY3Q6Zm9jdXN7b3V0bGluZToycHggc29saWQgI2Q3YjQ1NDc3O291dGxpbmUtb2Zmc2V0OjFweDtib3JkZXItY29sb3I6I2M0YTQ1Mn0KLnRvb2xze2dhcDo2cHg7bWFyZ2luLWJvdHRvbTo5cHg7cGFkZGluZy1ib3R0b206OHB4O2JvcmRlci1ib3R0b206MXB4IHNvbGlkICNlZWYwZjJ9Ci50b29sczpsYXN0LW9mLXR5cGV7Ym9yZGVyLWJvdHRvbTowfQoudG9vbHMgYnV0dG9ue3BhZGRpbmc6N3B4IDEwcHh9Ci50b29scyBsYWJlbHtjb2xvcjojNTI2MTZkO2ZvbnQtd2VpZ2h0OjY1MH0KLnN0YXR1c3tiYWNrZ3JvdW5kOiNmNWY4ZmE7Ym9yZGVyOjFweCBzb2xpZCAjZTVlYmVmO2NvbG9yOiM0MzUzNjI7cGFkZGluZzoxMHB4IDEycHg7Ym9yZGVyLXJhZGl1czo4cHg7Zm9udC1zaXplOjEycHh9Ci5jYW52YXNXcmFwe2JhY2tncm91bmQ6I2RjZTNlODtib3JkZXItY29sb3I6I2NiZDVkYztib3JkZXItcmFkaXVzOjlweDtoZWlnaHQ6Y2FsYygxMDB2aCAtIDIyNHB4KTttaW4taGVpZ2h0OjU0MHB4O2JveC1zaGFkb3c6aW5zZXQgMCAxcHggNXB4ICMyNDM2NDQxMn0KLnNoZWV0e2JveC1zaGFkb3c6MCA2cHggMjhweCAjMjYzNDQwMzN9Ci5saWJyYXJ5SGVhZHtnYXA6MTJweH0ubGlicmFyeUhlYWQgaDJ7Zm9udC1zaXplOjE1cHh9CiNzaG93TGlicmFyeXtmb250LXNpemU6MTRweDtwYWRkaW5nOjlweCAxNHB4O2JhY2tncm91bmQ6I2YwY2U3NTtib3JkZXItcmFkaXVzOjhweH0KI2xpYnJhcnlMb2NhdGlvbntmb250LXNpemU6MTJweDtjb2xvcjojNjA3MTdlfQoubGlicmFyeUFjdGlvbnN7Z2FwOjhweH0ubGlicmFyeUFjdGlvbnMgaW5wdXR7d2lkdGg6MjYwcHg7bWF4LXdpZHRoOjEwMCU7cGFkZGluZzo4cHggMTBweH0KLmZvbGRlckNhcmRze2dhcDo5cHg7bWFyZ2luLXRvcDoxM3B4fQouZm9sZGVyQ2FyZHtwYWRkaW5nOjEwcHggMTNweDtib3JkZXItcmFkaXVzOjlweDtiYWNrZ3JvdW5kOiNmZmY2ZGQ7Ym9yZGVyLWNvbG9yOiNlNGNhODQ7d2hpdGUtc3BhY2U6bm9ybWFsfQouZm9sZGVyQ2FyZC5jdXJyZW50e2JhY2tncm91bmQ6I2U1YmQ1NDtjb2xvcjojMmUyOTFifQoubGlicmFyeUZpbGVze2dhcDoxMXB4O21hcmdpbi10b3A6MTNweH0KLmZpbGVDYXJke2JhY2tncm91bmQ6I2ZmZjtib3JkZXI6MXB4IHNvbGlkICNlMWU2ZTk7Ym9yZGVyLXJhZGl1czoxMHB4O3BhZGRpbmc6MTNweDttaW4td2lkdGg6MjIwcHg7bWF4LXdpZHRoOjMzMHB4O2JveC1zaGFkb3c6MCAycHggOXB4ICMyMzM4NDkwYn0KLmZpbGVDYXJkIHN0cm9uZ3tmb250LXNpemU6MTNweDtjb2xvcjojMjYzODQ5O2xpbmUtaGVpZ2h0OjEuNDV9Ci5maWxlQ2FyZCAubXV0ZWR7bWFyZ2luOjdweCAwO2NvbG9yOiM3NDgxOGJ9Ci5maWxlQ2FyZCBidXR0b257bWFyZ2luOjVweCA1cHggMCAwfQoubGlicmFyeU5vdGV7bWFyZ2luLXRvcDoxMnB4O3BhZGRpbmctdG9wOjlweDtib3JkZXItdG9wOjFweCBzb2xpZCAjZWRmMGYyfQphc2lkZSAuY2FyZHtwYWRkaW5nOjEycHh9YXNpZGUgaDJ7Zm9udC1zaXplOjE0cHg7Y29sb3I6IzI2Mzc0N30KYXNpZGUgbGFiZWx7Y29sb3I6IzU0NjQ3Mztmb250LXdlaWdodDo3MDB9YXNpZGUgaW5wdXQsYXNpZGUgc2VsZWN0e2ZvbnQtc2l6ZToxMnB4fQphc2lkZSBidXR0b24sYXNpZGUgLmJ0bntmb250LXNpemU6MTFweDtwYWRkaW5nOjZweCA4cHh9CmFzaWRlIC5ncm91cHtiYWNrZ3JvdW5kOiNmY2ZkZmU7Ym9yZGVyOjFweCBzb2xpZCAjZTRlOWVjO2JvcmRlci1yYWRpdXM6OXB4O21hcmdpbjo5cHggMDtwYWRkaW5nOjlweH0KYXNpZGUgLnN1bXtmb250LXNpemU6MTlweDtmb250LXdlaWdodDo4MDA7Y29sb3I6IzI1M2I0OH0KLmV4cG9ydE9wdGlvbnN7YmFja2dyb3VuZDojZmZmYWYwO2JvcmRlci1jb2xvcjojZThkOWIxO3BhZGRpbmc6MTBweDtib3JkZXItcmFkaXVzOjlweH0KLmV4cG9ydE9wdGlvbnMgaDN7Zm9udC1zaXplOjEycHg7Y29sb3I6IzUzNDIxZn0KLmV4cG9ydE9wdGlvbnMgbGFiZWx7Zm9udC1zaXplOjExcHh9Ci5tdXRlZHtjb2xvcjojNzc4MzhkfQpAbWVkaWEobWF4LXdpZHRoOjg1MHB4KXttYWlue2dyaWQtdGVtcGxhdGUtY29sdW1uczoxZnJ9LmxpYnJhcnl7bWFyZ2luOjhweCA4cHggMH0uY2FudmFzV3JhcHtoZWlnaHQ6NjB2aDttaW4taGVpZ2h0OjQyMHB4fWhlYWRlcntwYWRkaW5nOjEzcHggMTVweH19CkBtZWRpYShtYXgtd2lkdGg6NTQwcHgpe2hlYWRlciBoMXtmb250LXNpemU6MTdweH0ubGlicmFyeUFjdGlvbnMgaW5wdXR7d2lkdGg6MTAwJX0uZmlsZUNhcmR7bWF4LXdpZHRoOm5vbmU7bWluLXdpZHRoOjEwMCV9fQovKiB2MTcg4oCTIGtvbXBha3Qgcml0bmluZ3NiaWJsaW90ZWsgKi8KLmxpYnJhcnl7cGFkZGluZzo3cHggMTBweDttYXJnaW4tYm90dG9tOjNweH0ubGlicmFyeUhlYWR7Z2FwOjdweH0ubGlicmFyeUhlYWQgaDJ7Zm9udC1zaXplOjEzcHh9LmxpYnJhcnkgI3Nob3dMaWJyYXJ5e2ZvbnQtc2l6ZToxMnB4O3BhZGRpbmc6NXB4IDEwcHh9LmxpYnJhcnkgLmJ0biwubGlicmFyeSBidXR0b257Zm9udC1zaXplOjExcHg7cGFkZGluZzo1cHggOHB4fS5saWJyYXJ5QWN0aW9uc3tnYXA6NXB4fS5saWJyYXJ5QWN0aW9ucyBpbnB1dHt3aWR0aDoyMTBweDtwYWRkaW5nOjVweCA4cHg7Zm9udC1zaXplOjEycHh9LmZpbGVDYXJke3BhZGRpbmc6OHB4O21pbi13aWR0aDoxNjVweDttYXgtd2lkdGg6MjM1cHg7Ym9yZGVyLXJhZGl1czo4cHh9LmZpbGVDYXJkIHN0cm9uZ3tmb250LXNpemU6MTJweH0uZmlsZUNhcmQgLm11dGVke2ZvbnQtc2l6ZToxMHB4O21hcmdpbjozcHggMH0uZm9sZGVyQ2FyZHMsLmxpYnJhcnlGaWxlc3ttYXJnaW4tdG9wOjVweDtnYXA6NXB4fS5saWJyYXJ5Tm90ZXtmb250LXNpemU6MTBweDttYXJnaW46NXB4IDAgMH0uc3VtbWFyeVRvb2xze2Rpc3BsYXk6ZmxleDthbGlnbi1pdGVtczpjZW50ZXI7Z2FwOjZweDtmbGV4LXdyYXA6d3JhcDttYXJnaW4tdG9wOjlweDtwYWRkaW5nOjhweDtiYWNrZ3JvdW5kOiNmNmY4ZmE7Ym9yZGVyOjFweCBzb2xpZCAjZTBlNWU5O2JvcmRlci1yYWRpdXM6OHB4fS5zdW1tYXJ5VG9vbHMgc2VsZWN0e3dpZHRoOmF1dG87bWluLXdpZHRoOjE1MHB4fS5zdW1tYXJ5VG9vbHMgLm11dGVke2ZsZXgtYmFzaXM6MTAwJX0ubGlua2VkTGlzdHtmb250LXNpemU6MTFweDtsaW5lLWhlaWdodDoxLjZ9LmxpbmtlZExpc3QgYnV0dG9ue2ZvbnQtc2l6ZToxMHB4O3BhZGRpbmc6M3B4IDZweH0KLnNuYXBUb2dnbGV7ZGlzcGxheTppbmxpbmUtZmxleDthbGlnbi1pdGVtczpjZW50ZXI7Z2FwOjVweDttYXJnaW46MCA0cHg7cGFkZGluZzo1cHggOXB4O2JhY2tncm91bmQ6I2Y4ZThiNztib3JkZXI6MXB4IHNvbGlkICNkOWJiNjQ7Ym9yZGVyLXJhZGl1czo3cHg7d2hpdGUtc3BhY2U6bm93cmFwO2ZvbnQtc2l6ZToxMnB4O2ZvbnQtd2VpZ2h0OjY1MH0uc25hcFRvZ2dsZSBpbnB1dHt3aWR0aDoxNXB4O2hlaWdodDoxNXB4O2FjY2VudC1jb2xvcjojYjU4YTFlfSA8L3N0eWxlPjwvaGVhZD48Ym9keT48aGVhZGVyPjxkaXY+PGRpdiBjbGFzcz0ibXV0ZWQiPlRJRExFVFQ8L2Rpdj48aDE+Uml0bmluZ2FyICYgbcOkbmdkbmluZzwvaDE+PC9kaXY+PGRpdiBjbGFzcz0ibXV0ZWQiPkludGVncmVyYWQgaSBUSURMRVRUIMK3IFYyNTwvZGl2PjwvaGVhZGVyPjxzZWN0aW9uIGNsYXNzPSJsaWJyYXJ5IiBpZD0ibGlicmFyeSI+PGRpdiBjbGFzcz0ibGlicmFyeUhlYWQiPjxidXR0b24gaWQ9InNob3dMaWJyYXJ5IiBjbGFzcz0iZ29sZCI+8J+TgSBNaW5hIHJpdG5pbmdhciDilr48L2J1dHRvbj48c3BhbiBjbGFzcz0ibXV0ZWQiIGlkPSJsaWJyYXJ5TG9jYXRpb24iPlZpc2EgZGluYSByaXRuaW5nc3Byb2pla3Q8L3NwYW4+PC9kaXY+PGRpdiBpZD0ibGlicmFyeUNvbnRlbnQiIGhpZGRlbj48ZGl2IGNsYXNzPSJsaWJyYXJ5SGVhZCIgc3R5bGU9Im1hcmdpbi10b3A6MTBweCI+PGgyIGlkPSJsaWJyYXJ5VGl0bGUiPlJpdG5pbmdzcHJvamVrdDwvaDI+PGJ1dHRvbiBpZD0iYmFja1RvRm9sZGVycyIgaGlkZGVuPuKGkCBBbGxhIHJpdG5pbmdzcHJvamVrdDwvYnV0dG9uPjwvZGl2PjxkaXYgaWQ9ImZvbGRlclZpZXciPjxkaXYgY2xhc3M9ImxpYnJhcnlBY3Rpb25zIiBzdHlsZT0ibWFyZ2luLXRvcDo5cHgiPjxpbnB1dCBpZD0ibmV3Rm9sZGVyTmFtZSIgcGxhY2Vob2xkZXI9Ik55dHQgcml0bmluZ3Nwcm9qZWt0LCB0LmV4LiBWZWdhc3RhZGVuIj48YnV0dG9uIGlkPSJjcmVhdGVGb2xkZXIiPisgU2thcGEgcml0bmluZ3Nwcm9qZWt0PC9idXR0b24+PC9kaXY+PGRpdiBjbGFzcz0iZm9sZGVyQ2FyZHMiIGlkPSJmb2xkZXJDYXJkcyI+PC9kaXY+PC9kaXY+PGRpdiBpZD0iZHJhd2luZ1ZpZXciIGhpZGRlbj48ZGl2IGNsYXNzPSJsaWJyYXJ5QWN0aW9ucyIgc3R5bGU9Im1hcmdpbi10b3A6MTBweCI+PGxhYmVsIGNsYXNzPSJidG4iIHN0eWxlPSJtYXJnaW46MCI+KyBMw6RnZyB0aWxsIHJpdG5pbmc8aW5wdXQgaWQ9ImxpYnJhcnlQZGYiIHR5cGU9ImZpbGUiIGFjY2VwdD0iYXBwbGljYXRpb24vcGRmLC5wZGYiIHN0eWxlPSJkaXNwbGF5Om5vbmUiPjwvbGFiZWw+PGJ1dHRvbiBpZD0iZGVsZXRlRm9sZGVyIj5UYSBib3J0IHJpdG5pbmdzcHJvamVrdDwvYnV0dG9uPjwvZGl2PjxkaXYgY2xhc3M9ImxpYnJhcnlGaWxlcyIgaWQ9ImxpYnJhcnlGaWxlcyI+PC9kaXY+PGRpdiBjbGFzcz0ic3VtbWFyeVRvb2xzIj48YnV0dG9uIGlkPSJmb2xkZXJTdW1tYXJ5IiBjbGFzcz0iZ29sZCI+4oaTIFNhbW1hbnN0w6RsbCBoZWxhIG1hcHBlbiAoUERGKTwvYnV0dG9uPjxzcGFuIGNsYXNzPSJtdXRlZCI+QWxsYSBzcGFyYWRlIG3DpHRuaW5nYXIgaSBtYXBwZW5zIHJpdG5pbmdhciBzdW1tZXJhcywgaW5rbHVzaXZlIMOXIEfDpW5nZXIuPC9zcGFuPjwvZGl2PjwvZGl2PjxwIGNsYXNzPSJtdXRlZCBsaWJyYXJ5Tm90ZSI+Uml0bmluZ2FyIG9jaCBtw6R0bmluZ2FyIHNwYXJhcyBsb2thbHQgaSBkZW4gaMOkciB3ZWJibMOkc2FyZW4sIGludGUgaSBtb2xuZXQuIFNwYXJhIGfDpHJuYSBhcmJldHNmaWwgc29tIHPDpGtlcmhldHNrb3BpYS48L3A+PC9kaXY+PC9zZWN0aW9uPjxtYWluPjxzZWN0aW9uIGNsYXNzPSJjYXJkIj48ZGl2IGNsYXNzPSJ0b29scyI+PGxhYmVsIHN0eWxlPSJtYXJnaW46MCI+TMOkZ2cgdGlsbCBQREYgaSBha3R1ZWxsdCByaXRuaW5nc3Byb2pla3QgPGlucHV0IGlkPSJwZGYiIHR5cGU9ImZpbGUiIGFjY2VwdD0iYXBwbGljYXRpb24vcGRmLC5wZGYiPjwvbGFiZWw+PGJ1dHRvbiBpZD0icHJldiI+4peAPC9idXR0b24+PHNwYW4gaWQ9InBhZ2VMYWJlbCI+U2lkYSDigJM8L3NwYW4+PGJ1dHRvbiBpZD0ibmV4dCI+4pa2PC9idXR0b24+PGJ1dHRvbiBpZD0iem9vbU91dCI+4oiSPC9idXR0b24+PHNwYW4gaWQ9Inpvb21MYWJlbCI+MTAwICU8L3NwYW4+PGJ1dHRvbiBpZD0iem9vbUluIj4rPC9idXR0b24+PGJ1dHRvbiBpZD0idG9nZ2xlQXNpZGUiPkTDtmxqIHNpZG9tZW55PC9idXR0b24+PC9kaXY+PGRpdiBjbGFzcz0idG9vbHMiPjxidXR0b24gZGF0YS1tb2RlPSJwYW4iIGNsYXNzPSJhY3RpdmUiPkZseXR0YSAvIHZpc2E8L2J1dHRvbj48YnV0dG9uIGRhdGEtbW9kZT0iZWRpdCI+UmVkaWdlcmEgbcOldHQ8L2J1dHRvbj48YnV0dG9uIGlkPSJzYXZlRWRpdCIgY2xhc3M9ImdvbGQiIGhpZGRlbj7inJMgU3BhcmEgw6RuZHJpbmc8L2J1dHRvbj48YnV0dG9uIGlkPSJjYW5jZWxFZGl0IiBoaWRkZW4+QXZicnl0IMOkbmRyaW5nPC9idXR0b24+PGJ1dHRvbiBkYXRhLW1vZGU9ImNhbGlicmF0ZSI+S2FsaWJyZXJhPC9idXR0b24+PGJ1dHRvbiBkYXRhLW1vZGU9Imxlbmd0aCI+TcOkdCBsw6RuZ2Q8L2J1dHRvbj48YnV0dG9uIGRhdGEtbW9kZT0iYXJlYSI+TcOkdCBhcmVhPC9idXR0b24+PGJ1dHRvbiBkYXRhLW1vZGU9ImF1dG9hcmVhIj7inKYgQXV0b21hdGlzayB5dGEgKHRlc3QpPC9idXR0b24+PGJ1dHRvbiBkYXRhLW1vZGU9ImZvbGxvdyI+4oadIEbDtmxqIGxpbmplICh0ZXN0KTwvYnV0dG9uPjxidXR0b24gaWQ9ImFwcHJvdmVGb2xsb3ciIGNsYXNzPSJnb2xkIiBoaWRkZW4+4pyTIFNwYXJhIGbDtmxqZCBsaW5qZTwvYnV0dG9uPjxidXR0b24gaWQ9ImNhbmNlbEZvbGxvdyIgaGlkZGVuPkF2YnJ5dCBsaW5qZTwvYnV0dG9uPjxidXR0b24gaWQ9ImFwcHJvdmVBdXRvIiBjbGFzcz0iZ29sZCIgaGlkZGVuPuKckyBHb2Rrw6RubiB5dGE8L2J1dHRvbj48YnV0dG9uIGlkPSJjYW5jZWxBdXRvIiBoaWRkZW4+QXZicnl0IHl0YTwvYnV0dG9uPjxsYWJlbCBjbGFzcz0ic25hcFRvZ2dsZSIgdGl0bGU9IkbDpHN0IG3DpHRwdW5rdGVyIHDDpSBtw7Zya2Egcml0bmluZ3NsaW5qZXIiPjxpbnB1dCBpZD0ic25hcEVuYWJsZWQiIHR5cGU9ImNoZWNrYm94IiBjaGVja2VkPiBTbsOkcHAgbW90IGxpbmplPC9sYWJlbD48YnV0dG9uIGlkPSJ1bmRvIj7ihrYgw4VuZ3JhPC9idXR0b24+PGJ1dHRvbiBpZD0icmVkbyI+4oa3IEfDtnIgb208L2J1dHRvbj48YnV0dG9uIGlkPSJmaW5pc2giIGNsYXNzPSJnb2xkIj7inJMgU3BhcmEgLyBsw6VzIG3DpHRuaW5nPC9idXR0b24+PC9kaXY+PGRpdiBjbGFzcz0ic3RhdHVzIiBpZD0ic3RhdHVzIj5Ww6RsaiBlbiBQREYtcml0bmluZyBmw7ZyIGF0dCBiw7ZyamEuIEthbGlicmVyYSB2YXJqZSBzaWRhIG1vdCBldHQga8OkbnQgbcOldHQgaW5uYW4gZHUgbcOkdGVyLjwvZGl2PjxkaXYgY2xhc3M9ImNhbnZhc1dyYXAiIGlkPSJzY3JvbGwiPjxkaXYgY2xhc3M9InNoZWV0IiBpZD0ic2hlZXQiPjxjYW52YXMgaWQ9ImNhbnZhcyI+PC9jYW52YXM+PHN2ZyBpZD0ib3ZlcmxheSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48L3N2Zz48L2Rpdj48L2Rpdj48cCBjbGFzcz0ibXV0ZWQiPlbDpGxqIFJlZGlnZXJhIG3DpXR0IGbDtnIgYXR0IGZseXR0YSBwdW5rdGVyIHDDpSBzcGFyYWRlIGxpbmplciBvY2ggeXRvci4gS2FsaWJyZXJhIG1lZCB0dsOlIHB1bmt0ZXIgb2NoIHRyeWNrIEdvZGvDpG5uIGthbGlicmVyaW5nLiBNw6R0IHNlZGFuIGRpcmVrdCBww6Ugcml0bmluZ2VuOiBtw6V0dGVuIHZpc2FzIHDDpSBsaW5qZXJuYSBvY2ggeXRvcm5hLiBGw7ZyIGzDpG5nZDoga2xpY2thIHR2w6UgcHVua3RlciBzw6UgbMOlc2VzIG9jaCBzcGFyYXMgc3Ryw6Rja2FuIGF1dG9tYXRpc2t0LiBOw6RzdGEga2xpY2sgYsO2cmphciBlbiBueSBzdHLDpGNrYS4gRsO2ciBhcmVhOiBrbGlja2EgcnVudCB5dGFuIG9jaCB0cnljayBTcGFyYSAvIGzDpXMgbcOkdG5pbmcuIER1YmJlbGtsaWNrYSBww6UgZW4gZ3J1cHAgZsO2ciBhdHQgYWt0aXZlcmEgZGVuLiDDhW5ncmEvR8O2ciBvbSBmdW5nZXJhciBmw7ZyIHNwYXJhZGUgbcOldHQuIEFyZWFuIHLDpGtuYXMgYXV0b21hdGlza3QgbWVkYW4gZHUgbWFya2VyYXIuIER1YmJlbGtsaWNrIGJlaMO2dnMgaW50ZS4gRXNjIGF2YnJ5dGVyIHDDpWfDpWVuZGUgbcOkdG5pbmcuIFJpdG5pbmdlbnMgc2thbGEgbcOlc3RlIGthbGlicmVyYXMgbW90IGV0dCBrw6RudCBtw6V0dC4gTcOkdG5pbmdhciDDpHIgdW5nZWbDpHJsaWdhIG9jaCBza2Ega29udHJvbGxlcmFzIG1vdCByaXRuaW5nZW5zIG3DpXR0c8OkdHRuaW5nLjwvcD48L3NlY3Rpb24+PGFzaWRlPjxzZWN0aW9uIGNsYXNzPSJjYXJkIj48aDI+U2thbGEgJiBwcm9qZWt0PC9oMj48bGFiZWw+UHJvamVrdCAodmFsZnJpdHQpPC9sYWJlbD48c2VsZWN0IGlkPSJwcm9qZWN0Ij48b3B0aW9uIHZhbHVlPSIiPkVqIGtvcHBsYWQgdGlsbCBwcm9qZWt0PC9vcHRpb24+PC9zZWxlY3Q+PGxhYmVsPlZlcmtsaWcgbMOkbmdkIOKAkyBmeWxsIGkgZWZ0ZXIgYXR0IGR1IG1hcmtlcmF0IHR2w6UgcHVua3RlcjwvbGFiZWw+PGRpdiBjbGFzcz0icm93Ij48aW5wdXQgaWQ9Imtub3duIiB0eXBlPSJudW1iZXIiIG1pbj0iMC4wMDEiIHN0ZXA9ImFueSIgcGxhY2Vob2xkZXI9IkFuZ2UgbcOldHQgZWZ0ZXIgbWFya2VyaW5nIiB2YWx1ZT0iIj48c2VsZWN0IGlkPSJrbm93blVuaXQiIHN0eWxlPSJ3aWR0aDo3NnB4O2ZsZXgtc2hyaW5rOjAiPjxvcHRpb24gdmFsdWU9Im0iPm08L29wdGlvbj48b3B0aW9uIHZhbHVlPSJjbSI+Y208L29wdGlvbj48b3B0aW9uIHZhbHVlPSJtbSI+bW08L29wdGlvbj48L3NlbGVjdD48L2Rpdj48bGFiZWw+VmlzYSBsw6RuZ2Rtw6V0dCBpPC9sYWJlbD48c2VsZWN0IGlkPSJkaXNwbGF5VW5pdCI+PG9wdGlvbiB2YWx1ZT0ibSI+TWV0ZXIgKG0pPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iY20iPkNlbnRpbWV0ZXIgKGNtKTwvb3B0aW9uPjxvcHRpb24gdmFsdWU9Im1tIj5NaWxsaW1ldGVyIChtbSk8L29wdGlvbj48L3NlbGVjdD48cCBjbGFzcz0ibXV0ZWQiPsOEbmRyYXIgYmFyYSB2aXNuaW5nZW4uIFNwYXJhZGUgbcOldHQgYmVow6VsbGVyIHNpbiBub2dncmFubmhldCBpIG1ldGVyLiBBcmVvciB2aXNhcyBpIG3Csi48L3A+PGxhYmVsIHN0eWxlPSJkaXNwbGF5OmZsZXg7YWxpZ24taXRlbXM6Y2VudGVyO2dhcDo3cHg7bWFyZ2luOjlweCAwIj48aW5wdXQgdHlwZT0iY2hlY2tib3giIGlkPSJzbmFwQ2FsaWJyYXRpb24iPiBBbnbDpG5kIHNuw6RwcCB2aWQga2FsaWJyZXJpbmcgKGF2IHNvbSBzdGFuZGFyZCk8L2xhYmVsPjxidXR0b24gaWQ9ImNhbGlicmF0ZUJ0biIgc3R5bGU9Im1hcmdpbi10b3A6OHB4O3dpZHRoOjEwMCUiPjEuIE1hcmtlcmEgdHbDpSBwdW5rdGVyPC9idXR0b24+PGJ1dHRvbiBpZD0iYXBwcm92ZUNhbCIgY2xhc3M9ImdvbGQiIHN0eWxlPSJtYXJnaW4tdG9wOjhweDt3aWR0aDoxMDAlIiBkaXNhYmxlZD4zLiBHb2Rrw6RubiBrYWxpYnJlcmluZzwvYnV0dG9uPjxkaXYgaWQ9InBlbmRpbmdUZXh0IiBjbGFzcz0ibXV0ZWQiIHN0eWxlPSJtYXJnaW4tdG9wOjhweCI+TWFya2VyYSBmw7Zyc3QgdHbDpSBwdW5rdGVyIHDDpSByaXRuaW5nZW4sIHNrcml2IHNlZGFuIG3DpXR0ZXQuPC9kaXY+PGRpdiBpZD0ic2NhbGVUZXh0IiBjbGFzcz0ic3RhdHVzIj5JbmdlbiBrYWxpYnJlcmluZzwvZGl2PjxkaXYgaWQ9ImF4aXNDaGVjayIgY2xhc3M9Im11dGVkIiBzdHlsZT0ibWFyZ2luLXRvcDo3cHgiPktvbnRyb2xsZXJhIHNrYWxhbiBtZWQgYsOlZGUgZXR0IHbDpWdyw6R0dCBvY2ggZXR0IGxvZHLDpHR0IG3DpXR0LjwvZGl2PjxsYWJlbD5Ba3RpdiBtw6R0Z3J1cHA8L2xhYmVsPjxzZWxlY3QgaWQ9Imdyb3VwIj48L3NlbGVjdD48ZGl2IGNsYXNzPSJyb3ciPjxpbnB1dCBpZD0iZ3JvdXBOYW1lIiBwbGFjZWhvbGRlcj0iVC5leC4gVXRzaWRhIGJhbGsiPjxidXR0b24gaWQ9ImFkZEdyb3VwIiBjbGFzcz0iZ29sZCI+KyBHcnVwcDwvYnV0dG9uPjwvZGl2PjxwIGNsYXNzPSJtdXRlZCI+VmFyamUgc2lkYSBrYW4gaGEgZWdlbiBrYWxpYnJlcmluZy4gTMOkbmdkZXIgb2NoIGFyZW9yIHN1bW1lcmFzIHNlcGFyYXQuPC9wPjwvc2VjdGlvbj48c2VjdGlvbiBjbGFzcz0iY2FyZCIgc3R5bGU9Im1hcmdpbi10b3A6MTJweCI+PGRpdiBjbGFzcz0iYmV0d2VlbiI+PGgyPk3DpHRuaW5nYXI8L2gyPjxidXR0b24gaWQ9ImV4cG9ydFBkZiIgY2xhc3M9ImdvbGQiPuKGkyBFeHBvcnRlcmEgUERGPC9idXR0b24+PGJ1dHRvbiBpZD0iZXhwb3J0Ij5TcGFyYSBhcmJldHNmaWw8L2J1dHRvbj48bGFiZWwgY2xhc3M9ImJ0biIgc3R5bGU9Im1hcmdpbjowIj7DlnBwbmEgYXJiZXRzZmlsPGlucHV0IHR5cGU9ImZpbGUiIGlkPSJpbXBvcnRKc29uIiBhY2NlcHQ9Ii5qc29uLGFwcGxpY2F0aW9uL2pzb24iIHN0eWxlPSJkaXNwbGF5Om5vbmUiPjwvbGFiZWw+PC9kaXY+PGRpdiBjbGFzcz0ic3VtbWFyeVRvb2xzIiBzdHlsZT0ibWFyZ2luLWJvdHRvbTo5cHgiPjxidXR0b24gaWQ9InN1bW1hcnlQZGYiIGNsYXNzPSJnb2xkIj7ihpMgRW5kYXN0IHNhbW1hbnN0w6RsbG5pbmc8L2J1dHRvbj48YnV0dG9uIGlkPSJpbXBvcnRUb1Byb2plY3QiPkltcG9ydGVyYSB0aWxsIHByb2pla3Q8L2J1dHRvbj48c3BhbiBjbGFzcz0ibXV0ZWQiPlNrYXBhIGVuIHNlcGFyYXQgc2FtbWFuc3TDpGxsbmluZ3MtUERGIG9jaCBrb3BwbGEgZGVuIHNlbmFyZSB0aWxsIGV0dCBwcm9qZWt0Ljwvc3Bhbj48L2Rpdj48ZGl2IGlkPSJwcm9qZWN0SW1wb3J0UGFuZWwiIGhpZGRlbiBjbGFzcz0iZXhwb3J0T3B0aW9ucyI+PGgzPkltcG9ydGVyYSBzYW1tYW5zdMOkbGxuaW5nIHRpbGwgcHJvamVrdDwvaDM+PGxhYmVsPlbDpGxqIHByb2pla3Q8L2xhYmVsPjxzZWxlY3QgaWQ9InRhcmdldFByb2plY3QiPjxvcHRpb24gdmFsdWU9IiI+VsOkbGogcHJvamVrdC4uLjwvb3B0aW9uPjwvc2VsZWN0PjxidXR0b24gaWQ9ImNvbmZpcm1JbXBvcnQiIGNsYXNzPSJnb2xkIiBzdHlsZT0ibWFyZ2luLXRvcDo3cHgiPlNwYXJhIGkgcHJvamVrdGV0cyB1bmRlcmxhZzwvYnV0dG9uPjxwIGNsYXNzPSJtdXRlZCI+VW5kZXJsYWdldCBzcGFyYXMgbG9rYWx0IGkgZGVuIGjDpHIgcHJvdG90eXBlbi4gS29wcGxpbmcgdGlsbCBPZmZlcnQgaSBodXZ1ZGFwcGVuIGtyw6R2ZXIgZW4gZ2VtZW5zYW0gZGF0YWJhcy9pbnRlZ3JhdGlvbi48L3A+PC9kaXY+PGRpdiBpZD0ibGlua2VkU3VtbWFyaWVzIiBjbGFzcz0ibGlua2VkTGlzdCI+PC9kaXY+PGRldGFpbHMgY2xhc3M9ImV4cG9ydE9wdGlvbnMiPjxzdW1tYXJ5IHN0eWxlPSJjdXJzb3I6cG9pbnRlcjtmb250LXdlaWdodDo3NTAiPuKamSBQREYtaW5zdMOkbGxuaW5nYXI8L3N1bW1hcnk+PGRpdiBzdHlsZT0icGFkZGluZy10b3A6OHB4Ij48aDM+VmFkIHNrYSBmw7ZsamEgbWVkIGkgUERGPzwvaDM+PGxhYmVsPjxpbnB1dCB0eXBlPSJjaGVja2JveCIgaWQ9InBkZkZpbGwiIGNoZWNrZWQ+RsOkcmdmeWxsZGEgeXRvciAobcKyKTwvbGFiZWw+PGxhYmVsPjxpbnB1dCB0eXBlPSJjaGVja2JveCIgaWQ9InBkZkxpbmVzIiBjaGVja2VkPkbDpHJnYWRlIG3DpHRsaW5qZXIgb2NoIGtvbnR1cmVyPC9sYWJlbD48bGFiZWw+PGlucHV0IHR5cGU9ImNoZWNrYm94IiBpZD0icGRmTGFiZWxzIiBjaGVja2VkPk3DpXR0ZXRpa2V0dGVyIHDDpSByaXRuaW5nZW48L2xhYmVsPjxsYWJlbD48aW5wdXQgdHlwZT0iY2hlY2tib3giIGlkPSJwZGZTdW1tYXJ5IiBjaGVja2VkPk3DpG5nZHNhbW1hbnN0w6RsbG5pbmc8L2xhYmVsPjwvZGl2PjwvZGV0YWlscz48ZGl2IGlkPSJncm91cHMiPjwvZGl2PjxkaXYgY2xhc3M9ImxpbmUiPjwvZGl2PjxkaXYgY2xhc3M9ImJldHdlZW4iPjxiPlRvdGFsdCBsw6RuZ2Q8L2I+PHNwYW4gY2xhc3M9InN1bSIgaWQ9InRvdGFsTGVuZ3RoIj4wIG08L3NwYW4+PC9kaXY+PGRpdiBjbGFzcz0iYmV0d2VlbiI+PGI+VG90YWx0IGFyZWE8L2I+PHNwYW4gY2xhc3M9InN1bSIgaWQ9InRvdGFsQXJlYSI+MCBtwrI8L3NwYW4+PC9kaXY+PHAgY2xhc3M9Im11dGVkIj5EdWJiZWxrbGlja2EgcMOlIGVuIGdydXBwIGbDtnIgYXR0IHbDpGxqYSBkZW4uIEFuZ2UgR8OlbmdlciAow5cpIGbDtnIgZmxlcmEgbGlrYWRhbmEgZGVsYXIuIFRvdGFsc3VtbW9yIG9jaCBQREYgcsOka25hcyBhdXRvbWF0aXNrdCBvbS4gRmx5dHRhIG3DpXR0IHRpbGwgZW4gYW5uYW4gZ3J1cHAgbWVkIGxpc3RhbiB1bmRlciBtw6V0dGV0LiBQREYtZXhwb3J0ZW4gaW5uZWjDpWxsZXIgcml0bmluZ3Ntw6V0dCBvY2ggbcOkbmdkc2FtbWFuc3TDpGxsbmluZy48L3A+PHAgY2xhc3M9Im11dGVkIj5Nw6R0bmluZ2FybmEgc3BhcmFzIGF1dG9tYXRpc2t0IGbDtnIgZGVubmEgUERGIGkgd2ViYmzDpHNhcmVuLiDDlnBwbmEgc2FtbWEgUERGIGlnZW4gZsO2ciBhdHQgZm9ydHPDpHR0YS4gRXhwb3J0ZXJhIEpTT04gc29tIGV4dHJhIHPDpGtlcmhldHNrb3BpYS48L3A+PC9zZWN0aW9uPjwvYXNpZGU+PC9tYWluPgo8c2NyaXB0IHNyYz0iaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9hamF4L2xpYnMvcGRmLmpzLzMuMTEuMTc0L3BkZi5taW4uanMiPjwvc2NyaXB0PjxzY3JpcHQgc3JjPSJodHRwczovL2NkbmpzLmNsb3VkZmxhcmUuY29tL2FqYXgvbGlicy9wZGYtbGliLzEuMTcuMS9wZGYtbGliLm1pbi5qcyI+PC9zY3JpcHQ+PHNjcmlwdD4KKCgpPT57J3VzZSBzdHJpY3QnO2NvbnN0ICQ9aWQ9PmRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGlkKSxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnO2xldCBzZWxlY3RlZElkPW51bGwsZWRpdFBvaW50cz1udWxsLGRyYWdJbmRleD0tMSxzdXBwcmVzc0NsaWNrPWZhbHNlO2xldCBhdXRvUHJldmlldz1mYWxzZTtsZXQgcGVuZGluZ0NhbD1udWxsLG9yaWdpbmFsUGRmRmlsZT1udWxsLHVuZG9TdGFjaz1bXSxyZWRvU3RhY2s9W107bGV0IGFjdGl2ZUZvbGRlcklkPW51bGwsYWN0aXZlRHJhd2luZ0lkPW51bGwsbGlicmFyeU9wZW49ZmFsc2UsZm9sZGVyVmlld09wZW49ZmFsc2U7bGV0IHBkZj1udWxsLHBkZktleT0nJyxkb2M9bnVsbCxwYWdlPTEsem9vbT0xLG1vZGU9J3BhbicscG9pbnRzPVtdLHNjYWxlQmFzZT17dzoxLGg6MX0sY2FsPXt9LGdyb3Vwcz1bXSxtZWFzdXJlbWVudHM9W10sZ3JvdXBJZD0nJyxyZW5kZXJpbmc9ZmFsc2U7Y29uc3QgY29sb3JzPVsnIzAwOGQ4MycsJyNjMjc4MjUnLCcjNTg3OGMyJywnIzljNjJhYScsJyNjYzU1NjUnLCcjNjY3ZjMzJ107Y29uc3QgZm10PShuLGQ9Mik9Pk51bWJlcihufHwwKS50b0xvY2FsZVN0cmluZygnc3YtU0UnLHttYXhpbXVtRnJhY3Rpb25EaWdpdHM6ZH0pO2NvbnN0IHNhdmU9KCk9PntpZighcGRmS2V5KXJldHVybjt0cnl7bG9jYWxTdG9yYWdlLnNldEl0ZW0oJ3RpZGxldHQtbWVhc3VyZS0nK3BkZktleSxKU09OLnN0cmluZ2lmeSh7Y2FsLGdyb3VwcyxtZWFzdXJlbWVudHMscHJvamVjdElkOiQoJ3Byb2plY3QnKS52YWx1ZX0pKTtpZihhY3RpdmVEcmF3aW5nSWQpdG91Y2hEcmF3aW5nKGFjdGl2ZURyYXdpbmdJZCl9Y2F0Y2goZSl7YWxlcnQoJ0t1bmRlIGludGUgc3BhcmEgbG9rYWx0LiBFeHBvcnRlcmEgbcOkdG5pbmdhcm5hIHNvbSBKU09OLicpfX07ZnVuY3Rpb24gc25hcHNob3QoKXtpZighcGRmS2V5KXJldHVybjt1bmRvU3RhY2sucHVzaChKU09OLnN0cmluZ2lmeSh7bWVhc3VyZW1lbnRzLGdyb3Vwcyxncm91cElkLGNhbH0pKTtpZih1bmRvU3RhY2subGVuZ3RoPjgwKXVuZG9TdGFjay5zaGlmdCgpO3JlZG9TdGFjaz1bXTt1cGRhdGVVbmRvQnV0dG9ucygpfWZ1bmN0aW9uIHJlc3RvcmVTbmFwc2hvdChyYXcpe2NvbnN0IGQ9SlNPTi5wYXJzZShyYXcpO21lYXN1cmVtZW50cz1kLm1lYXN1cmVtZW50cztncm91cHM9ZC5ncm91cHM7Z3JvdXBJZD1kLmdyb3VwSWQ7Y2FsPWQuY2FsO3BvaW50cz1bXTtjYW5jZWxFZGl0KCk7c2F2ZSgpO2RyYXdMaXN0KCk7ZHJhd092ZXJsYXkoKTt1cGRhdGVVbmRvQnV0dG9ucygpfWZ1bmN0aW9uIHVwZGF0ZVVuZG9CdXR0b25zKCl7JCgndW5kbycpLmRpc2FibGVkPSFwb2ludHMubGVuZ3RoJiYhdW5kb1N0YWNrLmxlbmd0aDskKCdyZWRvJykuZGlzYWJsZWQ9IXJlZG9TdGFjay5sZW5ndGh9ZnVuY3Rpb24gdW5kb0FjdGlvbigpe2lmKHBvaW50cy5sZW5ndGgpe3BvaW50cy5wb3AoKTtkcmF3T3ZlcmxheSgpO3VwZGF0ZVVuZG9CdXR0b25zKCk7cmV0dXJufWlmKCF1bmRvU3RhY2subGVuZ3RoKXJldHVybjtyZWRvU3RhY2sucHVzaChKU09OLnN0cmluZ2lmeSh7bWVhc3VyZW1lbnRzLGdyb3Vwcyxncm91cElkLGNhbH0pKTtyZXN0b3JlU25hcHNob3QodW5kb1N0YWNrLnBvcCgpKX1mdW5jdGlvbiByZWRvQWN0aW9uKCl7aWYoIXJlZG9TdGFjay5sZW5ndGgpcmV0dXJuO3VuZG9TdGFjay5wdXNoKEpTT04uc3RyaW5naWZ5KHttZWFzdXJlbWVudHMsZ3JvdXBzLGdyb3VwSWQsY2FsfSkpO3Jlc3RvcmVTbmFwc2hvdChyZWRvU3RhY2sucG9wKCkpfWZ1bmN0aW9uIHN0YXR1cyhzKXskKCdzdGF0dXMnKS50ZXh0Q29udGVudD1zfWZ1bmN0aW9uIGNvbG9yKGlkKXtyZXR1cm4gZ3JvdXBzLmZpbmQoZz0+Zy5pZD09PWlkKT8uY29sb3J8fGNvbG9yc1swXX1mdW5jdGlvbiBncm91cE5hbWUoaWQpe3JldHVybiBncm91cHMuZmluZChnPT5nLmlkPT09aWQpPy5uYW1lfHwnT2vDpG5kIGdydXBwJ31mdW5jdGlvbiBhZGRHcm91cChuYW1lKXtuYW1lPW5hbWUudHJpbSgpO2lmKCFuYW1lKXJldHVybjtjb25zdCBnPXtpZDonZycrRGF0ZS5ub3coKStNYXRoLnJhbmRvbSgpLG5hbWUsY29sb3I6Y29sb3JzW2dyb3Vwcy5sZW5ndGglY29sb3JzLmxlbmd0aF19O3NuYXBzaG90KCk7Z3JvdXBzLnB1c2goZyk7Z3JvdXBJZD1nLmlkO3NhdmUoKTtkcmF3TGlzdCgpfQpmdW5jdGlvbiBpbml0UHJvamVjdHMoKXt0cnl7Y29uc3QgZD1KU09OLnBhcnNlKGxvY2FsU3RvcmFnZS5nZXRJdGVtKCd0aWRsZXR0LWRlbW8tdjMnKXx8J3t9Jyk7JCgncHJvamVjdCcpLmlubmVySFRNTD0nPG9wdGlvbiB2YWx1ZT0iIj5FaiBrb3BwbGFkIHRpbGwgcHJvamVrdDwvb3B0aW9uPicrKGQucHJvamVjdHN8fFtdKS5maWx0ZXIocD0+IXAucXVvdGVPbmx5KS5tYXAocD0+JzxvcHRpb24gdmFsdWU9IicrTnVtYmVyKHAuaWQpKyciPicrU3RyaW5nKHAubmFtZSkucmVwbGFjZUFsbCgnJicsJyZhbXA7JykucmVwbGFjZUFsbCgnPCcsJyZsdDsnKSsnPC9vcHRpb24+Jykuam9pbignJyl9Y2F0Y2h7fX0KZnVuY3Rpb24gc2V0TW9kZShtKXtzbmFwTG9jaz1udWxsO3NuYXBIb3Zlcj1udWxsO21lYXN1cmVIb3Zlcj1udWxsO2NsZWFyRm9sbG93KCk7Y2xlYXJBdXRvKCk7aWYoc2VsZWN0ZWRJZCljYW5jZWxFZGl0KCk7aWYoKG09PT0nbGVuZ3RoJ3x8bT09PSdhcmVhJ3x8bT09PSdhdXRvYXJlYSd8fG09PT0nZm9sbG93JykmJiFjYWxbcGFnZV0pe3N0YXR1cygnS2FsaWJyZXJhIG9jaCBnb2Rrw6RubiBza2FsYW4gaW5uYW4gZHUgbcOkdGVyLicpO3JldHVybn1tb2RlPW07cG9pbnRzPVtdO2RvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGwoJ1tkYXRhLW1vZGVdJykuZm9yRWFjaChiPT5iLmNsYXNzTGlzdC50b2dnbGUoJ2FjdGl2ZScsYi5kYXRhc2V0Lm1vZGU9PT1tKSk7c3RhdHVzKG09PT0nY2FsaWJyYXRlJz8nU3RlZyAxOiBLbGlja2EgcMOlIHR2w6UgcHVua3Rlci4gS29udHJvbGxlcmEgc2VkYW4gYXZzdMOlbmRldCBvY2ggdHJ5Y2sgR29ka8Okbm4ga2FsaWJyZXJpbmcuJzptPT09J2xlbmd0aCc/J03DpHQgZnJpdHQgdmFyIHNvbSBoZWxzdC4gQmzDpXR0IGtyeXNzID0gZnJpIHB1bmt0LCBncsO2bnQgPSBzbsOkcHAuIE3DpXR0ZXQgdmlzYXMgbWVkYW4gZHUgZmx5dHRhciBtdXNlbi4gS2xpY2thIHR2w6UgcHVua3RlciBmw7ZyIGF0dCBzcGFyYS4nOm09PT0nYXJlYSc/J0tsaWNrYSBydW50IHl0YW4uIEFyZWFuIHLDpGtuYXMgYXV0b21hdGlza3QuIFRyeWNrIFNwYXJhIC8gbMOlcyBtw6R0bmluZyBuw6RyIGR1IMOkciBuw7ZqZC4nOm09PT0nYXV0b2FyZWEnPydLbGlja2EgSU5OQU5Gw5ZSIGVuIHNsdXRlbiB5dGEuIFRJRExFVFQgZsO2cnPDtmtlciBmw7ZsamEgZGVuIG3DtnJrYSBrb250dXJlbi4gR3JhbnNrYSBhbGx0aWQgcmVzdWx0YXRldC4nOm09PT0nZm9sbG93Jz8nS2xpY2thIHDDpSBsaW5qZW5zIHN0YXJ0IG9jaCBzZWRhbiBkZXNzIHNsdXQuIFRJRExFVFQgc8O2a2VyIGVuIHbDpGcgbMOkbmdzIG3DtnJrYSByaXRuaW5nc2xpbmplci4gR3JhbnNrYSBvY2ggZ29ka8Okbm4uJzptPT09J2VkaXQnPydLbGlja2EgcMOlIGV0dCBzcGFyYXQgbcOldHQsIGRyYSBkZSBibMOlIHB1bmt0ZXJuYSBvY2ggdHJ5Y2sgU3BhcmEgw6RuZHJpbmcuJzonVmlzYSBvY2ggem9vbWEgcml0bmluZ2VuLicpO2RyYXdPdmVybGF5KCl9CmZ1bmN0aW9uIHRvUHgocCl7cmV0dXJuIHt4OnAueCpzY2FsZUJhc2Uudyp6b29tLHk6cC55KnNjYWxlQmFzZS5oKnpvb219fWZ1bmN0aW9uIGFzcGVjdChwZz1wYWdlKXtjb25zdCBjPWNhbFtwZ107cmV0dXJuIChjJiZ0eXBlb2YgYz09PSdvYmplY3QnJiZjLmFzcGVjdCl8fHNjYWxlQmFzZS53L3NjYWxlQmFzZS5ofWZ1bmN0aW9uIGRpc3QoYSxiLGFyPWFzcGVjdCgpKXtyZXR1cm4gTWF0aC5oeXBvdCgoYS54LWIueCkqYXIsYS55LWIueSl9ZnVuY3Rpb24gcmF3TGVuZ3RoKHBzLGFyPWFzcGVjdCgpKXtyZXR1cm4gcHMuc2xpY2UoMSkucmVkdWNlKChzLHAsaSk9PnMrZGlzdChwLHBzW2ldLGFyKSwwKX1mdW5jdGlvbiByYXdBcmVhKHBzLGFyPWFzcGVjdCgpKXtyZXR1cm4gTWF0aC5hYnMocHMucmVkdWNlKChzLHAsaSk9PnMrcC54KnBzWyhpKzEpJXBzLmxlbmd0aF0ueS1wc1soaSsxKSVwcy5sZW5ndGhdLngqcC55LDApKSphci8yfWZ1bmN0aW9uIGZhY3RvcihwZz1wYWdlKXtjb25zdCBjPWNhbFtwZ107cmV0dXJuIE51bWJlcihjJiZ0eXBlb2YgYz09PSdvYmplY3QnP2MuZmFjdG9yOmN8fDApfWZ1bmN0aW9uIG1lYXN1cmVWYWx1ZShtKXtjb25zdCBmPWZhY3RvcihtLnBhZ2UpLGFyPWFzcGVjdChtLnBhZ2UpO3JldHVybiBtLnR5cGU9PT0nbGVuZ3RoJz9yYXdMZW5ndGgobS5wb2ludHMsYXIpKmY6cmF3QXJlYShtLnBvaW50cyxhcikqZipmfWZ1bmN0aW9uIG11bHRpcGxpZXIobSl7Y29uc3Qgbj1OdW1iZXIobS5tdWx0aXBsaWVyKTtyZXR1cm4gTnVtYmVyLmlzRmluaXRlKG4pJiZuPj0xP01hdGguZmxvb3Iobik6MX1mdW5jdGlvbiB0b3RhbFZhbHVlKG0pe3JldHVybiBtZWFzdXJlVmFsdWUobSkqbXVsdGlwbGllcihtKX1mdW5jdGlvbiB1bml0U2NhbGUoKXtyZXR1cm4gJCgnZGlzcGxheVVuaXQnKT8udmFsdWU9PT0nbW0nPzEwMDA6JCgnZGlzcGxheVVuaXQnKT8udmFsdWU9PT0nY20nPzEwMDoxfWZ1bmN0aW9uIHVuaXROYW1lKCl7cmV0dXJuICQoJ2Rpc3BsYXlVbml0Jyk/LnZhbHVlfHwnbSd9ZnVuY3Rpb24gZm9ybWF0TWVhc3VyZSh2YWx1ZSx0eXBlKXtyZXR1cm4gdHlwZT09PSdhcmVhJz9mbXQodmFsdWUpKycgbcKyJzpmbXQodmFsdWUqdW5pdFNjYWxlKCksdW5pdFNjYWxlKCk9PT0xMDAwPzE6MikrJyAnK3VuaXROYW1lKCl9ZnVuY3Rpb24ga25vd25NZXRlcnMoKXtjb25zdCB1bml0PSQoJ2tub3duVW5pdCcpLnZhbHVlO3JldHVybiBOdW1iZXIoJCgna25vd24nKS52YWx1ZSkvKHVuaXQ9PT0nbW0nPzEwMDA6dW5pdD09PSdjbSc/MTAwOjEpfWZ1bmN0aW9uIG1lYXN1cmVMYWJlbChtLHZhbHVlKXtyZXR1cm4gZm9ybWF0TWVhc3VyZSh2YWx1ZSxtLnR5cGUpKyhtdWx0aXBsaWVyKG0pPjE/JyDDlycrbXVsdGlwbGllcihtKTonJyl9ZnVuY3Rpb24gc3ZnRWwodGFnLGF0dHJzKXtjb25zdCBlbD1kb2N1bWVudC5jcmVhdGVFbGVtZW50TlMobnMsdGFnKTtmb3IoY29uc3QgW2ssdl0gb2YgT2JqZWN0LmVudHJpZXMoYXR0cnMpKWVsLnNldEF0dHJpYnV0ZShrLHYpO3JldHVybiBlbH0KZnVuY3Rpb24gYWRkTGFiZWwoc3ZnLHgseSxsYWJlbCxzdHJva2UpewogY29uc3Qgdz1NYXRoLm1heCg0OSxsYWJlbC5sZW5ndGgqOCsxOCksaD0yNTsKIHg9TWF0aC5tYXgody8yKzMsTWF0aC5taW4oc2NhbGVCYXNlLncqem9vbS13LzItMyx4KSk7eT1NYXRoLm1heChoLzIrMyxNYXRoLm1pbihzY2FsZUJhc2UuaCp6b29tLWgvMi0zLHkpKTsKIGNvbnN0IGc9c3ZnRWwoJ2cnLHsncG9pbnRlci1ldmVudHMnOidub25lJ30pOwogZy5hcHBlbmRDaGlsZChzdmdFbCgncmVjdCcse3g6eC13LzIseTp5LWgvMix3aWR0aDp3LGhlaWdodDpoLHJ4OjYsZmlsbDonI2ZmZmZmZicsJ2ZpbGwtb3BhY2l0eSc6Jy45Nicsc3Ryb2tlOnN0cm9rZSwnc3Ryb2tlLXdpZHRoJzoxLjV9KSk7CiBjb25zdCB0PXN2Z0VsKCd0ZXh0Jyx7eCx5OnkrNCwndGV4dC1hbmNob3InOidtaWRkbGUnLCdmb250LXNpemUnOjEzLCdmb250LXdlaWdodCc6NzUwLGZpbGw6JyMxNzJiMmEnfSk7dC50ZXh0Q29udGVudD1sYWJlbDtnLmFwcGVuZENoaWxkKHQpO3N2Zy5hcHBlbmRDaGlsZChnKQp9CmZ1bmN0aW9uIGxhYmVsUG9pbnQocHRzLHR5cGUpe2lmKHR5cGU9PT0nYXJlYScpe3JldHVybiB7eDpwdHMucmVkdWNlKChzLHApPT5zK3AueCwwKS9wdHMubGVuZ3RoLHk6cHRzLnJlZHVjZSgocyxwKT0+cytwLnksMCkvcHRzLmxlbmd0aH19Y29uc3QgaT1NYXRoLmZsb29yKChwdHMubGVuZ3RoLTEpLzIpO3JldHVybiB7eDoocHRzW2ldLngrcHRzW2krMV0ueCkvMix5OihwdHNbaV0ueStwdHNbaSsxXS55KS8yLTIwfX0KZnVuY3Rpb24gY2FuY2VsRWRpdCgpe3NlbGVjdGVkSWQ9bnVsbDtlZGl0UG9pbnRzPW51bGw7ZHJhZ0luZGV4PS0xOyQoJ3NhdmVFZGl0JykuaGlkZGVuPXRydWU7JCgnY2FuY2VsRWRpdCcpLmhpZGRlbj10cnVlO2RyYXdPdmVybGF5KCl9CmZ1bmN0aW9uIGVkaXRNZWFzdXJlbWVudChpZCl7Y29uc3QgbT1tZWFzdXJlbWVudHMuZmluZCh4PT54LmlkPT09aWQpO2lmKCFtKXJldHVybjtzZWxlY3RlZElkPWlkO2VkaXRQb2ludHM9bS5wb2ludHMubWFwKHA9Pih7Li4ucH0pKTskKCdzYXZlRWRpdCcpLmhpZGRlbj1mYWxzZTskKCdjYW5jZWxFZGl0JykuaGlkZGVuPWZhbHNlO3N0YXR1cygnRHJhIGRlIGJsw6UgcHVua3Rlcm5hLiBUcnljayBTcGFyYSDDpG5kcmluZyBuw6RyIG3DpXR0ZXQgc3TDpG1tZXIuJyk7ZHJhd092ZXJsYXkoKX0KbGV0IHNuYXBDYWNoZT1udWxsLGZvbGxvd1ByZXZpZXc9ZmFsc2UsZm9sbG93U3RhcnQ9bnVsbDtsZXQgc25hcEhvdmVyPW51bGw7bGV0IG1lYXN1cmVIb3Zlcj1udWxsOwpmdW5jdGlvbiBjbGVhckZvbGxvdygpe2ZvbGxvd1ByZXZpZXc9ZmFsc2U7Zm9sbG93U3RhcnQ9bnVsbDskKCdhcHByb3ZlRm9sbG93JykuaGlkZGVuPXRydWU7JCgnY2FuY2VsRm9sbG93JykuaGlkZGVuPXRydWV9CmZ1bmN0aW9uIGdldEluaygpe2NvbnN0IGM9JCgnY2FudmFzJyk7aWYoIWMud2lkdGgpcmV0dXJuIG51bGw7aWYoc25hcENhY2hlJiZzbmFwQ2FjaGUuY2FudmFzPT09YyYmc25hcENhY2hlLncwPT09Yy53aWR0aCYmc25hcENhY2hlLmgwPT09Yy5oZWlnaHQmJnNuYXBDYWNoZS5wZz09PXBhZ2UpcmV0dXJuIHNuYXBDYWNoZTtjb25zdCByYXRpbz1NYXRoLm1pbigxLDkwMC9NYXRoLm1heChjLndpZHRoLGMuaGVpZ2h0KSksdz1NYXRoLm1heCgxLE1hdGgucm91bmQoYy53aWR0aCpyYXRpbykpLGg9TWF0aC5tYXgoMSxNYXRoLnJvdW5kKGMuaGVpZ2h0KnJhdGlvKSksdG1wPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2NhbnZhcycpO3RtcC53aWR0aD13O3RtcC5oZWlnaHQ9aDtjb25zdCBjdHg9dG1wLmdldENvbnRleHQoJzJkJyx7d2lsbFJlYWRGcmVxdWVudGx5OnRydWV9KTtjdHguZHJhd0ltYWdlKGMsMCwwLHcsaCk7Y29uc3QgcmdiYT1jdHguZ2V0SW1hZ2VEYXRhKDAsMCx3LGgpLmRhdGEsaW5rPW5ldyBVaW50OEFycmF5KHcqaCk7Zm9yKGxldCBpPTAsaj0wO2k8aW5rLmxlbmd0aDtpKyssais9NClpbmtbaV09TWF0aC5yb3VuZCguMjk5KnJnYmFbal0rLjU4NypyZ2JhW2orMV0rLjExNCpyZ2JhW2orMl0pO3JldHVybiBzbmFwQ2FjaGU9e2NhbnZhczpjLHcwOmMud2lkdGgsaDA6Yy5oZWlnaHQscGc6cGFnZSx3LGgsaW5rfX0KLy8gUHJlY2lzaW9uc3Nuw6RwcDogbGl0ZW4gZsOlbmdzdHJhZGllLCBzdGFiaWwgbMOlc25pbmcgb2NoIGVuZGFzdCB0eWRsaWd0IG3DtnJrYSBwaXhsYXIuCmxldCBzbmFwTG9jaz1udWxsOwpsZXQgdmVjdG9yU2VnbWVudHM9W10sdmVjdG9yUmVhZHk9ZmFsc2UsdmVjdG9yUGFnZT0wOwpjb25zdCB2bXVsPShhLGIpPT5bYVswXSpiWzBdK2FbMl0qYlsxXSxhWzFdKmJbMF0rYVszXSpiWzFdLGFbMF0qYlsyXSthWzJdKmJbM10sYVsxXSpiWzJdK2FbM10qYlszXSxhWzBdKmJbNF0rYVsyXSpiWzVdK2FbNF0sYVsxXSpiWzRdK2FbM10qYls1XSthWzVdXTsKYXN5bmMgZnVuY3Rpb24gZXh0cmFjdFZlY3RvclNlZ21lbnRzKHBnKXsKIHZlY3RvclNlZ21lbnRzPVtdO3ZlY3RvclJlYWR5PWZhbHNlO3ZlY3RvclBhZ2U9cGFnZTsKIHRyeXsKICBjb25zdCBsaXN0PWF3YWl0IHBnLmdldE9wZXJhdG9yTGlzdCgpLE89cGRmanNMaWIuT1BTLHZpZXc9cGcuZ2V0Vmlld3BvcnQoe3NjYWxlOjEuNH0pLHN0YWNrPVtdOwogIGxldCBtYXRyaXg9WzEsMCwwLDEsMCwwXSxwYXRoPVtdLGN1cnNvcj1udWxsLHN1YnN0YXJ0PW51bGw7CiAgY29uc3QgcHQ9KHgseSk9Pntjb25zdCB2PXZpZXcuY29udmVydFRvVmlld3BvcnRQb2ludChtYXRyaXhbMF0qeCttYXRyaXhbMl0qeSttYXRyaXhbNF0sbWF0cml4WzFdKngrbWF0cml4WzNdKnkrbWF0cml4WzVdKTtyZXR1cm4ge3g6dlswXS92aWV3LndpZHRoLHk6dlsxXS92aWV3LmhlaWdodH19OwogIGNvbnN0IGFkZD0oYSxiKT0+e2lmKCFhfHwhYnx8dmVjdG9yU2VnbWVudHMubGVuZ3RoPj0xNTAwMDApcmV0dXJuO2NvbnN0IGxlbj1NYXRoLmh5cG90KChhLngtYi54KSp2aWV3LndpZHRoLChhLnktYi55KSp2aWV3LmhlaWdodCk7aWYobGVuPjAuMzUmJmxlbjxNYXRoLm1heCh2aWV3LndpZHRoLHZpZXcuaGVpZ2h0KSoyKXBhdGgucHVzaCh7YSxifSl9OwogIGNvbnN0IGN1cnZlPShhLGMxLGMyLGIpPT57bGV0IHByZXY9YTtmb3IobGV0IGk9MTtpPD04O2krKyl7Y29uc3QgdD1pLzgsdT0xLXQscT17eDp1KnUqdSphLngrMyp1KnUqdCpjMS54KzMqdSp0KnQqYzIueCt0KnQqdCpiLngseTp1KnUqdSphLnkrMyp1KnUqdCpjMS55KzMqdSp0KnQqYzIueSt0KnQqdCpiLnl9O2FkZChwcmV2LHEpO3ByZXY9cX19OwogIGNvbnN0IGNvbW1pdD0oKT0+e2lmKHBhdGgubGVuZ3RoKXt2ZWN0b3JTZWdtZW50cy5wdXNoKC4uLnBhdGgpO3BhdGg9W119fTsKICBmb3IobGV0IGk9MDtpPGxpc3QuZm5BcnJheS5sZW5ndGg7aSsrKXsKICAgY29uc3Qgb3A9bGlzdC5mbkFycmF5W2ldLGFyZ3M9bGlzdC5hcmdzQXJyYXlbaV18fFtdOwogICBpZihvcD09PU8uc2F2ZSl7c3RhY2sucHVzaChtYXRyaXguc2xpY2UoKSk7Y29udGludWV9CiAgIGlmKG9wPT09Ty5yZXN0b3JlKXttYXRyaXg9c3RhY2sucG9wKCl8fFsxLDAsMCwxLDAsMF07Y29udGludWV9CiAgIGlmKG9wPT09Ty50cmFuc2Zvcm0pe21hdHJpeD12bXVsKG1hdHJpeCxhcmdzKTtjb250aW51ZX0KICAgaWYob3A9PT1PLmNvbnN0cnVjdFBhdGgpewogICAgY29uc3Qgb3BzPWFyZ3NbMF0sbnVtcz1hcmdzWzFdO2xldCBrPTA7CiAgICBmb3IoY29uc3QgY21kIG9mIG9wcyl7CiAgICAgaWYoY21kPT09Ty5tb3ZlVG8pe2N1cnNvcj1wdChudW1zW2srK10sbnVtc1trKytdKTtzdWJzdGFydD1jdXJzb3J9CiAgICAgZWxzZSBpZihjbWQ9PT1PLmxpbmVUbyl7Y29uc3QgYj1wdChudW1zW2srK10sbnVtc1trKytdKTthZGQoY3Vyc29yLGIpO2N1cnNvcj1ifQogICAgIGVsc2UgaWYoY21kPT09Ty5jdXJ2ZVRvKXtjb25zdCBjMT1wdChudW1zW2srK10sbnVtc1trKytdKSxjMj1wdChudW1zW2srK10sbnVtc1trKytdKSxiPXB0KG51bXNbaysrXSxudW1zW2srK10pO2lmKGN1cnNvciljdXJ2ZShjdXJzb3IsYzEsYzIsYik7Y3Vyc29yPWJ9CiAgICAgZWxzZSBpZihjbWQ9PT1PLmN1cnZlVG8yKXtjb25zdCBjMj1wdChudW1zW2srK10sbnVtc1trKytdKSxiPXB0KG51bXNbaysrXSxudW1zW2srK10pO2lmKGN1cnNvciljdXJ2ZShjdXJzb3IsY3Vyc29yLGMyLGIpO2N1cnNvcj1ifQogICAgIGVsc2UgaWYoY21kPT09Ty5jdXJ2ZVRvMyl7Y29uc3QgYzE9cHQobnVtc1trKytdLG51bXNbaysrXSksYj1wdChudW1zW2srK10sbnVtc1trKytdKTtpZihjdXJzb3IpY3VydmUoY3Vyc29yLGMxLGIsYik7Y3Vyc29yPWJ9CiAgICAgZWxzZSBpZihjbWQ9PT1PLnJlY3RhbmdsZSl7Y29uc3QgeD1udW1zW2srK10seT1udW1zW2srK10sdz1udW1zW2srK10saD1udW1zW2srK10sYT1wdCh4LHkpLGI9cHQoeCt3LHkpLGM9cHQoeCt3LHkraCksZD1wdCh4LHkraCk7YWRkKGEsYik7YWRkKGIsYyk7YWRkKGMsZCk7YWRkKGQsYSk7Y3Vyc29yPWE7c3Vic3RhcnQ9YX0KICAgICBlbHNlIGlmKGNtZD09PU8uY2xvc2VQYXRoKXthZGQoY3Vyc29yLHN1YnN0YXJ0KTtjdXJzb3I9c3Vic3RhcnR9CiAgICB9CiAgIH0KICAgLy8gRsOlbmdhIMOkdmVuIGVuc2tpbGRhIHBhdGgtb3BlcmF0b3JlciBmcsOlbiB2aXNzYSBQREYtZ2VuZXJhdG9yZXIuCiAgIGVsc2UgaWYob3A9PT1PLnN0cm9rZXx8b3A9PT1PLmNsb3NlU3Ryb2tlfHxvcD09PU8uZmlsbFN0cm9rZXx8b3A9PT1PLmVvRmlsbFN0cm9rZXx8b3A9PT1PLmNsb3NlRmlsbFN0cm9rZXx8b3A9PT1PLmNsb3NlRU9GaWxsU3Ryb2tlKXtjb21taXQoKX0KICAgZWxzZSBpZihvcD09PU8uZW5kUGF0aHx8b3A9PT1PLmZpbGx8fG9wPT09Ty5lb0ZpbGwpe3BhdGg9W119CiAgfQogIHZlY3RvclJlYWR5PXZlY3RvclNlZ21lbnRzLmxlbmd0aD4wOwogfWNhdGNoKGUpe2NvbnNvbGUud2FybignVmVrdG9yZ2VvbWV0cmkga3VuZGUgaW50ZSBsw6RzYXM6JyxlKTt2ZWN0b3JTZWdtZW50cz1bXTt2ZWN0b3JSZWFkeT1mYWxzZX0KfQpmdW5jdGlvbiB2ZWN0b3JDYW5kaWRhdGUocCl7CiBpZighdmVjdG9yUmVhZHkpcmV0dXJuIG51bGw7CiBjb25zdCBjPSQoJ2NhbnZhcycpLHJhZGl1cz0xMSxjb3JuZXJSYWRpdXM9MTM7CiBsZXQgYmVzdD1udWxsLGJlc3REPXJhZGl1czsKIGZvcihjb25zdCBzZWcgb2YgdmVjdG9yU2VnbWVudHMpewogIGNvbnN0IGE9c2VnLmEsYj1zZWcuYixheD1hLngqYy53aWR0aCxheT1hLnkqYy5oZWlnaHQsYng9Yi54KmMud2lkdGgsYnk9Yi55KmMuaGVpZ2h0LHB4PXAueCpjLndpZHRoLHB5PXAueSpjLmhlaWdodDsKICBjb25zdCBkeD1ieC1heCxkeT1ieS1heSxsZW4yPWR4KmR4K2R5KmR5OwogIGlmKGxlbjI8LjEpY29udGludWU7CiAgY29uc3QgdD1NYXRoLm1heCgwLE1hdGgubWluKDEsKChweC1heCkqZHgrKHB5LWF5KSpkeSkvbGVuMikpOwogIGNvbnN0IHg9YXgrdCpkeCx5PWF5K3QqZHksZD1NYXRoLmh5cG90KHB4LXgscHkteSk7CiAgaWYoZDxiZXN0RCl7YmVzdEQ9ZDtiZXN0PXtwb2ludDp7eDp4L2Mud2lkdGgseTp5L2MuaGVpZ2h0fSxraW5kOidsaW5lJyxsb2NrZWQ6dHJ1ZX19CiAgZm9yKGNvbnN0IHYgb2YgW2EsYl0pe2NvbnN0IHZkPU1hdGguaHlwb3QocHgtdi54KmMud2lkdGgscHktdi55KmMuaGVpZ2h0KTtpZih2ZDxjb3JuZXJSYWRpdXMmJih2ZDxiZXN0RCszfHxiZXN0Py5raW5kIT09J2VuZCcpKXtiZXN0RD12ZDtiZXN0PXtwb2ludDp7Li4udn0sa2luZDonZW5kJyxsb2NrZWQ6dHJ1ZX19fQogfQogcmV0dXJuIGJlc3Q7Cn0KZnVuY3Rpb24gc25hcENhbmRpZGF0ZShwLGZvcmNlPWZhbHNlKXsKIGlmKG1vZGU9PT0nY2FsaWJyYXRlJyYmISQoJ3NuYXBDYWxpYnJhdGlvbicpLmNoZWNrZWQpcmV0dXJuIG51bGw7CiBpZighZm9yY2UmJiEkKCdzbmFwRW5hYmxlZCcpLmNoZWNrZWQpcmV0dXJuIG51bGw7CiBjb25zdCB2PXZlY3RvckNhbmRpZGF0ZShwKTtpZih2KXtzbmFwTG9jaz1udWxsO3JldHVybiB2fQogaWYodmVjdG9yUmVhZHkpcmV0dXJuIG51bGw7CiBjb25zdCBkPWdldEluaygpO2lmKCFkKXJldHVybiBudWxsOwogY29uc3Qge3csaCxpbmt9PWQsYz0kKCdjYW52YXMnKSxweD1wLngqKHctMSkscHk9cC55KihoLTEpOwogLy8gQXZzdMOlbmQgYW5nZXMgaSBza8Okcm1waXhsYXIsIHPDpSBwcmVjaXNpb25lbiBrw6RubnMgbGlrYWRhbiB2aWQgem9vbS4KIGNvbnN0IGFjcXVpcmU9Zm9yY2U/MTE6OSxyZWxlYXNlPTE1OwogaWYoc25hcExvY2smJiFmb3JjZSl7CiAgY29uc3QgZGlzdD1NYXRoLmh5cG90KChzbmFwTG9jay54LXAueCkqYy53aWR0aCwoc25hcExvY2sueS1wLnkpKmMuaGVpZ2h0KTsKICBpZihkaXN0PD1yZWxlYXNlKXJldHVybiB7cG9pbnQ6ey4uLnNuYXBMb2NrfSxsb2NrZWQ6dHJ1ZX07CiAgc25hcExvY2s9bnVsbDsKIH0KIGNvbnN0IHI9TWF0aC5tYXgoMSxNYXRoLmNlaWwoYWNxdWlyZSp3L2Mud2lkdGgpKTtsZXQgYmVzdD1udWxsLGJlc3RTY29yZT1JbmZpbml0eTsKIGZvcihsZXQgZHk9LXI7ZHk8PXI7ZHkrKylmb3IobGV0IGR4PS1yO2R4PD1yO2R4KyspewogIGNvbnN0IHh4PU1hdGgucm91bmQocHgpK2R4LHl5PU1hdGgucm91bmQocHkpK2R5OwogIGlmKHh4PDF8fHl5PDF8fHh4Pj13LTF8fHl5Pj1oLTEpY29udGludWU7CiAgY29uc3QgZGlzdGFuY2U9TWF0aC5oeXBvdCgoeHgvKHctMSktcC54KSpjLndpZHRoLCh5eS8oaC0xKS1wLnkpKmMuaGVpZ2h0KTsKICBpZihkaXN0YW5jZT5hY3F1aXJlKWNvbnRpbnVlOwogIGNvbnN0IGdyYXk9aW5rW3l5KncreHhdOwogIC8vIFbDpGxqIGJhcmEgdHlkbGlnYSBtw7Zya2Egc3RyZWNrLCBpbnRlIGxqdXNhIFBERi1za3VnZ29yLgogIGlmKGdyYXk+MTE1KWNvbnRpbnVlOwogIGNvbnN0IGNvbnRyYXN0PShpbmtbeXkqdyt4eC0xXStpbmtbeXkqdyt4eCsxXStpbmtbKHl5LTEpKncreHhdK2lua1soeXkrMSkqdyt4eF0pLzQtZ3JheTsKICBjb25zdCBzY29yZT1kaXN0YW5jZStncmF5Ki4wMjUtTWF0aC5tYXgoMCxjb250cmFzdCkqLjAxMjsKICBpZihzY29yZTxiZXN0U2NvcmUpe2Jlc3RTY29yZT1zY29yZTtiZXN0PXt4Onh4Lyh3LTEpLHk6eXkvKGgtMSl9fQogfQogaWYoIWJlc3QpcmV0dXJuIG51bGw7CiBpZighZm9yY2Upc25hcExvY2s9ey4uLmJlc3R9OwogcmV0dXJuIHtwb2ludDpiZXN0LGxvY2tlZDp0cnVlfTsKfQpmdW5jdGlvbiBzbmFwUG9pbnQocCxmb3JjZT1mYWxzZSl7cmV0dXJuIHNuYXBDYW5kaWRhdGUocCxmb3JjZSk/LnBvaW50fHxwfQoKZnVuY3Rpb24gdHJhY2VJbmsoYSxiKXtjb25zdCBkPWdldEluaygpO2lmKCFkKXJldHVybiBudWxsO2NvbnN0IHt3LGgsaW5rfT1kLE49dypoLGlkeD1wPT5NYXRoLm1heCgwLE1hdGgubWluKGgtMSxNYXRoLnJvdW5kKHAueSooaC0xKSkpKSp3K01hdGgubWF4KDAsTWF0aC5taW4ody0xLE1hdGgucm91bmQocC54Kih3LTEpKSkpLHN0YXJ0PWlkeChhKSxnb2FsPWlkeChiKTtjb25zdCBjb3N0PW5ldyBGbG9hdDY0QXJyYXkoTikscGFyZW50PW5ldyBJbnQzMkFycmF5KE4pLGNsb3NlZD1uZXcgVWludDhBcnJheShOKTtjb3N0LmZpbGwoSW5maW5pdHkpO3BhcmVudC5maWxsKC0xKTtjb3N0W3N0YXJ0XT0wO2NvbnN0IGhlYXA9W107ZnVuY3Rpb24gcHVzaChpZCxwcmlvcml0eSl7bGV0IGk9aGVhcC5sZW5ndGg7aGVhcC5wdXNoKFtwcmlvcml0eSxpZF0pO3doaWxlKGkpe2NvbnN0IGo9KGktMSk+PjE7aWYoaGVhcFtqXVswXTw9cHJpb3JpdHkpYnJlYWs7aGVhcFtpXT1oZWFwW2pdO2k9an1oZWFwW2ldPVtwcmlvcml0eSxpZF19ZnVuY3Rpb24gcG9wKCl7Y29uc3Qgcm9vdD1oZWFwWzBdLGxhc3Q9aGVhcC5wb3AoKTtpZihoZWFwLmxlbmd0aCl7bGV0IGk9MDt3aGlsZShpKjIrMTxoZWFwLmxlbmd0aCl7bGV0IGo9aSoyKzE7aWYoaisxPGhlYXAubGVuZ3RoJiZoZWFwW2orMV1bMF08aGVhcFtqXVswXSlqKys7aWYobGFzdFswXTw9aGVhcFtqXVswXSlicmVhaztoZWFwW2ldPWhlYXBbal07aT1qfWhlYXBbaV09bGFzdH1yZXR1cm4gcm9vdH1jb25zdCBneD1nb2FsJXcsZ3k9TWF0aC5mbG9vcihnb2FsL3cpO3B1c2goc3RhcnQsMCk7bGV0IHZpc2l0ZWQ9MDtjb25zdCBuZWlnaGJvcnM9W1stMSwwXSxbMSwwXSxbMCwtMV0sWzAsMV0sWy0xLC0xXSxbMSwtMV0sWy0xLDFdLFsxLDFdXTt3aGlsZShoZWFwLmxlbmd0aCYmdmlzaXRlZCsrPE1hdGgubWluKE4sNDgwMDAwKSl7Y29uc3QgWyx2XT1wb3AoKTtpZihjbG9zZWRbdl0pY29udGludWU7Y2xvc2VkW3ZdPTE7aWYodj09PWdvYWwpYnJlYWs7Y29uc3QgeD12JXcseT1NYXRoLmZsb29yKHYvdyk7Zm9yKGNvbnN0IFtkeCxkeV0gb2YgbmVpZ2hib3JzKXtjb25zdCBueD14K2R4LG55PXkrZHk7aWYobng8MHx8bng+PXd8fG55PDB8fG55Pj1oKWNvbnRpbnVlO2NvbnN0IG5pPW55Kncrbng7aWYoY2xvc2VkW25pXSljb250aW51ZTtjb25zdCBicmlnaHRuZXNzPWlua1tuaV07Y29uc3Qgc3RlcD1NYXRoLmh5cG90KGR4LGR5KSooMStNYXRoLm1heCgwLGJyaWdodG5lc3MtOTApLzI0KTtjb25zdCBuZXh0PWNvc3Rbdl0rc3RlcDtpZihuZXh0PGNvc3RbbmldKXtjb3N0W25pXT1uZXh0O3BhcmVudFtuaV09djtwdXNoKG5pLG5leHQrTWF0aC5oeXBvdChneC1ueCxneS1ueSkpfX19aWYocGFyZW50W2dvYWxdPDApcmV0dXJuIG51bGw7Y29uc3QgcGF0aD1bXTtmb3IobGV0IHY9Z29hbDt2Pj0wO3Y9cGFyZW50W3ZdKXtwYXRoLnB1c2goW3YldyxNYXRoLmZsb29yKHYvdyldKTtpZih2PT09c3RhcnQpYnJlYWs7aWYocGF0aC5sZW5ndGg+TilyZXR1cm4gbnVsbH1wYXRoLnJldmVyc2UoKTtsZXQgc2ltcGxpZmllZD1zaW1wbGlmeUxvb3AocGF0aCxNYXRoLm1heCgxLjMsTWF0aC5taW4odyxoKSouMDAxNSkpO2lmKHNpbXBsaWZpZWQubGVuZ3RoPjI1MClzaW1wbGlmaWVkPXNpbXBsaWZpZWQuZmlsdGVyKChfLGkpPT5pJU1hdGguY2VpbChzaW1wbGlmaWVkLmxlbmd0aC8yNTApPT09MHx8aT09PXNpbXBsaWZpZWQubGVuZ3RoLTEpO3JldHVybiBzaW1wbGlmaWVkLm1hcCgoW3gseV0pPT4oe3g6eC8ody0xKSx5OnkvKGgtMSl9KSl9CmZ1bmN0aW9uIGZvbGxvd0NsaWNrKHApe2lmKGZvbGxvd1ByZXZpZXcpe2NsZWFyRm9sbG93KCk7cG9pbnRzPVtdfWNvbnN0IHNuYXBwZWQ9c25hcFBvaW50KHAsdHJ1ZSk7aWYoIWZvbGxvd1N0YXJ0KXtmb2xsb3dTdGFydD1zbmFwcGVkO3BvaW50cz1bc25hcHBlZF07ZHJhd092ZXJsYXkoKTtzdGF0dXMoJ1N0YXJ0cHVua3QgdmFsZC4gS2xpY2thIHDDpSBzbHV0ZXQgYXYgYmFsa2xpbmplbi4nKTtyZXR1cm59Y29uc3Qgcm91dGU9dHJhY2VJbmsoZm9sbG93U3RhcnQsc25hcHBlZCk7aWYoIXJvdXRlfHxyb3V0ZS5sZW5ndGg8Mil7c3RhdHVzKCdLdW5kZSBpbnRlIGbDtmxqYSBsaW5qZW4uIFByb3ZhIG7DpHJtYXJlIGVsbGVyIGFudsOkbmQgTcOkdCBsw6RuZ2QuJyk7cmV0dXJufXBvaW50cz1yb3V0ZTtmb2xsb3dQcmV2aWV3PXRydWU7JCgnYXBwcm92ZUZvbGxvdycpLmhpZGRlbj1mYWxzZTskKCdjYW5jZWxGb2xsb3cnKS5oaWRkZW49ZmFsc2U7ZHJhd092ZXJsYXkoKTtzdGF0dXMoJ0bDtnJzbGFnOiAnK2Zvcm1hdE1lYXN1cmUocmF3TGVuZ3RoKHBvaW50cykqZmFjdG9yKCksJ2xlbmd0aCcpKycuIEtvbnRyb2xsZXJhIGF0dCBkZW4gcm9zYSBsaW5qZW4gZsO2bGplciByw6R0dCBiYWxrIGlubmFuIGR1IHNwYXJhci4nKX0KJCgnYXBwcm92ZUZvbGxvdycpLm9uY2xpY2s9KCk9PntpZighZm9sbG93UHJldmlld3x8cG9pbnRzLmxlbmd0aDwyKXJldHVybjtzbmFwc2hvdCgpO21lYXN1cmVtZW50cy5wdXNoKHtpZDonbScrRGF0ZS5ub3coKStNYXRoLnJhbmRvbSgpLHBhZ2UsdHlwZTonbGVuZ3RoJyxncm91cElkLG11bHRpcGxpZXI6MSxwb2ludHM6cG9pbnRzLm1hcChwPT4oey4uLnB9KSl9KTtjbGVhckZvbGxvdygpO3BvaW50cz1bXTtzYXZlKCk7ZHJhd092ZXJsYXkoKTtkcmF3TGlzdCgpO3VwZGF0ZVVuZG9CdXR0b25zKCk7c3RhdHVzKCdGw7ZsamQgbGluamUgc3BhcmFkLiBLb250cm9sbGVyYSBsw6RuZ2RlbiBtb3Qgcml0bmluZ2VuLicpfTskKCdjYW5jZWxGb2xsb3cnKS5vbmNsaWNrPSgpPT57Y2xlYXJGb2xsb3coKTtwb2ludHM9W107ZHJhd092ZXJsYXkoKTtzdGF0dXMoJ0xpbmplbiBhdmJydXRlbi4nKX07CmZ1bmN0aW9uIGV2ZW50UG9pbnQoZSl7Y29uc3Qgcj0kKCdvdmVybGF5JykuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCk7cmV0dXJuIHt4Ok1hdGgubWF4KDAsTWF0aC5taW4oMSwoZS5jbGllbnRYLXIubGVmdCkvci53aWR0aCkpLHk6TWF0aC5tYXgoMCxNYXRoLm1pbigxLChlLmNsaWVudFktci50b3ApL3IuaGVpZ2h0KSl9fQpmdW5jdGlvbiBkcmF3T3ZlcmxheSgpe2NvbnN0IHN2Zz0kKCdvdmVybGF5Jyk7c3ZnLmlubmVySFRNTD0nJztzdmcuc2V0QXR0cmlidXRlKCd2aWV3Qm94JyxgMCAwICR7c2NhbGVCYXNlLncqem9vbX0gJHtzY2FsZUJhc2UuaCp6b29tfWApOwogLy8gR3LDtm4gdmlzdWVsbCBiZWtyw6RmdGVsc2UgbsOkciBtYXJrw7ZyZW4gdmVya2xpZ2VuIGhhciBoaXR0YXQgZW4gcml0bmluZ3NsaW5qZS4KIGlmKHNuYXBIb3ZlciYmc25hcEhvdmVyLmZvdW5kJiYkKCdzbmFwRW5hYmxlZCcpLmNoZWNrZWQmJlsnY2FsaWJyYXRlJywnbGVuZ3RoJywnYXJlYScsJ2ZvbGxvdyddLmluY2x1ZGVzKG1vZGUpKXsKICBjb25zdCBwPXRvUHgoc25hcEhvdmVyLnBvaW50KSxxPXRvUHgoc25hcEhvdmVyLmN1cnNvcik7CiAgY29uc3QgZz1zdmdFbCgnZycseydwb2ludGVyLWV2ZW50cyc6J25vbmUnLCdkYXRhLXNuYXAtcHJldmlldyc6J3RydWUnfSk7CiAgZy5hcHBlbmRDaGlsZChzdmdFbCgnbGluZScse3gxOnEueCx5MTpxLnkseDI6cC54LHkyOnAueSxzdHJva2U6JyMxNmEzNGEnLCdzdHJva2Utd2lkdGgnOjIsJ3N0cm9rZS1kYXNoYXJyYXknOiczIDMnfSkpOwogIGcuYXBwZW5kQ2hpbGQoc3ZnRWwoJ2NpcmNsZScse2N4OnAueCxjeTpwLnkscjoxNSxmaWxsOicjMjJjNTVlJywnZmlsbC1vcGFjaXR5JzonLjE3JyxzdHJva2U6JyMxNmEzNGEnLCdzdHJva2Utd2lkdGgnOjN9KSk7CiAgZy5hcHBlbmRDaGlsZChzdmdFbCgnY2lyY2xlJyx7Y3g6cC54LGN5OnAueSxyOjUsZmlsbDonIzE2YTM0YScsc3Ryb2tlOicjZmZmJywnc3Ryb2tlLXdpZHRoJzoyfSkpOwogIGNvbnN0IHRhZz1zdmdFbCgndGV4dCcse3g6cC54KzE4LHk6cC55LTE1LGZpbGw6JyMwODdhMzQnLCdmb250LXNpemUnOjEzLCdmb250LXdlaWdodCc6ODAwLHN0cm9rZTonI2ZmZicsJ3N0cm9rZS13aWR0aCc6MywncGFpbnQtb3JkZXInOidzdHJva2UnfSk7dGFnLnRleHRDb250ZW50PSfinJMgU07DhFBQJztnLmFwcGVuZENoaWxkKHRhZyk7c3ZnLmFwcGVuZENoaWxkKGcpOwogfQoKIGZvcihjb25zdCBtIG9mIG1lYXN1cmVtZW50cy5maWx0ZXIoeD0+eC5wYWdlPT09cGFnZSkpewogY29uc3QgcHRzPShtLmlkPT09c2VsZWN0ZWRJZD9lZGl0UG9pbnRzOm0ucG9pbnRzKS5tYXAodG9QeCksYz1jb2xvcihtLmdyb3VwSWQpOwogY29uc3Qgc2hhcGU9c3ZnRWwobS50eXBlPT09J2FyZWEnPydwb2x5Z29uJzoncG9seWxpbmUnLHtwb2ludHM6cHRzLm1hcChwPT5wLngrJywnK3AueSkuam9pbignICcpLGZpbGw6bS50eXBlPT09J2FyZWEnP2MrJzMzJzonbm9uZScsc3Ryb2tlOmMsJ3N0cm9rZS13aWR0aCc6bS5pZD09PXNlbGVjdGVkSWQ/NTozLCdzdHJva2UtbGluZWpvaW4nOidyb3VuZCcsJ3ZlY3Rvci1lZmZlY3QnOidub24tc2NhbGluZy1zdHJva2UnfSk7aWYobW9kZT09PSdlZGl0Jyl7c2hhcGUuc2V0QXR0cmlidXRlKCdwb2ludGVyLWV2ZW50cycsbS50eXBlPT09J2FyZWEnPydhbGwnOidzdHJva2UnKTtzaGFwZS5zdHlsZS5jdXJzb3I9J3BvaW50ZXInO3NoYXBlLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJyxlPT57ZS5zdG9wUHJvcGFnYXRpb24oKTtlZGl0TWVhc3VyZW1lbnQobS5pZCl9KX1zdmcuYXBwZW5kQ2hpbGQoc2hhcGUpOwogcHRzLmZvckVhY2goKHAsaSk9Pntjb25zdCBkb3Q9c3ZnRWwoJ2NpcmNsZScse2N4OnAueCxjeTpwLnkscjptLmlkPT09c2VsZWN0ZWRJZD84OjMuNSxmaWxsOm0uaWQ9PT1zZWxlY3RlZElkPycjMTY2OGMxJzpjLHN0cm9rZTonI2ZmZicsJ3N0cm9rZS13aWR0aCc6bS5pZD09PXNlbGVjdGVkSWQ/MjowfSk7aWYobS5pZD09PXNlbGVjdGVkSWQpe2RvdC5jbGFzc0xpc3QuYWRkKCdlZGl0SGFuZGxlJyk7ZG90LmFkZEV2ZW50TGlzdGVuZXIoJ3BvaW50ZXJkb3duJyxlPT57ZS5wcmV2ZW50RGVmYXVsdCgpO2Uuc3RvcFByb3BhZ2F0aW9uKCk7ZHJhZ0luZGV4PWk7c3VwcHJlc3NDbGljaz10cnVlOyQoJ292ZXJsYXknKS5zZXRQb2ludGVyQ2FwdHVyZShlLnBvaW50ZXJJZCl9KX1zdmcuYXBwZW5kQ2hpbGQoZG90KX0pOwogY29uc3QgcG9zPWxhYmVsUG9pbnQocHRzLG0udHlwZSk7Y29uc3QgdmFsPW0uaWQ9PT1zZWxlY3RlZElkPyh7dHlwZTptLnR5cGUscG9pbnRzOmVkaXRQb2ludHMscGFnZTptLnBhZ2V9KTptO2NvbnN0IHY9bS5pZD09PXNlbGVjdGVkSWQ/KG0udHlwZT09PSdhcmVhJz9yYXdBcmVhKGVkaXRQb2ludHMsYXNwZWN0KG0ucGFnZSkpKmZhY3RvcihtLnBhZ2UpKioyOnJhd0xlbmd0aChlZGl0UG9pbnRzLGFzcGVjdChtLnBhZ2UpKSpmYWN0b3IobS5wYWdlKSk6bWVhc3VyZVZhbHVlKG0pO2FkZExhYmVsKHN2Zyxwb3MueCxwb3MueSxtZWFzdXJlTGFiZWwobSx2KSxjKQogfQogaWYocG9pbnRzLmxlbmd0aCl7Y29uc3QgcHRzPXBvaW50cy5tYXAodG9QeCksaXNDYWw9bW9kZT09PSdjYWxpYnJhdGUnOwogc3ZnLmFwcGVuZENoaWxkKHN2Z0VsKChtb2RlPT09J2FyZWEnfHxtb2RlPT09J2F1dG9hcmVhJykmJnB0cy5sZW5ndGg+PTM/J3BvbHlnb24nOidwb2x5bGluZScse3BvaW50czpwdHMubWFwKHA9PnAueCsnLCcrcC55KS5qb2luKCcgJyksZmlsbDoobW9kZT09PSdhcmVhJ3x8bW9kZT09PSdhdXRvYXJlYScpJiZwdHMubGVuZ3RoPj0zPycjZTYzOTY2MzgnOidub25lJyxzdHJva2U6aXNDYWw/JyMxYjZjYmUnOicjZTYzOTY2Jywnc3Ryb2tlLXdpZHRoJzozLCdzdHJva2UtZGFzaGFycmF5JzonNyA0J30pKTsKIHB0cy5mb3JFYWNoKHA9PnN2Zy5hcHBlbmRDaGlsZChzdmdFbCgnY2lyY2xlJyx7Y3g6cC54LGN5OnAueSxyOjUsZmlsbDppc0NhbD8nIzFiNmNiZSc6JyNlNjM5NjYnfSkpKTsKIGlmKChtb2RlPT09J2xlbmd0aCd8fG1vZGU9PT0nYXJlYSd8fG1vZGU9PT0nYXV0b2FyZWEnfHxtb2RlPT09J2ZvbGxvdycpJiZjYWxbcGFnZV0mJnBvaW50cy5sZW5ndGg+PTIpe2NvbnN0IHY9KG1vZGU9PT0nbGVuZ3RoJ3x8bW9kZT09PSdmb2xsb3cnKT9yYXdMZW5ndGgocG9pbnRzKSpmYWN0b3IoKTpwb2ludHMubGVuZ3RoPj0zP3Jhd0FyZWEocG9pbnRzKSpmYWN0b3IoKSpmYWN0b3IoKTowO2lmKHY+MCl7Y29uc3QgcG9zPWxhYmVsUG9pbnQocHRzLG1vZGUpO2FkZExhYmVsKHN2Zyxwb3MueCxwb3MueSxmbXQodikrKChtb2RlPT09J2xlbmd0aCd8fG1vZGU9PT0nZm9sbG93Jyk/JyBtJzonIG3CsicpLCcjZTYzOTY2Jyl9fQogaWYoaXNDYWwmJnBvaW50cy5sZW5ndGg9PT0yKXtjb25zdCBwb3M9bGFiZWxQb2ludChwdHMsJ2xlbmd0aCcpO2FkZExhYmVsKHN2Zyxwb3MueCxwb3MueSxmbXQoTnVtYmVyKCQoJ2tub3duJykudmFsdWUpKSsnICcrJCgna25vd25Vbml0JykudmFsdWUsJyMxYjZjYmUnKX0KIH0KfQpmdW5jdGlvbiBkcmF3TGlzdCgpe2lmKCFncm91cHMubGVuZ3RoKWFkZEdyb3VwKCdVdHNpZGEgYmFsaycpOyQoJ2dyb3VwJykuaW5uZXJIVE1MPWdyb3Vwcy5tYXAoZz0+YDxvcHRpb24gdmFsdWU9IiR7Zy5pZH0iICR7Zy5pZD09PWdyb3VwSWQ/J3NlbGVjdGVkJzonJ30+JHtnLm5hbWUucmVwbGFjZUFsbCgnPCcsJyZsdDsnKX08L29wdGlvbj5gKS5qb2luKCcnKTtjb25zdCB0b3RhbD0odHlwZSk9Pm1lYXN1cmVtZW50cy5maWx0ZXIobT0+bS50eXBlPT09dHlwZSkucmVkdWNlKCh2LG0pPT52K3RvdGFsVmFsdWUobSksMCk7JCgndG90YWxMZW5ndGgnKS50ZXh0Q29udGVudD1mb3JtYXRNZWFzdXJlKHRvdGFsKCdsZW5ndGgnKSwnbGVuZ3RoJyk7JCgndG90YWxBcmVhJykudGV4dENvbnRlbnQ9Zm9ybWF0TWVhc3VyZSh0b3RhbCgnYXJlYScpLCdhcmVhJyk7JCgnZ3JvdXBzJykuaW5uZXJIVE1MPScnO2dyb3Vwcy5mb3JFYWNoKGc9Pntjb25zdCBibG9jaz1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCdkaXYnKTtibG9jay5jbGFzc05hbWU9J2dyb3VwJztjb25zdCB0aXRsZT1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCdkaXYnKTt0aXRsZS5jbGFzc05hbWU9J2JldHdlZW4nO2NvbnN0IG5hbWU9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnYicpO25hbWUudGV4dENvbnRlbnQ9Zy5uYW1lO25hbWUuc3R5bGUuY29sb3I9Zy5jb2xvcjt0aXRsZS5hcHBlbmRDaGlsZChuYW1lKTtibG9jay5vbmRibGNsaWNrPWU9PntpZihlLnRhcmdldC5jbG9zZXN0KCdidXR0b24sc2VsZWN0JykpcmV0dXJuO2dyb3VwSWQ9Zy5pZDtkcmF3TGlzdCgpO3N0YXR1cygnQWt0aXYgbcOkdGdydXBwOiAnK2cubmFtZSsnLiBOeWEgbcOldHQgaGFtbmFyIGjDpHIuJyl9O2Jsb2NrLnN0eWxlLmN1cnNvcj0nZGVmYXVsdCc7aWYoZ3JvdXBJZD09PWcuaWQpe2Jsb2NrLnN0eWxlLmJvcmRlcj0nMnB4IHNvbGlkICcrZy5jb2xvcjtjb25zdCBtYXJrPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ3NwYW4nKTttYXJrLnRleHRDb250ZW50PScg4pyTIEFrdGl2JzttYXJrLmNsYXNzTmFtZT0ndGlueSc7dGl0bGUuYXBwZW5kQ2hpbGQobWFyayl9Y29uc3QgcmVtb3ZlPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2J1dHRvbicpO3JlbW92ZS50ZXh0Q29udGVudD0nVGEgYm9ydCBncnVwcCc7cmVtb3ZlLm9uY2xpY2s9KCk9PntpZighY29uZmlybSgnVGEgYm9ydCBncnVwcGVuIG9jaCBkZXNzIG3DpHRuaW5nYXI/JykpcmV0dXJuO3NuYXBzaG90KCk7bWVhc3VyZW1lbnRzPW1lYXN1cmVtZW50cy5maWx0ZXIobT0+bS5ncm91cElkIT09Zy5pZCk7Z3JvdXBzPWdyb3Vwcy5maWx0ZXIoeD0+eC5pZCE9PWcuaWQpO2dyb3VwSWQ9Z3JvdXBzWzBdPy5pZHx8Jyc7c2F2ZSgpO2RyYXdMaXN0KCk7ZHJhd092ZXJsYXkoKX07dGl0bGUuYXBwZW5kQ2hpbGQocmVtb3ZlKTtibG9jay5hcHBlbmRDaGlsZCh0aXRsZSk7Zm9yKGNvbnN0IHR5cGUgb2YgWydsZW5ndGgnLCdhcmVhJ10pe2NvbnN0IGl0ZW1zPW1lYXN1cmVtZW50cy5maWx0ZXIobT0+bS5ncm91cElkPT09Zy5pZCYmbS50eXBlPT09dHlwZSk7aWYoIWl0ZW1zLmxlbmd0aCljb250aW51ZTtjb25zdCBzdW09aXRlbXMucmVkdWNlKCh2LG0pPT52K3RvdGFsVmFsdWUobSksMCk7Y29uc3QgaD1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCdwJyk7aC5jbGFzc05hbWU9J211dGVkJztoLnRleHRDb250ZW50PSh0eXBlPT09J2xlbmd0aCc/J0zDpG5nZDogJzonQXJlYTogJykrZm9ybWF0TWVhc3VyZShzdW0sdHlwZSk7YmxvY2suYXBwZW5kQ2hpbGQoaCk7aXRlbXMuZm9yRWFjaChtPT57Y29uc3Qgcm93PWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpO3Jvdy5jbGFzc05hbWU9J21lYXN1cmUnO2NvbnN0IGxhYmVsPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ3NwYW4nKTtsYWJlbC50ZXh0Q29udGVudD0nU2lkYSAnK20ucGFnZSsnIMK3ICcrZm9ybWF0TWVhc3VyZShtZWFzdXJlVmFsdWUobSksdHlwZSkrKG11bHRpcGxpZXIobSk+MT8nIMOXICcrbXVsdGlwbGllcihtKSsnID0gJytmb3JtYXRNZWFzdXJlKHRvdGFsVmFsdWUobSksdHlwZSk6JycpO3Jvdy5hcHBlbmRDaGlsZChsYWJlbCk7Y29uc3QgbXVsdFdyYXA9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnbGFiZWwnKTttdWx0V3JhcC5zdHlsZS5jc3NUZXh0PSdkaXNwbGF5OmZsZXg7YWxpZ24taXRlbXM6Y2VudGVyO2dhcDozcHg7bWFyZ2luOjA7d2hpdGUtc3BhY2U6bm93cmFwJzttdWx0V3JhcC50ZXh0Q29udGVudD0nw5cnO2NvbnN0IG11bHQ9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnaW5wdXQnKTttdWx0LnR5cGU9J251bWJlcic7bXVsdC5taW49JzEnO211bHQuc3RlcD0nMSc7bXVsdC52YWx1ZT1tdWx0aXBsaWVyKG0pO211bHQudGl0bGU9J0fDpW5nZXIg4oCTIGFudGFsIGxpa2FkYW5hJzttdWx0LnNldEF0dHJpYnV0ZSgnYXJpYS1sYWJlbCcsJ0fDpW5nZXIgZsO2ciBtw6R0bmluZyBww6Ugc2lkYSAnK20ucGFnZSk7bXVsdC5zdHlsZS5jc3NUZXh0PSd3aWR0aDo0OXB4O21pbi13aWR0aDo0OXB4O3BhZGRpbmc6NHB4O2ZvbnQtc2l6ZToxMnB4JzttdWx0LmFkZEV2ZW50TGlzdGVuZXIoJ2NoYW5nZScsKCk9Pntjb25zdCBuPU51bWJlcihtdWx0LnZhbHVlKTtpZighTnVtYmVyLmlzU2FmZUludGVnZXIobil8fG48MXx8bj4xMDAwMDApe2FsZXJ0KCdBbmdlIGV0dCBoZWx0YWwgbWVsbGFuIDEgb2NoIDEwMCAwMDAuJyk7bXVsdC52YWx1ZT1tdWx0aXBsaWVyKG0pO3JldHVybn1pZihuPT09bXVsdGlwbGllcihtKSlyZXR1cm47c25hcHNob3QoKTttLm11bHRpcGxpZXI9bjtzYXZlKCk7ZHJhd0xpc3QoKTtkcmF3T3ZlcmxheSgpfSk7bXVsdFdyYXAuYXBwZW5kQ2hpbGQobXVsdCk7cm93LmFwcGVuZENoaWxkKG11bHRXcmFwKTtjb25zdCBkZWw9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnYnV0dG9uJyk7ZGVsLnRleHRDb250ZW50PSdUYSBib3J0JztkZWwub25jbGljaz0oKT0+e3NuYXBzaG90KCk7bWVhc3VyZW1lbnRzPW1lYXN1cmVtZW50cy5maWx0ZXIoeD0+eC5pZCE9PW0uaWQpO3NhdmUoKTtkcmF3TGlzdCgpO2RyYXdPdmVybGF5KCl9O3Jvdy5hcHBlbmRDaGlsZChkZWwpO2NvbnN0IG1vdmU9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnc2VsZWN0Jyk7bW92ZS50aXRsZT0nRmx5dHRhIG3DpXR0IHRpbGwgYW5uYW4gZ3J1cHAnO21vdmUuc3R5bGUubWF4V2lkdGg9JzEwNXB4Jztncm91cHMuZm9yRWFjaChvdGhlcj0+e2NvbnN0IG9wdD1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCdvcHRpb24nKTtvcHQudmFsdWU9b3RoZXIuaWQ7b3B0LnRleHRDb250ZW50PW90aGVyLm5hbWU7b3B0LnNlbGVjdGVkPW90aGVyLmlkPT09bS5ncm91cElkO21vdmUuYXBwZW5kQ2hpbGQob3B0KX0pO21vdmUub25jaGFuZ2U9ZT0+e3NuYXBzaG90KCk7bS5ncm91cElkPWUudGFyZ2V0LnZhbHVlO3NhdmUoKTtkcmF3TGlzdCgpO2RyYXdPdmVybGF5KCl9O3Jvdy5hcHBlbmRDaGlsZChtb3ZlKTtibG9jay5hcHBlbmRDaGlsZChyb3cpfSl9ICQoJ2dyb3VwcycpLmFwcGVuZENoaWxkKGJsb2NrKX0pOyQoJ3NjYWxlVGV4dCcpLnRleHRDb250ZW50PWNhbFtwYWdlXT8n4pyTIEdvZGvDpG5kIGthbGlicmVyaW5nOiAnK2ZtdChmYWN0b3IoKSw2KSsnIG1ldGVyIHBlciByaXRuaW5nc2VuaGV0JzonU2lkYSAnK3BhZ2UrJyBzYWtuYXIga2FsaWJyZXJpbmcnOyQoJ2F4aXNDaGVjaycpLnRleHRDb250ZW50PWNhbFtwYWdlXT8nS29udHJvbGw6IDEwICUgYXYgcml0bmluZ2VucyBicmVkZCA9ICcrZm10KC4xKmFzcGVjdCgpKmZhY3RvcigpLDMpKycgbTsgMTAgJSBhdiByaXRuaW5nZW5zIGjDtmpkID0gJytmbXQoLjEqZmFjdG9yKCksMykrJyBtLiBLb250cm9sbG3DpHQgZXR0IGvDpG50IG3DpXR0IGkgYsOlZGEgcmlrdG5pbmdhcm5hLic6J0thbGlicmVyYSB1dGFuIHNuw6RwcCBmw7Zyc3Qgb2NoIGtvbnRyb2xsbcOkdCBzZWRhbiBiw6VkZSB2w6VncsOkdHQgb2NoIGxvZHLDpHR0Lic7fQphc3luYyBmdW5jdGlvbiByZW5kZXIoKXtpZighcGRmfHxyZW5kZXJpbmcpcmV0dXJuO3JlbmRlcmluZz10cnVlO3NuYXBMb2NrPW51bGw7c25hcENhY2hlPW51bGw7Y2xlYXJGb2xsb3coKTt0cnl7Y29uc3QgcGc9YXdhaXQgcGRmLmdldFBhZ2UocGFnZSksdmlld3BvcnQ9cGcuZ2V0Vmlld3BvcnQoe3NjYWxlOjEuNH0pO3NjYWxlQmFzZT17dzp2aWV3cG9ydC53aWR0aCxoOnZpZXdwb3J0LmhlaWdodH07Y29uc3QgY2FudmFzPSQoJ2NhbnZhcycpO2NhbnZhcy53aWR0aD1NYXRoLnJvdW5kKHZpZXdwb3J0LndpZHRoKnpvb20pO2NhbnZhcy5oZWlnaHQ9TWF0aC5yb3VuZCh2aWV3cG9ydC5oZWlnaHQqem9vbSk7Y2FudmFzLnN0eWxlLndpZHRoPWNhbnZhcy53aWR0aCsncHgnO2NhbnZhcy5zdHlsZS5oZWlnaHQ9Y2FudmFzLmhlaWdodCsncHgnOyQoJ3NoZWV0Jykuc3R5bGUud2lkdGg9Y2FudmFzLndpZHRoKydweCc7JCgnc2hlZXQnKS5zdHlsZS5oZWlnaHQ9Y2FudmFzLmhlaWdodCsncHgnO2F3YWl0IHBnLnJlbmRlcih7Y2FudmFzQ29udGV4dDpjYW52YXMuZ2V0Q29udGV4dCgnMmQnKSx2aWV3cG9ydDpwZy5nZXRWaWV3cG9ydCh7c2NhbGU6MS40Knpvb219KX0pLnByb21pc2U7YXdhaXQgZXh0cmFjdFZlY3RvclNlZ21lbnRzKHBnKTskKCdwYWdlTGFiZWwnKS50ZXh0Q29udGVudD0nU2lkYSAnK3BhZ2UrJyBhdiAnK3BkZi5udW1QYWdlczskKCd6b29tTGFiZWwnKS50ZXh0Q29udGVudD1NYXRoLnJvdW5kKHpvb20qMTAwKSsnICUnO2RyYXdPdmVybGF5KCk7ZHJhd0xpc3QoKX1jYXRjaChlKXtzdGF0dXMoJ0t1bmRlIGludGUgdmlzYSBQREY6ICcrZS5tZXNzYWdlKX1maW5hbGx5e3JlbmRlcmluZz1mYWxzZX19CmFzeW5jIGZ1bmN0aW9uIG9wZW5EcmF3aW5nRmlsZShmKXtzbmFwTG9jaz1udWxsO3NuYXBDYWNoZT1udWxsO2NsZWFyRm9sbG93KCk7aWYoIWYpcmV0dXJuO2lmKCF3aW5kb3cucGRmanNMaWIpcmV0dXJuIHN0YXR1cygnUERGLWJpYmxpb3Rla2V0IGt1bmRlIGludGUgbGFkZGFzLiBBbnNsdXQgdGlsbCBpbnRlcm5ldCBvY2ggw7ZwcG5hIHNpZGFuIGlnZW4uJyk7dHJ5e3BkZmpzTGliLkdsb2JhbFdvcmtlck9wdGlvbnMud29ya2VyU3JjPSdodHRwczovL2NkbmpzLmNsb3VkZmxhcmUuY29tL2FqYXgvbGlicy9wZGYuanMvMy4xMS4xNzQvcGRmLndvcmtlci5taW4uanMnO2NvbnN0IGJ5dGVzPWF3YWl0IGYuYXJyYXlCdWZmZXIoKTtvcmlnaW5hbFBkZkZpbGU9ZjtwZGY9YXdhaXQgcGRmanNMaWIuZ2V0RG9jdW1lbnQoe2RhdGE6bmV3IFVpbnQ4QXJyYXkoYnl0ZXMpfSkucHJvbWlzZTtjYW5jZWxFZGl0KCk7cGRmS2V5PWYubmFtZSsnLScrZi5zaXplO2NvbnN0IHNhdmVkPUpTT04ucGFyc2UobG9jYWxTdG9yYWdlLmdldEl0ZW0oJ3RpZGxldHQtbWVhc3VyZS0nK3BkZktleSl8fCdudWxsJyk7Y2FsPXNhdmVkPy5jYWx8fHt9O2dyb3Vwcz1zYXZlZD8uZ3JvdXBzfHxbXTttZWFzdXJlbWVudHM9c2F2ZWQ/Lm1lYXN1cmVtZW50c3x8W107Z3JvdXBJZD1ncm91cHNbMF0/LmlkfHwnJztwYWdlPTE7em9vbT0xO3VuZG9TdGFjaz1bXTtyZWRvU3RhY2s9W107dXBkYXRlVW5kb0J1dHRvbnMoKTtpbml0UHJvamVjdHMoKTskKCdwcm9qZWN0JykudmFsdWU9c2F2ZWQ/LnByb2plY3RJZHx8Jyc7aWYoIWdyb3Vwcy5sZW5ndGgpYWRkR3JvdXAoJ1V0c2lkYSBiYWxrJyk7YXdhaXQgcmVuZGVyKCk7c3RhdHVzKCdQREYgw7ZwcG5hZC4gVsOkbGogS2FsaWJyZXJhLCBtYXJrZXJhIHR2w6UgcHVua3RlciBvY2ggdHJ5Y2sgR29ka8Okbm4ga2FsaWJyZXJpbmcuJyl9Y2F0Y2goZXJyKXtzdGF0dXMoJ0ZlbCB2aWQgUERGLWlubMOkc25pbmc6ICcrZXJyLm1lc3NhZ2UpfX0KZnVuY3Rpb24gY2xlYXJBdXRvKCl7YXV0b1ByZXZpZXc9ZmFsc2U7cG9pbnRzPVtdOyQoJ2FwcHJvdmVBdXRvJykuaGlkZGVuPXRydWU7JCgnY2FuY2VsQXV0bycpLmhpZGRlbj10cnVlfQokKCdjYW5jZWxBdXRvJykub25jbGljaz0oKT0+e2NsZWFyQXV0bygpO2RyYXdPdmVybGF5KCk7c3RhdHVzKCdBdXRvbWF0aXNrIHl0YSBhdmJydXRlbi4nKX07CiQoJ2FwcHJvdmVBdXRvJykub25jbGljaz0oKT0+e2lmKCFhdXRvUHJldmlld3x8cG9pbnRzLmxlbmd0aDwzKXJldHVybjtjb25zdCBwcz1wb2ludHMubWFwKHA9Pih7Li4ucH0pKTtzbmFwc2hvdCgpO21lYXN1cmVtZW50cy5wdXNoKHtpZDonbScrRGF0ZS5ub3coKStNYXRoLnJhbmRvbSgpLHBhZ2UsdHlwZTonYXJlYScsZ3JvdXBJZCxtdWx0aXBsaWVyOjEscG9pbnRzOnBzfSk7Y2xlYXJBdXRvKCk7c2F2ZSgpO2RyYXdPdmVybGF5KCk7ZHJhd0xpc3QoKTt1cGRhdGVVbmRvQnV0dG9ucygpO3N0YXR1cygnQXV0b21hdGlzayB5dGEgZ29ka8OkbmQgb2NoIHNwYXJhZC4gS29udHJvbGxlcmEgYWxsdGlkIG3DpG5nZGVuIG1vdCByaXRuaW5nZW4uJyl9OwpmdW5jdGlvbiBzaW1wbGlmeUxvb3AocG9seSx0b2xlcmFuY2Upe2lmKHBvbHkubGVuZ3RoPDQpcmV0dXJuIHBvbHk7Y29uc3Qga2VlcD1uZXcgVWludDhBcnJheShwb2x5Lmxlbmd0aCk7a2VlcFswXT1rZWVwW3BvbHkubGVuZ3RoLTFdPTE7Y29uc3Qgc3RhY2s9W1swLHBvbHkubGVuZ3RoLTFdXTt3aGlsZShzdGFjay5sZW5ndGgpe2NvbnN0IFthLGJdPXN0YWNrLnBvcCgpLHA9cG9seVthXSxxPXBvbHlbYl0sZHg9cVswXS1wWzBdLGR5PXFbMV0tcFsxXSxkZW49ZHgqZHgrZHkqZHk7bGV0IGZhcj0wLGlkeD0tMTtmb3IobGV0IGk9YSsxO2k8YjtpKyspe2NvbnN0IHY9cG9seVtpXSx0PWRlbj9NYXRoLm1heCgwLE1hdGgubWluKDEsKCh2WzBdLXBbMF0pKmR4Kyh2WzFdLXBbMV0pKmR5KS9kZW4pKTowO2NvbnN0IGQ9KHZbMF0tcFswXS10KmR4KSoqMisodlsxXS1wWzFdLXQqZHkpKioyO2lmKGQ+ZmFyKXtmYXI9ZDtpZHg9aX19aWYoaWR4Pj0wJiZmYXI+dG9sZXJhbmNlKnRvbGVyYW5jZSl7a2VlcFtpZHhdPTE7c3RhY2sucHVzaChbYSxpZHhdLFtpZHgsYl0pfX1yZXR1cm4gcG9seS5maWx0ZXIoKF8saSk9PmtlZXBbaV0pfQpmdW5jdGlvbiBkZXRlY3RBdXRvQXJlYShlKXtpZighcGRmfHwhY2FsW3BhZ2VdKXJldHVybiBzdGF0dXMoJ0thbGlicmVyYSBzaWRhbiBmw7Zyc3QuJyk7Y2xlYXJBdXRvKCk7Y29uc3Qgc291cmNlPSQoJ2NhbnZhcycpO2lmKCFzb3VyY2Uud2lkdGgpcmV0dXJuO2NvbnN0IHJhdGlvPU1hdGgubWluKDEsMTEwMC9NYXRoLm1heChzb3VyY2Uud2lkdGgsc291cmNlLmhlaWdodCkpLHc9TWF0aC5tYXgoMSxNYXRoLnJvdW5kKHNvdXJjZS53aWR0aCpyYXRpbykpLGg9TWF0aC5tYXgoMSxNYXRoLnJvdW5kKHNvdXJjZS5oZWlnaHQqcmF0aW8pKTtjb25zdCB0bXA9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnY2FudmFzJyk7dG1wLndpZHRoPXc7dG1wLmhlaWdodD1oO2NvbnN0IGN0eD10bXAuZ2V0Q29udGV4dCgnMmQnLHt3aWxsUmVhZEZyZXF1ZW50bHk6dHJ1ZX0pO2N0eC5kcmF3SW1hZ2Uoc291cmNlLDAsMCx3LGgpO2NvbnN0IHBpeD1jdHguZ2V0SW1hZ2VEYXRhKDAsMCx3LGgpLmRhdGE7Y29uc3QgYm91bmRzPSQoJ292ZXJsYXknKS5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKSxzeD1NYXRoLmZsb29yKChlLmNsaWVudFgtYm91bmRzLmxlZnQpL2JvdW5kcy53aWR0aCp3KSxzeT1NYXRoLmZsb29yKChlLmNsaWVudFktYm91bmRzLnRvcCkvYm91bmRzLmhlaWdodCpoKTtpZihzeDwwfHxzeD49d3x8c3k8MHx8c3k+PWgpcmV0dXJuO2NvbnN0IHNpemU9dypoLGJhcnJpZXI9bmV3IFVpbnQ4QXJyYXkoc2l6ZSk7Zm9yKGxldCBpPTAsaj0wO2k8c2l6ZTtpKyssais9NCl7Y29uc3Qgcj1waXhbal0sZz1waXhbaisxXSxiPXBpeFtqKzJdO2JhcnJpZXJbaV09KE1hdGgubWluKHIsZyxiKTwxNDV8fCgocitnK2IpLzM8MTkwKSk/MTowfWNvbnN0IHNlZWQ9c3kqdytzeDtpZihiYXJyaWVyW3NlZWRdKXJldHVybiBzdGF0dXMoJ0tsaWNrYSBww6UgZW4gbGp1cyBwdW5rdCBJTk5BTkbDllIgeXRhbiwgaW50ZSBww6Ugc2rDpGx2YSBsaW5qZW4uJyk7Y29uc3Qgc2Vlbj1uZXcgVWludDhBcnJheShzaXplKSxxdWV1ZT1uZXcgSW50MzJBcnJheShzaXplKTtsZXQgaGVhZD0wLHRhaWw9MTtxdWV1ZVswXT1zZWVkO3NlZW5bc2VlZF09MTtsZXQgdG91Y2hlc0VkZ2U9ZmFsc2U7Y29uc3QgbGltaXQ9TWF0aC5mbG9vcihzaXplKi42NSk7d2hpbGUoaGVhZDx0YWlsKXtjb25zdCB2PXF1ZXVlW2hlYWQrK10seD12JXcseT0odi93KXwwO2lmKHg9PT0wfHx4PT09dy0xfHx5PT09MHx8eT09PWgtMSl0b3VjaGVzRWRnZT10cnVlO2lmKHRhaWw+bGltaXQpYnJlYWs7Zm9yKGNvbnN0IG4gb2YgW3g+MD92LTE6LTEseDx3LTE/disxOi0xLHk+MD92LXc6LTEseTxoLTE/dit3Oi0xXSl7aWYobj49MCYmIWJhcnJpZXJbbl0mJiFzZWVuW25dKXtzZWVuW25dPTE7cXVldWVbdGFpbCsrXT1ufX19aWYodG91Y2hlc0VkZ2V8fHRhaWw+bGltaXR8fHRhaWw8MzUpcmV0dXJuIHN0YXR1cygnSW5nZW4gdHlkbGlndCBzbHV0ZW4geXRhIGhpdHRhZGVzLiBQcm92YSBlbiBhbm5hbiBwdW5rdCBlbGxlciBhbnbDpG5kIE3DpHQgYXJlYSBtYW51ZWxsdC4nKTsKLy8gVHJhY2Ugb3JpZW50ZWQgcGl4ZWwgZWRnZXMgYXJvdW5kIHRoZSBzZWxlY3RlZCB3aGl0ZSByZWdpb24sIHRoZW4gc2ltcGxpZnkgdGhlIGNvbnRvdXIuCmNvbnN0IGVkZ2VzPW5ldyBNYXAoKTtmdW5jdGlvbiBhZGRFZGdlKGF4LGF5LGJ4LGJ5KXtjb25zdCBrPWF4KycsJytheTtsZXQgYT1lZGdlcy5nZXQoayk7aWYoIWEpZWRnZXMuc2V0KGssYT1bXSk7YS5wdXNoKFtieCxieV0pfWZvcihsZXQgaT0wO2k8dGFpbDtpKyspe2NvbnN0IHY9cXVldWVbaV0seD12JXcseT0odi93KXwwO2lmKHk9PT0wfHwhc2Vlblt2LXddKWFkZEVkZ2UoeCx5LHgrMSx5KTtpZih4PT09dy0xfHwhc2Vlblt2KzFdKWFkZEVkZ2UoeCsxLHkseCsxLHkrMSk7aWYoeT09PWgtMXx8IXNlZW5bdit3XSlhZGRFZGdlKHgrMSx5KzEseCx5KzEpO2lmKHg9PT0wfHwhc2Vlblt2LTFdKWFkZEVkZ2UoeCx5KzEseCx5KX1sZXQgbG9vcHM9W107bGV0IGd1YXJkPTA7d2hpbGUoZWRnZXMuc2l6ZSYmZ3VhcmQrKzxzaXplKXtjb25zdCBzdGFydD1lZGdlcy5rZXlzKCkubmV4dCgpLnZhbHVlO2xldCBrZXk9c3RhcnQsbG9vcD1bXSxzdGVwcz0wO3doaWxlKHN0ZXBzKys8c2l6ZSoyKXtjb25zdCBhcnI9ZWRnZXMuZ2V0KGtleSk7aWYoIWFycnx8IWFyci5sZW5ndGgpYnJlYWs7Y29uc3QgW3gseV09a2V5LnNwbGl0KCcsJykubWFwKE51bWJlcik7bG9vcC5wdXNoKFt4LHldKTtjb25zdCBuZXh0PWFyci5wb3AoKTtpZighYXJyLmxlbmd0aCllZGdlcy5kZWxldGUoa2V5KTtrZXk9bmV4dC5qb2luKCcsJyk7aWYoa2V5PT09c3RhcnQpYnJlYWt9aWYoa2V5PT09c3RhcnQmJmxvb3AubGVuZ3RoPj00KWxvb3BzLnB1c2gobG9vcCl9aWYoIWxvb3BzLmxlbmd0aClyZXR1cm4gc3RhdHVzKCdLb250dXJlbiBrdW5kZSBpbnRlIGbDtmxqYXMuIFByb3ZhIG1hbnVlbGwgbcOkdG5pbmcuJyk7bG9vcHMuc29ydCgoYSxiKT0+Yi5sZW5ndGgtYS5sZW5ndGgpO2xldCBwb2x5PWxvb3BzWzBdO3BvbHkucHVzaChwb2x5WzBdKTtwb2x5PXNpbXBsaWZ5TG9vcChwb2x5LE1hdGgubWF4KDEuNSxNYXRoLm1pbih3LGgpKi4wMDIpKTtwb2x5LnBvcCgpO2lmKHBvbHkubGVuZ3RoPjUwMClwb2x5PXBvbHkuZmlsdGVyKChfLGkpPT5pJU1hdGguY2VpbChwb2x5Lmxlbmd0aC81MDApPT09MCk7aWYocG9seS5sZW5ndGg8MylyZXR1cm4gc3RhdHVzKCdLb250dXJlbiBibGV2IGbDtnIgbGl0ZW4uJyk7cG9pbnRzPXBvbHkubWFwKChbeCx5XSk9Pih7eDpNYXRoLm1heCgwLE1hdGgubWluKDEseC93KSkseTpNYXRoLm1heCgwLE1hdGgubWluKDEseS9oKSl9KSk7YXV0b1ByZXZpZXc9dHJ1ZTskKCdhcHByb3ZlQXV0bycpLmhpZGRlbj1mYWxzZTskKCdjYW5jZWxBdXRvJykuaGlkZGVuPWZhbHNlO2RyYXdPdmVybGF5KCk7Y29uc3QgYXJlYT1yYXdBcmVhKHBvaW50cykqZmFjdG9yKCkqKjI7c3RhdHVzKCdGw7Zyc2xhZzogJytmbXQoYXJlYSkrJyBtwrIuIEdyYW5za2EgZGVuIHJvc2Ega29udHVyZW4gbm9nYS4gVHJ5Y2sgR29ka8Okbm4geXRhIGVsbGVyIEF2YnJ5dC4gRnVuZ2VyYXIgYsOkc3QgcMOlIHR5ZGxpZ3Qgc2x1dG5hIGxpbmplci4nKTt9CiQoJ292ZXJsYXknKS5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsZT0+e2lmKHN1cHByZXNzQ2xpY2spcmV0dXJuO2lmKCFwZGZ8fG1vZGU9PT0ncGFuJ3x8bW9kZT09PSdlZGl0JylyZXR1cm47aWYobW9kZT09PSdjYWxpYnJhdGUnJiZwb2ludHMubGVuZ3RoPj0yKXtwb2ludHM9W107cGVuZGluZ0NhbD1udWxsOyQoJ2FwcHJvdmVDYWwnKS5kaXNhYmxlZD10cnVlfWlmKGUuZGV0YWlsPjEpcmV0dXJuO2lmKG1vZGU9PT0nYXV0b2FyZWEnKXtkZXRlY3RBdXRvQXJlYShlKTtyZXR1cm59aWYobW9kZT09PSdmb2xsb3cnKXtmb2xsb3dDbGljayhldmVudFBvaW50KGUpKTtyZXR1cm59Y29uc3Qgcj0kKCdvdmVybGF5JykuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCkscD17eDooZS5jbGllbnRYLXIubGVmdCkvci53aWR0aCx5OihlLmNsaWVudFktci50b3ApL3IuaGVpZ2h0fTtpZihwLng8MHx8cC54PjF8fHAueTwwfHxwLnk+MSlyZXR1cm47cG9pbnRzLnB1c2goc25hcFBvaW50KHApKTt1cGRhdGVVbmRvQnV0dG9ucygpO2lmKG1vZGU9PT0nY2FsaWJyYXRlJyYmcG9pbnRzLmxlbmd0aD09PTIpe2NvbnN0IGQ9ZGlzdChwb2ludHNbMF0scG9pbnRzWzFdKTtpZihkPjApe3BlbmRpbmdDYWw9e3BhZ2UsZCxwb2ludHM6cG9pbnRzLm1hcChwPT4oey4uLnB9KSl9OyQoJ3BlbmRpbmdUZXh0JykudGV4dENvbnRlbnQ9J+KckyBUdsOlIHB1bmt0ZXIgbWFya2VyYWRlLiBBbmdlIG51IHZlcmtsaWcgbMOkbmdkIG92YW4gb2NoIGtsaWNrYSBHb2Rrw6RubiBrYWxpYnJlcmluZy4nOyQoJ2FwcHJvdmVDYWwnKS5kaXNhYmxlZD0hKGtub3duTWV0ZXJzKCk+MCk7JCgna25vd24nKS5mb2N1cygpO3N0YXR1cygnU3RlZyAyOiBBbmdlIHZlcmtsaWd0IG3DpXR0IG9jaCBnb2Rrw6Rubi4nKX1lbHNle3N0YXR1cygnUHVua3Rlcm5hIG3DpXN0ZSBsaWdnYSBww6Ugb2xpa2EgcGxhdHNlci4nKTtwb2ludHM9W119fWlmKG1vZGU9PT0nbGVuZ3RoJyYmcG9pbnRzLmxlbmd0aD09PTIpe2ZpbmlzaE1lYXN1cmVtZW50KCk7cmV0dXJufWRyYXdPdmVybGF5KCl9KTsKZnVuY3Rpb24gZHJhd0xpdmVQcmV2aWV3KHN2Zyxwb2ludCxmb3VuZCl7CiBpZighbWVhc3VyZUhvdmVyKXJldHVybjsKIGNvbnN0IHE9dG9QeChwb2ludCksZz1zdmdFbCgnZycseydwb2ludGVyLWV2ZW50cyc6J25vbmUnLCdkYXRhLWxpdmUtcHJldmlldyc6J3RydWUnfSk7CiAvLyBIw6Vya29yc2V0IGZpbm5zIGFsbHRpZDogZ3LDtm50IG7DpHIgZW4gc27DpHBwcHVua3QgaGl0dGF0cywgYmzDpXR0IHZpZCBmcmkgbcOkdG5pbmcuCiBjb25zdCBjPWZvdW5kPycjMTZhMzRhJzonIzI1NjNlYic7CiBnLmFwcGVuZENoaWxkKHN2Z0VsKCdwYXRoJyx7ZDpgTSAke3EueC0xMH0gJHtxLnl9IGggMjAgTSAke3EueH0gJHtxLnktMTB9IHYgMjBgLHN0cm9rZTpjLCdzdHJva2Utd2lkdGgnOjIuMixmaWxsOidub25lJ30pKTsKIGlmKCFmb3VuZClnLmFwcGVuZENoaWxkKHN2Z0VsKCdjaXJjbGUnLHtjeDpxLngsY3k6cS55LHI6MyxmaWxsOicjZmZmJyxzdHJva2U6Yywnc3Ryb2tlLXdpZHRoJzoxLjV9KSk7CiBpZihwb2ludHMubGVuZ3RoJiZbJ2xlbmd0aCcsJ2FyZWEnLCdjYWxpYnJhdGUnXS5pbmNsdWRlcyhtb2RlKSl7CiAgY29uc3QgYT10b1B4KHBvaW50c1twb2ludHMubGVuZ3RoLTFdKTsKICBnLmFwcGVuZENoaWxkKHN2Z0VsKCdsaW5lJyx7eDE6YS54LHkxOmEueSx4MjpxLngseTI6cS55LHN0cm9rZTonIzI1NjNlYicsJ3N0cm9rZS13aWR0aCc6Miwnc3Ryb2tlLWRhc2hhcnJheSc6JzYgNCd9KSk7CiAgaWYoY2FsW3BhZ2VdJiZtb2RlIT09J2NhbGlicmF0ZScpewogICBjb25zdCB2PW1vZGU9PT0nYXJlYScmJnBvaW50cy5sZW5ndGg+PTI/cmF3QXJlYShbLi4ucG9pbnRzLHBvaW50XSkqZmFjdG9yKCkqKjI6ZGlzdChwb2ludHNbcG9pbnRzLmxlbmd0aC0xXSxwb2ludCkqZmFjdG9yKCk7CiAgIGNvbnN0IGxhYmVsPWZvcm1hdE1lYXN1cmUodixtb2RlPT09J2FyZWEnJiZwb2ludHMubGVuZ3RoPj0yPydhcmVhJzonbGVuZ3RoJyk7CiAgIGNvbnN0IHg9TWF0aC5tYXgoNjUsTWF0aC5taW4oc2NhbGVCYXNlLncqem9vbS02NSwoYS54K3EueCkvMikpLHk9TWF0aC5tYXgoMTksTWF0aC5taW4oc2NhbGVCYXNlLmgqem9vbS0xOSwoYS55K3EueSkvMi0xOSkpOwogICBjb25zdCB3PU1hdGgubWF4KDkwLGxhYmVsLmxlbmd0aCo4KzIwKTsKICAgZy5hcHBlbmRDaGlsZChzdmdFbCgncmVjdCcse3g6eC13LzIseTp5LTEzLHdpZHRoOncsaGVpZ2h0OjI2LHJ4OjYsZmlsbDonI2ZmZicsc3Ryb2tlOicjMjU2M2ViJywnc3Ryb2tlLXdpZHRoJzoxLjV9KSk7CiAgIGNvbnN0IHQ9c3ZnRWwoJ3RleHQnLHt4LHk6eSs1LCd0ZXh0LWFuY2hvcic6J21pZGRsZScsZmlsbDonIzE3M2Q3YScsJ2ZvbnQtc2l6ZSc6MTQsJ2ZvbnQtd2VpZ2h0Jzo4MDB9KTt0LnRleHRDb250ZW50PWxhYmVsO2cuYXBwZW5kQ2hpbGQodCk7CiAgfQogfQogc3ZnLmFwcGVuZENoaWxkKGcpOwp9CiQoJ292ZXJsYXknKS5hZGRFdmVudExpc3RlbmVyKCdwb2ludGVybW92ZScsZT0+ewogaWYoZHJhZ0luZGV4Pj0wJiZlZGl0UG9pbnRzKXtlZGl0UG9pbnRzW2RyYWdJbmRleF09c25hcFBvaW50KGV2ZW50UG9pbnQoZSkpO2RyYXdPdmVybGF5KCk7cmV0dXJufQogaWYoIXBkZnx8IVsnY2FsaWJyYXRlJywnbGVuZ3RoJywnYXJlYScsJ2ZvbGxvdyddLmluY2x1ZGVzKG1vZGUpKXtpZihzbmFwSG92ZXJ8fG1lYXN1cmVIb3Zlcil7c25hcEhvdmVyPW51bGw7bWVhc3VyZUhvdmVyPW51bGw7ZHJhd092ZXJsYXkoKX1yZXR1cm59CiBjb25zdCBjdXJzb3I9ZXZlbnRQb2ludChlKSxjYW5kaWRhdGU9JCgnc25hcEVuYWJsZWQnKS5jaGVja2VkP3NuYXBDYW5kaWRhdGUoY3Vyc29yKTpudWxsLHBvaW50PWNhbmRpZGF0ZT8ucG9pbnR8fGN1cnNvcixmb3VuZD0hIWNhbmRpZGF0ZTsKIG1lYXN1cmVIb3Zlcj17Y3Vyc29yLHBvaW50fTsKIHNuYXBIb3Zlcj17Y3Vyc29yLHBvaW50LGZvdW5kfTsKIGNvbnN0IHN2Zz0kKCdvdmVybGF5Jyk7c3ZnLnF1ZXJ5U2VsZWN0b3IoJ1tkYXRhLXNuYXAtcHJldmlld10nKT8ucmVtb3ZlKCk7c3ZnLnF1ZXJ5U2VsZWN0b3IoJ1tkYXRhLWxpdmUtcHJldmlld10nKT8ucmVtb3ZlKCk7CiBkcmF3TGl2ZVByZXZpZXcoc3ZnLHBvaW50LGZvdW5kKTsKIGlmKGZvdW5kKXtjb25zdCBwPXRvUHgocG9pbnQpLHE9dG9QeChjdXJzb3IpLGc9c3ZnRWwoJ2cnLHsncG9pbnRlci1ldmVudHMnOidub25lJywnZGF0YS1zbmFwLXByZXZpZXcnOid0cnVlJ30pOwogLy8gRXR0IHJpa3RpZ3QgZ3LDtm50IGjDpXJrb3JzIG1hcmtlcmFyIGV4YWt0IGRlbiBsw6VzdGEgcHVua3Rlbi4KIGcuYXBwZW5kQ2hpbGQoc3ZnRWwoJ2xpbmUnLHt4MTpxLngseTE6cS55LHgyOnAueCx5MjpwLnksc3Ryb2tlOicjMTZhMzRhJywnc3Ryb2tlLXdpZHRoJzoxLjUsJ3N0cm9rZS1kYXNoYXJyYXknOiczIDMnfSkpOwogZy5hcHBlbmRDaGlsZChzdmdFbCgnY2lyY2xlJyx7Y3g6cC54LGN5OnAueSxyOjExLGZpbGw6JyMyMmM1NWUnLCdmaWxsLW9wYWNpdHknOicuMTMnLHN0cm9rZTonIzE2YTM0YScsJ3N0cm9rZS13aWR0aCc6Mn0pKTsKIGcuYXBwZW5kQ2hpbGQoc3ZnRWwoJ3BhdGgnLHtkOmBNICR7cC54LTE3fSAke3AueX0gaCAzNCBNICR7cC54fSAke3AueS0xN30gdiAzNGAsc3Ryb2tlOicjMDBiMzQ0Jywnc3Ryb2tlLXdpZHRoJzoyLjUsZmlsbDonbm9uZSd9KSk7CiBnLmFwcGVuZENoaWxkKHN2Z0VsKCdjaXJjbGUnLHtjeDpwLngsY3k6cC55LHI6Mi41LGZpbGw6JyMwMGExM2InLHN0cm9rZTonI2ZmZicsJ3N0cm9rZS13aWR0aCc6MX0pKTsKIGlmKGNhbmRpZGF0ZT8ua2luZD09PSdlbmQnKWcuYXBwZW5kQ2hpbGQoc3ZnRWwoJ3JlY3QnLHt4OnAueC02LHk6cC55LTYsd2lkdGg6MTIsaGVpZ2h0OjEyLGZpbGw6JyNkY2ZjZTcnLHN0cm9rZTonIzE1ODAzZCcsJ3N0cm9rZS13aWR0aCc6Mn0pKTsKIGNvbnN0IHQ9c3ZnRWwoJ3RleHQnLHt4OnAueCsxOSx5OnAueS0xNixmaWxsOicjMDg3YTM0JywnZm9udC1zaXplJzoxMiwnZm9udC13ZWlnaHQnOjgwMCxzdHJva2U6JyNmZmYnLCdzdHJva2Utd2lkdGgnOjMsJ3BhaW50LW9yZGVyJzonc3Ryb2tlJ30pO3QudGV4dENvbnRlbnQ9Y2FuZGlkYXRlPy5raW5kPT09J2VuZCc/J8OETkRQVU5LVCc6Y2FuZGlkYXRlPy5raW5kPT09J2xpbmUnPydWRUtUT1InOidMw4VTVCc7Zy5hcHBlbmRDaGlsZCh0KTtzdmcuYXBwZW5kQ2hpbGQoZyl9Cgp9KTsKJCgnb3ZlcmxheScpLmFkZEV2ZW50TGlzdGVuZXIoJ3BvaW50ZXJsZWF2ZScsKCk9PntzbmFwTG9jaz1udWxsO3NuYXBIb3Zlcj1udWxsO21lYXN1cmVIb3Zlcj1udWxsOyQoJ292ZXJsYXknKS5xdWVyeVNlbGVjdG9yKCdbZGF0YS1zbmFwLXByZXZpZXddJyk/LnJlbW92ZSgpOyQoJ292ZXJsYXknKS5xdWVyeVNlbGVjdG9yKCdbZGF0YS1saXZlLXByZXZpZXddJyk/LnJlbW92ZSgpfSk7CiQoJ3NuYXBFbmFibGVkJykuYWRkRXZlbnRMaXN0ZW5lcignY2hhbmdlJywoKT0+e3NuYXBMb2NrPW51bGw7c25hcEhvdmVyPW51bGw7ZHJhd092ZXJsYXkoKX0pO3dpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdwb2ludGVydXAnLCgpPT57aWYoZHJhZ0luZGV4Pj0wKXtkcmFnSW5kZXg9LTE7c2V0VGltZW91dCgoKT0+e3N1cHByZXNzQ2xpY2s9ZmFsc2V9LDgwKX19KTskKCdzYXZlRWRpdCcpLm9uY2xpY2s9KCk9Pntjb25zdCBtPW1lYXN1cmVtZW50cy5maW5kKHg9PnguaWQ9PT1zZWxlY3RlZElkKTtpZihtJiZlZGl0UG9pbnRzKXtzbmFwc2hvdCgpO20ucG9pbnRzPWVkaXRQb2ludHMubWFwKHA9Pih7Li4ucH0pKTtzYXZlKCk7ZHJhd0xpc3QoKTtzdGF0dXMoJ03DpHRuaW5nZW4gdXBwZGF0ZXJhZCBvY2ggc3BhcmFkLicpfWNhbmNlbEVkaXQoKX07JCgnY2FuY2VsRWRpdCcpLm9uY2xpY2s9KCk9PntjYW5jZWxFZGl0KCk7c3RhdHVzKCfDhG5kcmluZ2VuIGF2YnJ1dGVuLicpfTskKCd0b2dnbGVBc2lkZScpLm9uY2xpY2s9KCk9Pntkb2N1bWVudC5ib2R5LmNsYXNzTGlzdC50b2dnbGUoJ2hpZGVBc2lkZScpOyQoJ3RvZ2dsZUFzaWRlJykudGV4dENvbnRlbnQ9ZG9jdW1lbnQuYm9keS5jbGFzc0xpc3QuY29udGFpbnMoJ2hpZGVBc2lkZScpPydWaXNhIHNpZG9tZW55JzonRMO2bGogc2lkb21lbnknfTtmdW5jdGlvbiBmaW5pc2hNZWFzdXJlbWVudCgpe2lmKG1vZGUhPT0nbGVuZ3RoJyYmbW9kZSE9PSdhcmVhJylyZXR1cm4gc3RhdHVzKCdWw6RsaiBsw6RuZ2QgZWxsZXIgYXJlYSBmw7Zyc3QuJyk7aWYoIWNhbFtwYWdlXSlyZXR1cm4gc3RhdHVzKCdLYWxpYnJlcmEgZGVuIGjDpHIgc2lkYW4gaW5uYW4gZHUgc3BhcmFyIGVuIG3DpHRuaW5nLicpO2lmKHBvaW50cy5sZW5ndGg8KG1vZGU9PT0nYXJlYSc/MzoyKSlyZXR1cm4gc3RhdHVzKCdGw7ZyIGbDpSBwdW5rdGVyIGbDtnIgbcOkdG5pbmdlbi4nKTtzbmFwc2hvdCgpO21lYXN1cmVtZW50cy5wdXNoKHtpZDonbScrRGF0ZS5ub3coKStNYXRoLnJhbmRvbSgpLHBhZ2UsdHlwZTptb2RlLGdyb3VwSWQsbXVsdGlwbGllcjoxLHBvaW50czpwb2ludHMubWFwKHA9Pih7Li4ucH0pKX0pO3BvaW50cz1bXTtzYXZlKCk7ZHJhd092ZXJsYXkoKTtkcmF3TGlzdCgpO3VwZGF0ZVVuZG9CdXR0b25zKCk7c3RhdHVzKCdNw6R0bmluZ2VuIHNwYXJhZCBvY2ggbcOldHRzYXR0IHDDpSByaXRuaW5nZW4uIER1IGthbiBiw7ZyamEgbcOkdGEgcMOlIGVuIGFubmFuIHBsYXRzLicpfTskKCdmaW5pc2gnKS5vbmNsaWNrPWZpbmlzaE1lYXN1cmVtZW50OyQoJ292ZXJsYXknKS5hZGRFdmVudExpc3RlbmVyKCdkYmxjbGljaycsZT0+e2UucHJldmVudERlZmF1bHQoKX0pO2RvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ2tleWRvd24nLGU9PntpZigoZS5jdHJsS2V5fHxlLm1ldGFLZXkpJiZlLmtleS50b0xvd2VyQ2FzZSgpPT09J3onKXtlLnByZXZlbnREZWZhdWx0KCk7ZS5zaGlmdEtleT9yZWRvQWN0aW9uKCk6dW5kb0FjdGlvbigpO3JldHVybn1pZigoZS5jdHJsS2V5fHxlLm1ldGFLZXkpJiZlLmtleS50b0xvd2VyQ2FzZSgpPT09J3knKXtlLnByZXZlbnREZWZhdWx0KCk7cmVkb0FjdGlvbigpO3JldHVybn1pZihlLmtleT09PSdFc2NhcGUnJiZwb2ludHMubGVuZ3RoKXtjbGVhckZvbGxvdygpO2NsZWFyQXV0bygpO2RyYXdPdmVybGF5KCk7c3RhdHVzKCdQw6Vnw6VlbmRlIG3DpHRuaW5nIGF2YnJ1dGVuLicpfX0pOyQoJ3VuZG8nKS5vbmNsaWNrPXVuZG9BY3Rpb247JCgncmVkbycpLm9uY2xpY2s9cmVkb0FjdGlvbjtkb2N1bWVudC5xdWVyeVNlbGVjdG9yQWxsKCdbZGF0YS1tb2RlXScpLmZvckVhY2goYj0+Yi5vbmNsaWNrPSgpPT5zZXRNb2RlKGIuZGF0YXNldC5tb2RlKSk7JCgna25vd24nKS5hZGRFdmVudExpc3RlbmVyKCdpbnB1dCcsKCk9PntpZihwZW5kaW5nQ2FsKSQoJ2FwcHJvdmVDYWwnKS5kaXNhYmxlZD0hKGtub3duTWV0ZXJzKCk+MCl9KTskKCdrbm93blVuaXQnKS5hZGRFdmVudExpc3RlbmVyKCdjaGFuZ2UnLCgpPT57aWYocGVuZGluZ0NhbCkkKCdhcHByb3ZlQ2FsJykuZGlzYWJsZWQ9IShrbm93bk1ldGVycygpPjApfSk7JCgnY2FsaWJyYXRlQnRuJykub25jbGljaz0oKT0+e3BlbmRpbmdDYWw9bnVsbDskKCdhcHByb3ZlQ2FsJykuZGlzYWJsZWQ9dHJ1ZTskKCdwZW5kaW5nVGV4dCcpLnRleHRDb250ZW50PSdNYXJrZXJhIHR2w6UgcHVua3RlciBww6Ugcml0bmluZ2VuLic7c2V0TW9kZSgnY2FsaWJyYXRlJyl9OyQoJ2FwcHJvdmVDYWwnKS5vbmNsaWNrPSgpPT57aWYoIXBlbmRpbmdDYWx8fHBlbmRpbmdDYWwucGFnZSE9PXBhZ2UpcmV0dXJuO2NvbnN0IGtub3duPWtub3duTWV0ZXJzKCk7aWYoIShrbm93bj4wKSl7c3RhdHVzKCdBbmdlIGV0dCBnaWx0aWd0IG3DpXR0LicpO3JldHVybn1zbmFwc2hvdCgpO2NhbFtwYWdlXT17ZmFjdG9yOmtub3duL3BlbmRpbmdDYWwuZCxhc3BlY3Q6c2NhbGVCYXNlLncvc2NhbGVCYXNlLmh9O3BlbmRpbmdDYWw9bnVsbDtwb2ludHM9W107JCgnYXBwcm92ZUNhbCcpLmRpc2FibGVkPXRydWU7JCgncGVuZGluZ1RleHQnKS50ZXh0Q29udGVudD0n4pyTIEthbGlicmVyaW5nZW4gZ29ka8OkbmQgZsO2ciBzaWRhICcrcGFnZTtzYXZlKCk7ZHJhd0xpc3QoKTtzZXRNb2RlKCdsZW5ndGgnKTtzdGF0dXMoJ1NrYWxhbiDDpHIgZ29ka8OkbmQhIEtsaWNrYSB0dsOlIHB1bmt0ZXI6IHN0csOkY2thbiBzcGFyYXMgb2NoIGzDpXNlcyBhdXRvbWF0aXNrdC4gTsOkc3RhIGtsaWNrIGLDtnJqYXIgZW4gbnkgc3Ryw6Rja2EuJyl9OyQoJ2dyb3VwJykub25jaGFuZ2U9ZT0+e2dyb3VwSWQ9ZS50YXJnZXQudmFsdWU7ZHJhd0xpc3QoKX07JCgnYWRkR3JvdXAnKS5vbmNsaWNrPSgpPT57JCgnZ3JvdXBOYW1lJykudmFsdWUudHJpbSgpJiZhZGRHcm91cCgkKCdncm91cE5hbWUnKS52YWx1ZSk7JCgnZ3JvdXBOYW1lJykudmFsdWU9Jyd9OyQoJ3Byb2plY3QnKS5vbmNoYW5nZT1zYXZlOyQoJ2Rpc3BsYXlVbml0Jykub25jaGFuZ2U9KCk9PntkcmF3TGlzdCgpO2RyYXdPdmVybGF5KCl9OyQoJ3ByZXYnKS5vbmNsaWNrPSgpPT57aWYocGRmJiZwYWdlPjEpe3BhZ2UtLTtwb2ludHM9W107cGVuZGluZ0NhbD1udWxsOyQoJ2FwcHJvdmVDYWwnKS5kaXNhYmxlZD10cnVlO3JlbmRlcigpfX07JCgnbmV4dCcpLm9uY2xpY2s9KCk9PntpZihwZGYmJnBhZ2U8cGRmLm51bVBhZ2VzKXtwYWdlKys7cG9pbnRzPVtdO3BlbmRpbmdDYWw9bnVsbDskKCdhcHByb3ZlQ2FsJykuZGlzYWJsZWQ9dHJ1ZTtyZW5kZXIoKX19OyQoJ3pvb21JbicpLm9uY2xpY2s9KCk9Pnt6b29tPU1hdGgubWluKDMsem9vbSoxLjI1KTtyZW5kZXIoKX07JCgnem9vbU91dCcpLm9uY2xpY2s9KCk9Pnt6b29tPU1hdGgubWF4KC40LHpvb20vMS4yNSk7cmVuZGVyKCl9OyQoJ2V4cG9ydCcpLm9uY2xpY2s9KCk9Pntjb25zdCBibG9iPW5ldyBCbG9iKFtKU09OLnN0cmluZ2lmeSh7cGRmTmFtZTpwZGZLZXksY2FsLGdyb3VwcyxtZWFzdXJlbWVudHMscHJvamVjdElkOiQoJ3Byb2plY3QnKS52YWx1ZX0sbnVsbCwyKV0se3R5cGU6J2FwcGxpY2F0aW9uL2pzb24nfSksdXJsPVVSTC5jcmVhdGVPYmplY3RVUkwoYmxvYiksYT1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCdhJyk7YS5ocmVmPXVybDthLmRvd25sb2FkPSdUSURMRVRUX21hdG5pbmdhci5qc29uJzthLmNsaWNrKCk7c2V0VGltZW91dCgoKT0+VVJMLnJldm9rZU9iamVjdFVSTCh1cmwpLDEwMDApfTsKZnVuY3Rpb24gc2FmZVBkZlRleHQodCl7cmV0dXJuIFN0cmluZyh0KS5yZXBsYWNlKC9bXHUyMDEwLVx1MjAxNV0vZywnLScpLnJlcGxhY2UoL1teXHgyMC1ceDdlXHUwMGEwLVx1MDBmZlx1MjBhY1x1MjAxOFx1MjAxOVx1MjAxY1x1MjAxZF0vZywnICcpfQpmdW5jdGlvbiBwZGZSZ2IoaGV4KXtjb25zdCB2PWhleC5yZXBsYWNlKCcjJywnJyk7cmV0dXJuIFBERkxpYi5yZ2IocGFyc2VJbnQodi5zbGljZSgwLDIpLDE2KS8yNTUscGFyc2VJbnQodi5zbGljZSgyLDQpLDE2KS8yNTUscGFyc2VJbnQodi5zbGljZSg0LDYpLDE2KS8yNTUpfQphc3luYyBmdW5jdGlvbiBleHBvcnRQZGYoKXtpZighb3JpZ2luYWxQZGZGaWxlKXJldHVybiBhbGVydCgnw5ZwcG5hIGVuIFBERiBmw7Zyc3QuJyk7aWYoIXdpbmRvdy5QREZMaWIpcmV0dXJuIGFsZXJ0KCdQREYtZXhwb3J0YmlibGlvdGVrZXQga3VuZGUgaW50ZSBsYWRkYXMuIEtvbnRyb2xsZXJhIGludGVybmV0YW5zbHV0bmluZ2VuLicpO2NvbnN0IGJ0bj0kKCdleHBvcnRQZGYnKTtidG4uZGlzYWJsZWQ9dHJ1ZTtidG4udGV4dENvbnRlbnQ9J1NrYXBhciBQREbigKYnO3RyeXsKLy8gRXhwb3J0IGZyb20gdGhlIHNhbWUgUERGLmpzIHZpZXdwb3J0IHVzZWQgYnkgdGhlIG9uLXNjcmVlbiBtZWFzdXJlbWVudCBvdmVybGF5LgovLyBUaGlzIGhhbmRsZXMgcGFnZSByb3RhdGlvbiwgY3JvcCBib3hlcyBhbmQgdG9wLWxlZnQgc2NyZWVuIGNvb3JkaW5hdGVzIGNvbnNpc3RlbnRseS4KY29uc3QgZG9jT3V0PWF3YWl0IFBERkxpYi5QREZEb2N1bWVudC5jcmVhdGUoKTsKY29uc3QgZm9udD1hd2FpdCBkb2NPdXQuZW1iZWRGb250KFBERkxpYi5TdGFuZGFyZEZvbnRzLkhlbHZldGljYSk7CmNvbnN0IGJvbGQ9YXdhaXQgZG9jT3V0LmVtYmVkRm9udChQREZMaWIuU3RhbmRhcmRGb250cy5IZWx2ZXRpY2FCb2xkKTsKY29uc3QgaW5jbHVkZUZpbGw9JCgncGRmRmlsbCcpLmNoZWNrZWQsaW5jbHVkZUxpbmVzPSQoJ3BkZkxpbmVzJykuY2hlY2tlZCxpbmNsdWRlTGFiZWxzPSQoJ3BkZkxhYmVscycpLmNoZWNrZWQsaW5jbHVkZVN1bW1hcnk9JCgncGRmU3VtbWFyeScpLmNoZWNrZWQ7CmNvbnN0IGFsbFBhZ2VzPUFycmF5LmZyb20oe2xlbmd0aDpwZGYubnVtUGFnZXN9LChfLGkpPT5pKzEpOwpmb3IobGV0IGl4PTA7aXg8YWxsUGFnZXMubGVuZ3RoO2l4KyspewogY29uc3QgcGRmUGFnZT1hd2FpdCBwZGYuZ2V0UGFnZShpeCsxKTsKIGNvbnN0IHZpZXdwb3J0PXBkZlBhZ2UuZ2V0Vmlld3BvcnQoe3NjYWxlOjJ9KTsKIGNvbnN0IGN2PWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2NhbnZhcycpO2N2LndpZHRoPU1hdGguY2VpbCh2aWV3cG9ydC53aWR0aCk7Y3YuaGVpZ2h0PU1hdGguY2VpbCh2aWV3cG9ydC5oZWlnaHQpOwogY29uc3QgY3R4PWN2LmdldENvbnRleHQoJzJkJyk7Y3R4LmZpbGxTdHlsZT0nI2ZmZic7Y3R4LmZpbGxSZWN0KDAsMCxjdi53aWR0aCxjdi5oZWlnaHQpOwogYXdhaXQgcGRmUGFnZS5yZW5kZXIoe2NhbnZhc0NvbnRleHQ6Y3R4LHZpZXdwb3J0fSkucHJvbWlzZTsKIGNvbnN0IHN4PWN2LndpZHRoLHN5PWN2LmhlaWdodDsKIGZvcihjb25zdCBtIG9mIG1lYXN1cmVtZW50cy5maWx0ZXIobT0+bS5wYWdlPT09aXgrMSkpewogIGNvbnN0IHB0cz1tLnBvaW50cy5tYXAocD0+KHt4OnAueCpzeCx5OnAueSpzeX0pKTtjb25zdCBjb2w9Y29sb3IobS5ncm91cElkKTsKICBjdHguc2F2ZSgpO2N0eC5iZWdpblBhdGgoKTtwdHMuZm9yRWFjaCgocCxpKT0+aT9jdHgubGluZVRvKHAueCxwLnkpOmN0eC5tb3ZlVG8ocC54LHAueSkpOwogIGlmKG0udHlwZT09PSdhcmVhJyYmcHRzLmxlbmd0aD49Myl7Y3R4LmNsb3NlUGF0aCgpO2lmKGluY2x1ZGVGaWxsKXtjdHguZ2xvYmFsQWxwaGE9LjI4O2N0eC5maWxsU3R5bGU9Y29sO2N0eC5maWxsKCk7Y3R4Lmdsb2JhbEFscGhhPTF9fQogIGlmKGluY2x1ZGVMaW5lcyl7Y3R4LnN0cm9rZVN0eWxlPWNvbDtjdHgubGluZVdpZHRoPTMuNTtjdHgubGluZUpvaW49J3JvdW5kJztjdHguc3Ryb2tlKCk7Zm9yKGNvbnN0IHAgb2YgcHRzKXtjdHguYmVnaW5QYXRoKCk7Y3R4LmFyYyhwLngscC55LDQsMCxNYXRoLlBJKjIpO2N0eC5maWxsU3R5bGU9Y29sO2N0eC5maWxsKCl9fQogIGlmKGluY2x1ZGVMYWJlbHMpewogICBjb25zdCBhdD1sYWJlbFBvaW50KHB0cyxtLnR5cGUpLGxhYmVsPW1lYXN1cmVMYWJlbChtLG1lYXN1cmVWYWx1ZShtKSk7CiAgIGN0eC5mb250PSdib2xkIDIxcHggQXJpYWwsIHNhbnMtc2VyaWYnO2NvbnN0IHR3PWN0eC5tZWFzdXJlVGV4dChsYWJlbCkud2lkdGgscGFkPTk7CiAgIGNvbnN0IGx4PU1hdGgubWF4KHR3LzIrcGFkLE1hdGgubWluKHN4LXR3LzItcGFkLGF0LngpKTtjb25zdCBseT1NYXRoLm1heCgyMCxNYXRoLm1pbihzeS0yMCxhdC55KSk7CiAgIGN0eC5maWxsU3R5bGU9J3JnYmEoMjU1LDI1NSwyNTUsLjk2KSc7Y3R4LnN0cm9rZVN0eWxlPWNvbDtjdHgubGluZVdpZHRoPTI7CiAgIGN0eC5maWxsUmVjdChseC10dy8yLXBhZCxseS0xOSx0dytwYWQqMiwzMSk7Y3R4LnN0cm9rZVJlY3QobHgtdHcvMi1wYWQsbHktMTksdHcrcGFkKjIsMzEpOwogICBjdHguZmlsbFN0eWxlPScjMTcyYjJhJztjdHgudGV4dEFsaWduPSdjZW50ZXInO2N0eC50ZXh0QmFzZWxpbmU9J21pZGRsZSc7Y3R4LmZpbGxUZXh0KGxhYmVsLGx4LGx5LTMpOwogIH0KICBjdHgucmVzdG9yZSgpOwogfQogY29uc3QgcG5nPWF3YWl0IGRvY091dC5lbWJlZFBuZyhjdi50b0RhdGFVUkwoJ2ltYWdlL3BuZycpKTsKIGNvbnN0IHdpZHRoPXZpZXdwb3J0LndpZHRoLzIsaGVpZ2h0PXZpZXdwb3J0LmhlaWdodC8yOwogY29uc3Qgb3V0cHV0UGFnZT1kb2NPdXQuYWRkUGFnZShbd2lkdGgsaGVpZ2h0XSk7b3V0cHV0UGFnZS5kcmF3SW1hZ2UocG5nLHt4OjAseTowLHdpZHRoLGhlaWdodH0pOwp9Ci8vIFN1bW1hcnk6IGdyb3VwIGJ5IG5hbWUgYW5kIHVuaXQsIG5ldmVyIGFkZCBtZXRlcnMgYW5kIHNxdWFyZSBtZXRlcnMgdG9nZXRoZXIuCmlmKGluY2x1ZGVTdW1tYXJ5KXtjb25zdCBlbnRyaWVzPVtdO2Zvcihjb25zdCBnIG9mIGdyb3Vwcyl7Zm9yKGNvbnN0IHR5cGUgb2YgWydsZW5ndGgnLCdhcmVhJ10pe2NvbnN0IG1zPW1lYXN1cmVtZW50cy5maWx0ZXIobT0+bS5ncm91cElkPT09Zy5pZCYmbS50eXBlPT09dHlwZSk7aWYobXMubGVuZ3RoKWVudHJpZXMucHVzaCh7bmFtZTpnLm5hbWUsdHlwZSxzdW06bXMucmVkdWNlKChhLG0pPT5hK3RvdGFsVmFsdWUobSksMCksY291bnQ6bXMubGVuZ3RofSl9fQpjb25zdCBXPTU5NSxIPTg0MixtYXJnaW49NDg7bGV0IHN1bW1hcnk9ZG9jT3V0LmFkZFBhZ2UoW1csSF0pLHk9SC02NTtjb25zdCBkYXJrPVBERkxpYi5yZ2IoLjE2LC4xOCwuMTgpLG11dGVkPVBERkxpYi5yZ2IoLjQ1LC40NSwuNDUpO2Z1bmN0aW9uIG5leHRQYWdlKCl7c3VtbWFyeT1kb2NPdXQuYWRkUGFnZShbVyxIXSk7eT1ILTU1fWZ1bmN0aW9uIHRleHRMaW5lKHN0cixzaXplPTExLGlzQm9sZD1mYWxzZSl7aWYoeTw2NSluZXh0UGFnZSgpO3N1bW1hcnkuZHJhd1RleHQoc2FmZVBkZlRleHQoc3RyKSx7eDptYXJnaW4seSxzaXplLGZvbnQ6aXNCb2xkP2JvbGQ6Zm9udCxjb2xvcjpkYXJrfSk7eS09c2l6ZSsxMX0KdGV4dExpbmUoJ1RJRExFVFQgLSBNw4ROR0RTQU1NQU5TVMOETExOSU5HJywxOCx0cnVlKTt5LT0xMDt0ZXh0TGluZSgnUml0bmluZzogJysocGRmS2V5LnNwbGl0KCctJykuc2xpY2UoMCwtMSkuam9pbignLScpfHxwZGZLZXkpLDEwKTt0ZXh0TGluZSgnRGF0dW06ICcrbmV3IERhdGUoKS50b0xvY2FsZURhdGVTdHJpbmcoJ3N2LVNFJyksMTApO3RleHRMaW5lKCdBbnRhbCByaXRuaW5nc3NpZG9yOiAnK2FsbFBhZ2VzLmxlbmd0aCwxMCk7eS09MTQ7CnRleHRMaW5lKCdNw6R0Z3J1cHAnLDEyLHRydWUpO3N1bW1hcnkuZHJhd1RleHQoJ1RvdGFsJyx7eDo0MjUseTp5KzIzLHNpemU6MTIsZm9udDpib2xkLGNvbG9yOmRhcmt9KTtzdW1tYXJ5LmRyYXdMaW5lKHtzdGFydDp7eDptYXJnaW4seTp5KzExfSxlbmQ6e3g6Vy1tYXJnaW4seTp5KzExfSx0aGlja25lc3M6MSxjb2xvcjptdXRlZH0pO2Zvcihjb25zdCBlIG9mIGVudHJpZXMpe2lmKHk8NjUpbmV4dFBhZ2UoKTtjb25zdCBuYW1lPXNhZmVQZGZUZXh0KGUubmFtZSkrKGUudHlwZT09PSdhcmVhJz8nIChhcmVhKSc6JyAobMOkbmdkKScpO3N1bW1hcnkuZHJhd1RleHQobmFtZS5zbGljZSgwLDUzKSx7eDptYXJnaW4seSxzaXplOjExLGZvbnQsY29sb3I6ZGFya30pO3N1bW1hcnkuZHJhd1RleHQoc2FmZVBkZlRleHQoZm9ybWF0TWVhc3VyZShlLnN1bSxlLnR5cGUpKSx7eDo0MjUseSxzaXplOjExLGZvbnQ6Ym9sZCxjb2xvcjpkYXJrfSk7eS09MjR9aWYoIWVudHJpZXMubGVuZ3RoKXRleHRMaW5lKCdJbmdhIG3DpHRuaW5nYXIgc3BhcmFkZS4nLDExKTt5LT0xNTsKY29uc3QgbGVuZ3RoPWVudHJpZXMuZmlsdGVyKGU9PmUudHlwZT09PSdsZW5ndGgnKS5yZWR1Y2UoKHMsZSk9PnMrZS5zdW0sMCksYXJlYT1lbnRyaWVzLmZpbHRlcihlPT5lLnR5cGU9PT0nYXJlYScpLnJlZHVjZSgocyxlKT0+cytlLnN1bSwwKTt0ZXh0TGluZSgnVG90YWwgbMOkbmdkOiAnK2Zvcm1hdE1lYXN1cmUobGVuZ3RoLCdsZW5ndGgnKSwxMix0cnVlKTt0ZXh0TGluZSgnVG90YWwgYXJlYTogJytmbXQoYXJlYSkrJyBtwrInLDEyLHRydWUpO3ktPTEyO3RleHRMaW5lKCdTa2FsYToga2FsaWJyZXJhZCBwZXIgcml0bmluZ3NzaWRhLicsMTApO3RleHRMaW5lKCdLb250cm9sbGVyYSBhbGx0aWQgbcOldHRlbiBtb3Qgcml0bmluZ2VucyBtw6V0dHPDpHR0bmluZy4nLDEwKTt9CmNvbnN0IG91dD1hd2FpdCBkb2NPdXQuc2F2ZSgpO2NvbnN0IGJsb2I9bmV3IEJsb2IoW291dF0se3R5cGU6J2FwcGxpY2F0aW9uL3BkZid9KSx1cmw9VVJMLmNyZWF0ZU9iamVjdFVSTChibG9iKSxhPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2EnKTthLmhyZWY9dXJsO2EuZG93bmxvYWQ9J1RJRExFVFRfbWFuZ2RuaW5nXycrKHBkZktleS5zcGxpdCgnLScpWzBdfHwncml0bmluZycpLnJlcGxhY2UoL1wucGRmJC9pLCcnKS5yZXBsYWNlKC9bXmEtekEtWjAtOV8tXS9nLCdfJykrJy5wZGYnO2EuY2xpY2soKTtzZXRUaW1lb3V0KCgpPT5VUkwucmV2b2tlT2JqZWN0VVJMKHVybCksNTAwMCk7c3RhdHVzKCdQREYgZXhwb3J0ZXJhZCBtZWQgdmFsZGEgcml0bmluZ3NtYXJrZXJpbmdhcicrKGluY2x1ZGVTdW1tYXJ5Pycgb2NoIHNhbW1hbnN0w6RsbG5pbmcuJzonLicpKTt9Y2F0Y2goZSl7Y29uc29sZS5lcnJvcihlKTthbGVydCgnUERGLWV4cG9ydGVuIG1pc3NseWNrYWRlczogJytlLm1lc3NhZ2UpfWZpbmFsbHl7YnRuLmRpc2FibGVkPWZhbHNlO2J0bi50ZXh0Q29udGVudD0n4oaTIEV4cG9ydGVyYSBQREYnfX0KJCgnZXhwb3J0UGRmJykub25jbGljaz1leHBvcnRQZGY7CiQoJ2ltcG9ydEpzb24nKS5vbmNoYW5nZT1hc3luYyBlPT57Y29uc3QgZmlsZT1lLnRhcmdldC5maWxlc1swXTtpZighZmlsZSlyZXR1cm47aWYoIXBkZilyZXR1cm4gYWxlcnQoJ8OWcHBuYSBvcmlnaW5hbHJpdG5pbmdlbiBpbm5hbiBkdSBsw6RzZXIgaW4gYXJiZXRzZmlsZW4uJyk7dHJ5e2NvbnN0IGQ9SlNPTi5wYXJzZShhd2FpdCBmaWxlLnRleHQoKSk7aWYoIUFycmF5LmlzQXJyYXkoZC5tZWFzdXJlbWVudHMpfHwhQXJyYXkuaXNBcnJheShkLmdyb3VwcykpdGhyb3cgRXJyb3IoJ09naWx0aWcgYXJiZXRzZmlsJyk7aWYoZC5wZGZOYW1lIT09cGRmS2V5JiYhY29uZmlybSgnQXJiZXRzZmlsZW4gaMO2ciB0aWxsIGVuIGFubmFuIFBERi4gVmlsbCBkdSDDpG5kw6UgbMOkc2EgaW4gZGVuPycpKXJldHVybjtzbmFwc2hvdCgpO21lYXN1cmVtZW50cz1kLm1lYXN1cmVtZW50cztncm91cHM9ZC5ncm91cHM7Y2FsPWQuY2FsfHx7fTtncm91cElkPWdyb3Vwc1swXT8uaWR8fCcnO3NhdmUoKTtkcmF3TGlzdCgpO2RyYXdPdmVybGF5KCk7c3RhdHVzKCdBcmJldHNmaWxlbiDDpHIgaW5sw6RzdC4nKX1jYXRjaChlcnIpe2FsZXJ0KCdLdW5kZSBpbnRlIMO2cHBuYSBhcmJldHNmaWxlbjogJytlcnIubWVzc2FnZSl9ZmluYWxseXtlLnRhcmdldC52YWx1ZT0nJ319OwoKLy8gSW5kZXhlZERCIHN0b3JlcyBhY3R1YWwgUERGIGJ5dGVzIHNvIGZpbGVzIGNhbiBiZSByZW9wZW5lZCB3aXRob3V0IHVwbG9hZGluZyBhZ2Fpbi4KY29uc3QgREJfTkFNRT0ndGlkbGV0dC1yaXRuaW5nc2JpYmxpb3Rlay12MSc7bGV0IGRiUHJvbWlzZTsKZnVuY3Rpb24gZGIoKXtpZighZGJQcm9taXNlKWRiUHJvbWlzZT1uZXcgUHJvbWlzZSgocmVzb2x2ZSxyZWplY3QpPT57Y29uc3Qgcj1pbmRleGVkREIub3BlbihEQl9OQU1FLDIpO3Iub251cGdyYWRlbmVlZGVkPSgpPT57Y29uc3QgZD1yLnJlc3VsdDtpZighZC5vYmplY3RTdG9yZU5hbWVzLmNvbnRhaW5zKCdmb2xkZXJzJykpZC5jcmVhdGVPYmplY3RTdG9yZSgnZm9sZGVycycse2tleVBhdGg6J2lkJ30pO2lmKCFkLm9iamVjdFN0b3JlTmFtZXMuY29udGFpbnMoJ2RyYXdpbmdzJykpZC5jcmVhdGVPYmplY3RTdG9yZSgnZHJhd2luZ3MnLHtrZXlQYXRoOidpZCd9KTtpZighZC5vYmplY3RTdG9yZU5hbWVzLmNvbnRhaW5zKCdzdW1tYXJpZXMnKSlkLmNyZWF0ZU9iamVjdFN0b3JlKCdzdW1tYXJpZXMnLHtrZXlQYXRoOidpZCd9KX07ci5vbnN1Y2Nlc3M9KCk9PnJlc29sdmUoci5yZXN1bHQpO3Iub25lcnJvcj0oKT0+cmVqZWN0KHIuZXJyb3IpfSk7cmV0dXJuIGRiUHJvbWlzZX0KYXN5bmMgZnVuY3Rpb24gc3RvcmVBY3Rpb24oc3RvcmUsbW9kZSxhY3Rpb24pe2NvbnN0IGQ9YXdhaXQgZGIoKTtyZXR1cm4gbmV3IFByb21pc2UoKHJlc29sdmUscmVqZWN0KT0+e2NvbnN0IHR4PWQudHJhbnNhY3Rpb24oc3RvcmUsbW9kZSksb3M9dHgub2JqZWN0U3RvcmUoc3RvcmUpO2xldCByZXE7dHJ5e3JlcT1hY3Rpb24ob3MpfWNhdGNoKGUpe3JlamVjdChlKTtyZXR1cm59cmVxLm9uc3VjY2Vzcz0oKT0+cmVzb2x2ZShyZXEucmVzdWx0KTtyZXEub25lcnJvcj0oKT0+cmVqZWN0KHJlcS5lcnJvcik7dHgub25lcnJvcj0oKT0+cmVqZWN0KHR4LmVycm9yKX0pfQpjb25zdCBnZXRBbGw9c3RvcmU9PnN0b3JlQWN0aW9uKHN0b3JlLCdyZWFkb25seScsb3M9Pm9zLmdldEFsbCgpKTsKY29uc3QgZ2V0T25lPShzdG9yZSxpZCk9PnN0b3JlQWN0aW9uKHN0b3JlLCdyZWFkb25seScsb3M9Pm9zLmdldChpZCkpOwpjb25zdCBwdXRPbmU9KHN0b3JlLGVudHJ5KT0+c3RvcmVBY3Rpb24oc3RvcmUsJ3JlYWR3cml0ZScsb3M9Pm9zLnB1dChlbnRyeSkpOwpjb25zdCBkZWxldGVPbmU9KHN0b3JlLGlkKT0+c3RvcmVBY3Rpb24oc3RvcmUsJ3JlYWR3cml0ZScsb3M9Pm9zLmRlbGV0ZShpZCkpOwpjb25zdCBuZXdJZD0oKT0+RGF0ZS5ub3coKS50b1N0cmluZygzNikrJy0nK01hdGgucmFuZG9tKCkudG9TdHJpbmcoMzYpLnNsaWNlKDIpOwpmdW5jdGlvbiBsb2NhbERhdGUodil7cmV0dXJuIHY/bmV3IERhdGUodikudG9Mb2NhbGVTdHJpbmcoJ3N2LVNFJyx7ZGF0ZVN0eWxlOidzaG9ydCcsdGltZVN0eWxlOidzaG9ydCd9KTon4oCTJ30KYXN5bmMgZnVuY3Rpb24gdG91Y2hEcmF3aW5nKGlkKXt0cnl7Y29uc3QgZD1hd2FpdCBnZXRPbmUoJ2RyYXdpbmdzJyxpZCk7aWYoZCl7ZC51cGRhdGVkPURhdGUubm93KCk7YXdhaXQgcHV0T25lKCdkcmF3aW5ncycsZCl9fWNhdGNoKGUpe2NvbnNvbGUud2FybignS3VuZGUgaW50ZSB1cHBkYXRlcmEgZmlsZW5zIGRhdHVtJyxlKX19CmFzeW5jIGZ1bmN0aW9uIGVuc3VyZUZvbGRlcigpe2xldCBmcz1hd2FpdCBnZXRBbGwoJ2ZvbGRlcnMnKTtpZighZnMubGVuZ3RoKXtjb25zdCBmPXtpZDpuZXdJZCgpLG5hbWU6J01pbmEgcml0bmluZ2FyJyxjcmVhdGVkOkRhdGUubm93KCl9O2F3YWl0IHB1dE9uZSgnZm9sZGVycycsZik7ZnM9W2ZdfWlmKCFmcy5zb21lKGY9PmYuaWQ9PT1hY3RpdmVGb2xkZXJJZCkpYWN0aXZlRm9sZGVySWQ9ZnNbMF0uaWQ7cmV0dXJuIGZzfQovLyB2MTc6IHN1bW1hcnkgUERGIGlzIGdlbmVyYXRlZCBmcm9tIHNhdmVkIGRhdGEsIG5vdCBmcm9tIHRoZSBjdXJyZW50bHkgdmlzaWJsZSBwYWdlLgpmdW5jdGlvbiBmb2xkZXJNZWFzdXJlbWVudHMoZHJhd2luZyl7Y29uc3Qga2V5PWRyYXdpbmcubmFtZSsnLScrZHJhd2luZy5zaXplO3RyeXtjb25zdCBzYXZlZD1KU09OLnBhcnNlKGxvY2FsU3RvcmFnZS5nZXRJdGVtKCd0aWRsZXR0LW1lYXN1cmUtJytrZXkpfHwne30nKTtyZXR1cm4ge2dyb3VwczpzYXZlZC5ncm91cHN8fFtdLG1lYXN1cmVtZW50czpzYXZlZC5tZWFzdXJlbWVudHN8fFtdLGNhbDpzYXZlZC5jYWx8fHt9fX1jYXRjaHtyZXR1cm4ge2dyb3VwczpbXSxtZWFzdXJlbWVudHM6W10sY2FsOnt9fX19CmZ1bmN0aW9uIG1lYXN1cmVGb3JTYXZlZChtLGNhbGlicmF0aW9uKXtjb25zdCBjPWNhbGlicmF0aW9uW20ucGFnZV0sZj1OdW1iZXIoYyYmdHlwZW9mIGM9PT0nb2JqZWN0Jz9jLmZhY3RvcjpjfHwwKTtpZighZilyZXR1cm4gbnVsbDtjb25zdCBhcj1OdW1iZXIoYyYmdHlwZW9mIGM9PT0nb2JqZWN0Jz9jLmFzcGVjdDoxKXx8MTtjb25zdCBwdHM9bS5wb2ludHN8fFtdO2lmKG0udHlwZT09PSdsZW5ndGgnKXJldHVybiBwdHMuc2xpY2UoMSkucmVkdWNlKChzdW0scCxpKT0+c3VtK01hdGguaHlwb3QoKHAueC1wdHNbaV0ueCkqYXIscC55LXB0c1tpXS55KSwwKSpmKm11bHRpcGxpZXIobSk7aWYobS50eXBlPT09J2FyZWEnJiZwdHMubGVuZ3RoPj0zKXJldHVybiBNYXRoLmFicyhwdHMucmVkdWNlKChzdW0scCxpKT0+c3VtK3AueCpwdHNbKGkrMSklcHRzLmxlbmd0aF0ueS1wdHNbKGkrMSklcHRzLmxlbmd0aF0ueCpwLnksMCkpKmFyLzIqZipmKm11bHRpcGxpZXIobSk7cmV0dXJuIG51bGx9CmZ1bmN0aW9uIGNvbGxlY3RSb3dzKGRyYXdpbmdzKXtjb25zdCByb3dzPVtdLGlzc3Vlcz1bXTtmb3IoY29uc3QgZHJhd2luZyBvZiBkcmF3aW5ncyl7Y29uc3QgZD1mb2xkZXJNZWFzdXJlbWVudHMoZHJhd2luZyk7Zm9yKGNvbnN0IGcgb2YgZC5ncm91cHMpe2Zvcihjb25zdCB0eXBlIG9mIFsnbGVuZ3RoJywnYXJlYSddKXtjb25zdCBtcz1kLm1lYXN1cmVtZW50cy5maWx0ZXIobT0+bS5ncm91cElkPT09Zy5pZCYmbS50eXBlPT09dHlwZSk7aWYoIW1zLmxlbmd0aCljb250aW51ZTtsZXQgc3VtPTAsY291bnQ9MDtmb3IoY29uc3QgbSBvZiBtcyl7Y29uc3Qgdj1tZWFzdXJlRm9yU2F2ZWQobSxkLmNhbCk7aWYodj09PW51bGwpe2lzc3Vlcy5wdXNoKGRyYXdpbmcubmFtZSsnIOKAkyBzaWRhICcrbS5wYWdlKTtjb250aW51ZX1zdW0rPXY7Y291bnQrK31pZihjb3VudClyb3dzLnB1c2goe2RyYXdpbmc6ZHJhd2luZy5uYW1lLGdyb3VwOmcubmFtZSx0eXBlLHN1bSxjb3VudH0pfX19cmV0dXJuIHtyb3dzLGlzc3Vlc319CmFzeW5jIGZ1bmN0aW9uIGNyZWF0ZVN1bW1hcnlQZGYoZHJhd2luZ3MsdGl0bGUpe2lmKCF3aW5kb3cuUERGTGliKXRocm93IEVycm9yKCdQREYtYmlibGlvdGVrZXQga3VuZGUgaW50ZSBsYWRkYXMuJyk7Y29uc3Qge3Jvd3MsaXNzdWVzfT1jb2xsZWN0Um93cyhkcmF3aW5ncyk7aWYoIXJvd3MubGVuZ3RoKXRocm93IEVycm9yKCdJbmdhIGthbGlicmVyYWRlIG3DpHRuaW5nYXIgaGl0dGFkZXMgaSBkZSB2YWxkYSByaXRuaW5nYXJuYS4nKTtpZihpc3N1ZXMubGVuZ3RoJiYhY29uZmlybShpc3N1ZXMubGVuZ3RoKycgbcOkdG5pbmdhciBzYWtuYXIga2FsaWJyZXJpbmcgb2NoIGtvbW1lciBpbnRlIG1lZC4gRm9ydHPDpHR0YT8nKSlyZXR1cm4gbnVsbDtjb25zdCBvdXQ9YXdhaXQgUERGTGliLlBERkRvY3VtZW50LmNyZWF0ZSgpLG5vcm1hbD1hd2FpdCBvdXQuZW1iZWRGb250KFBERkxpYi5TdGFuZGFyZEZvbnRzLkhlbHZldGljYSksYm9sZD1hd2FpdCBvdXQuZW1iZWRGb250KFBERkxpYi5TdGFuZGFyZEZvbnRzLkhlbHZldGljYUJvbGQpO2NvbnN0IFc9NTk1LEg9ODQyLG1hcmdpbj00MjtsZXQgcGcseTtjb25zdCBpbms9UERGTGliLnJnYiguMTUsLjIxLC4yNiksZ3JleT1QREZMaWIucmdiKC40NCwuNDksLjUzKTtmdW5jdGlvbiBuZXdQYWdlKCl7cGc9b3V0LmFkZFBhZ2UoW1csSF0pO3k9SC01NX1mdW5jdGlvbiBsaW5lKHQsc2l6ZT0xMCxiPWZhbHNlLHg9bWFyZ2luKXtpZih5PDY1KW5ld1BhZ2UoKTtwZy5kcmF3VGV4dChzYWZlUGRmVGV4dCh0KSx7eCx5LHNpemUsZm9udDpiP2JvbGQ6bm9ybWFsLGNvbG9yOmlua30pO3ktPXNpemUrOX1uZXdQYWdlKCk7bGluZSgnVElETEVUVCAtIE3DhE5HRFNBTU1BTlNUw4RMTE5JTkcnLDE3LHRydWUpO2xpbmUoJ1JpdG5pbmdzcHJvamVrdDogJyt0aXRsZSwxMSx0cnVlKTtsaW5lKCdEYXR1bTogJytuZXcgRGF0ZSgpLnRvTG9jYWxlRGF0ZVN0cmluZygnc3YtU0UnKSwxMCk7bGluZSgnQW50YWwgcml0bmluZ2FyOiAnK2RyYXdpbmdzLmxlbmd0aCwxMCk7eS09MTQ7Y29uc3QgdG90YWxzPXtsZW5ndGg6MCxhcmVhOjB9O2Zvcihjb25zdCBkcmF3aW5nIG9mIGRyYXdpbmdzKXtjb25zdCBkcj1yb3dzLmZpbHRlcihyPT5yLmRyYXdpbmc9PT1kcmF3aW5nLm5hbWUpO2lmKCFkci5sZW5ndGgpY29udGludWU7aWYoeTwxMTUpbmV3UGFnZSgpO2xpbmUoZHJhd2luZy5uYW1lLDEyLHRydWUpO2Zvcihjb25zdCByIG9mIGRyKXt0b3RhbHNbci50eXBlXSs9ci5zdW07Y29uc3QgbmFtZT1yLmdyb3VwLmxlbmd0aD4zNz9yLmdyb3VwLnNsaWNlKDAsMzQpKycuLi4nOnIuZ3JvdXA7bGluZSgnICAnK25hbWUrJyAoJytyLmNvdW50Kycgc3QpJywxMCxmYWxzZSk7eSs9MTk7cGcuZHJhd1RleHQoc2FmZVBkZlRleHQoZm9ybWF0TWVhc3VyZShyLnN1bSxyLnR5cGUpKSx7eDo0MzAseSxzaXplOjEwLGZvbnQ6Ym9sZCxjb2xvcjppbmt9KTt5LT0xOX15LT04fXktPTEyO2xpbmUoJ1RPVEFMIEzDhE5HRDogJytmb3JtYXRNZWFzdXJlKHRvdGFscy5sZW5ndGgsJ2xlbmd0aCcpLDEzLHRydWUpO2xpbmUoJ1RPVEFMIEFSRUE6ICcrZm9ybWF0TWVhc3VyZSh0b3RhbHMuYXJlYSwnYXJlYScpLDEzLHRydWUpO3ktPTEyO2xpbmUoJ0fDpW5nZXIgKMOXKSBpbmfDpXIgaSBzdW1tb3JuYS4nLDkpO2xpbmUoJ0tvbnRyb2xsZXJhIGFsbHRpZCBtw6RuZ2Rlcm5hIG1vdCBvcmlnaW5hbHJpdG5pbmdhcm5hLicsOSk7aWYoaXNzdWVzLmxlbmd0aClsaW5lKCdPYnM6ICcraXNzdWVzLmxlbmd0aCsnIG3DpHRuaW5nYXIgdXRhbiBrYWxpYnJlcmluZyBoYXIgdXRlbMOkbW5hdHMuJyw5KTtjb25zdCBieXRlcz1hd2FpdCBvdXQuc2F2ZSgpO3JldHVybiBuZXcgQmxvYihbYnl0ZXNdLHt0eXBlOidhcHBsaWNhdGlvbi9wZGYnfSl9CmZ1bmN0aW9uIGRvd25sb2FkQmxvYihibG9iLG5hbWUpe2NvbnN0IHVybD1VUkwuY3JlYXRlT2JqZWN0VVJMKGJsb2IpLGE9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnYScpO2EuaHJlZj11cmw7YS5kb3dubG9hZD1uYW1lO2EuY2xpY2soKTtzZXRUaW1lb3V0KCgpPT5VUkwucmV2b2tlT2JqZWN0VVJMKHVybCksMTUwMDApfQphc3luYyBmdW5jdGlvbiBmb2xkZXJDb250ZXh0KCl7Y29uc3QgZm9sZGVycz1hd2FpdCBlbnN1cmVGb2xkZXIoKSxmb2xkZXI9Zm9sZGVycy5maW5kKGY9PmYuaWQ9PT1hY3RpdmVGb2xkZXJJZCk7aWYoIWZvbGRlcil0aHJvdyBFcnJvcignVsOkbGogZW4gcml0bmluZ3NtYXBwIGbDtnJzdC4nKTtjb25zdCBkcmF3aW5ncz0oYXdhaXQgZ2V0QWxsKCdkcmF3aW5ncycpKS5maWx0ZXIoZD0+ZC5mb2xkZXJJZD09PWZvbGRlci5pZCk7aWYoIWRyYXdpbmdzLmxlbmd0aCl0aHJvdyBFcnJvcignUml0bmluZ3NtYXBwZW4gc2FrbmFyIHJpdG5pbmdhci4nKTtyZXR1cm4ge2ZvbGRlcixkcmF3aW5nc319CmFzeW5jIGZ1bmN0aW9uIHN1bW1hcnlGb3JBY3RpdmVGb2xkZXIoKXtjb25zdCB7Zm9sZGVyLGRyYXdpbmdzfT1hd2FpdCBmb2xkZXJDb250ZXh0KCk7Y29uc3QgYmxvYj1hd2FpdCBjcmVhdGVTdW1tYXJ5UGRmKGRyYXdpbmdzLGZvbGRlci5uYW1lKTtpZihibG9iKWRvd25sb2FkQmxvYihibG9iLCdUSURMRVRUX3NhbW1hbnN0YWxsbmluZ18nK2ZvbGRlci5uYW1lLnJlcGxhY2UoL1teYS16QS1aMC05Xy1dL2csJ18nKSsnLnBkZicpO3JldHVybiBibG9ifQokKCdmb2xkZXJTdW1tYXJ5Jykub25jbGljaz1hc3luYygpPT57dHJ5e2F3YWl0IHN1bW1hcnlGb3JBY3RpdmVGb2xkZXIoKX1jYXRjaChlKXthbGVydChlLm1lc3NhZ2UpfX07CiQoJ3N1bW1hcnlQZGYnKS5vbmNsaWNrPWFzeW5jKCk9Pnt0cnl7aWYoIW9yaWdpbmFsUGRmRmlsZSl0aHJvdyBFcnJvcignw5ZwcG5hIGVuIHJpdG5pbmcgZsO2cnN0LicpO2NvbnN0IGRyYXdpbmc9e25hbWU6b3JpZ2luYWxQZGZGaWxlLm5hbWUsc2l6ZTpvcmlnaW5hbFBkZkZpbGUuc2l6ZX07Y29uc3QgYmxvYj1hd2FpdCBjcmVhdGVTdW1tYXJ5UGRmKFtkcmF3aW5nXSxkcmF3aW5nLm5hbWUpO2lmKGJsb2IpZG93bmxvYWRCbG9iKGJsb2IsJ1RJRExFVFRfc2FtbWFuc3RhbGxuaW5nXycrZHJhd2luZy5uYW1lLnJlcGxhY2UoL1teYS16QS1aMC05Xy1dL2csJ18nKSsnLnBkZicpfWNhdGNoKGUpe2FsZXJ0KGUubWVzc2FnZSl9fTsKJCgnaW1wb3J0VG9Qcm9qZWN0Jykub25jbGljaz0oKT0+eyQoJ3Byb2plY3RJbXBvcnRQYW5lbCcpLmhpZGRlbj0hJCgncHJvamVjdEltcG9ydFBhbmVsJykuaGlkZGVuO3JlZnJlc2hUYXJnZXRQcm9qZWN0cygpfTsKZnVuY3Rpb24gcmVmcmVzaFRhcmdldFByb2plY3RzKCl7Y29uc3QgdGFyZ2V0PSQoJ3RhcmdldFByb2plY3QnKSxzZWxlY3RlZD10YXJnZXQudmFsdWU7dGFyZ2V0LnJlcGxhY2VDaGlsZHJlbihuZXcgT3B0aW9uKCdWw6RsaiBwcm9qZWt0Li4uJywnJykpO3RyeXtjb25zdCBkPUpTT04ucGFyc2UobG9jYWxTdG9yYWdlLmdldEl0ZW0oJ3RpZGxldHQtZGVtby12MycpfHwne30nKTtmb3IoY29uc3QgcCBvZiAoZC5wcm9qZWN0c3x8W10pLmZpbHRlcihwPT4hcC5xdW90ZU9ubHkpKXRhcmdldC5hZGQobmV3IE9wdGlvbihwLm5hbWUsU3RyaW5nKHAuaWQpKSl9Y2F0Y2h7fXRhcmdldC52YWx1ZT1zZWxlY3RlZHx8JCgncHJvamVjdCcpLnZhbHVlfHwnJ30KYXN5bmMgZnVuY3Rpb24gc2hvd0xpbmtlZFN1bW1hcmllcygpe2NvbnN0IGJveD0kKCdsaW5rZWRTdW1tYXJpZXMnKTtib3gucmVwbGFjZUNoaWxkcmVuKCk7dHJ5e2NvbnN0IGFsbD1hd2FpdCBnZXRBbGwoJ3N1bW1hcmllcycpO2lmKCFhbGwubGVuZ3RoKXJldHVybjtjb25zdCBoZWFkaW5nPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ3AnKTtoZWFkaW5nLnRleHRDb250ZW50PSdTcGFyYWRlIHByb2pla3R1bmRlcmxhZyBpIHJpdG5pbmdzdmVya3R5Z2V0JztoZWFkaW5nLnN0eWxlLmZvbnRXZWlnaHQ9JzcwMCc7Ym94LmFwcGVuZChoZWFkaW5nKTtmb3IoY29uc3QgaXRlbSBvZiBhbGwuc29ydCgoYSxiKT0+Yi5jcmVhdGVkLWEuY3JlYXRlZCkpe2NvbnN0IHJvdz1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCdkaXYnKTtyb3cuY2xhc3NOYW1lPSdiZXR3ZWVuJztjb25zdCBuYW1lPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ3NwYW4nKTtuYW1lLnRleHRDb250ZW50PWl0ZW0ucHJvamVjdE5hbWUrJyDCtyAnK2l0ZW0uZm9sZGVyTmFtZSsnIMK3ICcrbG9jYWxEYXRlKGl0ZW0uY3JlYXRlZCk7Y29uc3QgZ2V0PWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2J1dHRvbicpO2dldC50ZXh0Q29udGVudD0nSMOkbXRhIFBERic7Z2V0Lm9uY2xpY2s9KCk9PmRvd25sb2FkQmxvYihpdGVtLmJsb2IsaXRlbS5maWxlbmFtZSk7Y29uc3QgZGVsPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2J1dHRvbicpO2RlbC50ZXh0Q29udGVudD0nVGEgYm9ydCc7ZGVsLm9uY2xpY2s9YXN5bmMoKT0+e2lmKCFjb25maXJtKCdUYSBib3J0IGRldHRhIHByb2pla3R1bmRlcmxhZz8nKSlyZXR1cm47YXdhaXQgZGVsZXRlT25lKCdzdW1tYXJpZXMnLGl0ZW0uaWQpO3Nob3dMaW5rZWRTdW1tYXJpZXMoKX07cm93LmFwcGVuZChuYW1lLGdldCxkZWwpO2JveC5hcHBlbmQocm93KX19Y2F0Y2goZSl7Ym94LnRleHRDb250ZW50PSdLdW5kZSBpbnRlIGzDpHNhIHByb2pla3R1bmRlcmxhZzogJytlLm1lc3NhZ2V9fQokKCdjb25maXJtSW1wb3J0Jykub25jbGljaz1hc3luYygpPT57dHJ5e2NvbnN0IGlkPSQoJ3RhcmdldFByb2plY3QnKS52YWx1ZTtpZighaWQpdGhyb3cgRXJyb3IoJ1bDpGxqIGV0dCBwcm9qZWt0LicpO2NvbnN0IHJhdz1KU09OLnBhcnNlKGxvY2FsU3RvcmFnZS5nZXRJdGVtKCd0aWRsZXR0LWRlbW8tdjMnKXx8J3t9JykscHJvamVjdD0ocmF3LnByb2plY3RzfHxbXSkuZmluZChwPT5TdHJpbmcocC5pZCk9PT1pZCk7aWYoIXByb2plY3QpdGhyb3cgRXJyb3IoJ1Byb2pla3RldCBmaW5ucyBpbnRlIGkgZGVuIGjDpHIgd2ViYmzDpHNhcmVucyBUSURMRVRULWRhdGEuJyk7Y29uc3Qge2ZvbGRlcixkcmF3aW5nc309YXdhaXQgZm9sZGVyQ29udGV4dCgpLGJsb2I9YXdhaXQgY3JlYXRlU3VtbWFyeVBkZihkcmF3aW5ncyxmb2xkZXIubmFtZSk7aWYoIWJsb2IpcmV0dXJuO2NvbnN0IGZpbGVuYW1lPSdUSURMRVRUX3NhbW1hbnN0YWxsbmluZ18nK2ZvbGRlci5uYW1lLnJlcGxhY2UoL1teYS16QS1aMC05Xy1dL2csJ18nKSsnLnBkZic7YXdhaXQgcHV0T25lKCdzdW1tYXJpZXMnLHtpZDpuZXdJZCgpLHByb2plY3RJZDppZCxwcm9qZWN0TmFtZTpwcm9qZWN0Lm5hbWUsZm9sZGVySWQ6Zm9sZGVyLmlkLGZvbGRlck5hbWU6Zm9sZGVyLm5hbWUsZmlsZW5hbWUsYmxvYixjcmVhdGVkOkRhdGUubm93KCl9KTskKCdwcm9qZWN0SW1wb3J0UGFuZWwnKS5oaWRkZW49dHJ1ZTthd2FpdCBzaG93TGlua2VkU3VtbWFyaWVzKCk7c3RhdHVzKCdTYW1tYW5zdMOkbGxuaW5nZW4gc3BhcmFkIGxva2FsdCBmw7ZyICcrcHJvamVjdC5uYW1lKycuIERlbiB2aXNhcyB1bmRlciBwcm9qZWt0dW5kZXJsYWcgaMOkciwgw6RubnUgaW50ZSBhdXRvbWF0aXNrdCBpIE9mZmVydCBpIGh1dnVkYXBwZW4uJyl9Y2F0Y2goZSl7YWxlcnQoJ0ltcG9ydCBtaXNzbHlja2FkZXM6ICcrZS5tZXNzYWdlKX19OwpzaG93TGlua2VkU3VtbWFyaWVzKCk7CmFzeW5jIGZ1bmN0aW9uIHNob3dEcmF3aW5ncygpe3RyeXsKIGNvbnN0IGZvbGRlcnM9YXdhaXQgZW5zdXJlRm9sZGVyKCksYWxsPWF3YWl0IGdldEFsbCgnZHJhd2luZ3MnKTtjb25zdCBmYz0kKCdmb2xkZXJDYXJkcycpLGxjPSQoJ2xpYnJhcnlGaWxlcycpO2ZjLnJlcGxhY2VDaGlsZHJlbigpO2xjLnJlcGxhY2VDaGlsZHJlbigpOwogJCgnbGlicmFyeUNvbnRlbnQnKS5oaWRkZW49IWxpYnJhcnlPcGVuOyQoJ2ZvbGRlclZpZXcnKS5oaWRkZW49Zm9sZGVyVmlld09wZW47JCgnZHJhd2luZ1ZpZXcnKS5oaWRkZW49IWZvbGRlclZpZXdPcGVuOyQoJ2JhY2tUb0ZvbGRlcnMnKS5oaWRkZW49IWZvbGRlclZpZXdPcGVuOwogJCgnbGlicmFyeVRpdGxlJykudGV4dENvbnRlbnQ9Zm9sZGVyVmlld09wZW4/KGZvbGRlcnMuZmluZChmPT5mLmlkPT09YWN0aXZlRm9sZGVySWQpPy5uYW1lfHwnUml0bmluZ2FyJyk6J1JpdG5pbmdzcHJvamVrdCc7CiAkKCdsaWJyYXJ5TG9jYXRpb24nKS50ZXh0Q29udGVudD1mb2xkZXJWaWV3T3Blbj8nw5ZwcGVuIG1hcHA6ICcrKGZvbGRlcnMuZmluZChmPT5mLmlkPT09YWN0aXZlRm9sZGVySWQpPy5uYW1lfHwnJyk6J1bDpGxqIGV0dCByaXRuaW5nc3Byb2pla3QnOwogJCgnc2hvd0xpYnJhcnknKS50ZXh0Q29udGVudD1saWJyYXJ5T3Blbj8n8J+TgSBNaW5hIHJpdG5pbmdhciDilrQnOifwn5OBIE1pbmEgcml0bmluZ2FyIOKWvic7CiBmb3IoY29uc3QgZm9sZGVyIG9mIGZvbGRlcnMpe2NvbnN0IGNvdW50PWFsbC5maWx0ZXIoZD0+ZC5mb2xkZXJJZD09PWZvbGRlci5pZCkubGVuZ3RoO2NvbnN0IGNhcmQ9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7Y2FyZC5jbGFzc05hbWU9J2ZpbGVDYXJkJztjb25zdCB0aXRsZT1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCdzdHJvbmcnKTt0aXRsZS50ZXh0Q29udGVudD0n8J+TgSAnK2ZvbGRlci5uYW1lO2NvbnN0IG1ldGE9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7bWV0YS5jbGFzc05hbWU9J211dGVkJzttZXRhLnRleHRDb250ZW50PWNvdW50Kycgcml0bmluZycrKGNvdW50PT09MT8nJzonYXInKTtjb25zdCBvcGVuPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2J1dHRvbicpO29wZW4udGV4dENvbnRlbnQ9J8OWcHBuYSBwcm9qZWt0JztvcGVuLm9uY2xpY2s9KCk9PnthY3RpdmVGb2xkZXJJZD1mb2xkZXIuaWQ7Zm9sZGVyVmlld09wZW49dHJ1ZTtsaWJyYXJ5T3Blbj10cnVlO3Nob3dEcmF3aW5ncygpfTtjb25zdCBkZWw9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnYnV0dG9uJyk7ZGVsLnRleHRDb250ZW50PSdUYSBib3J0JztkZWwub25jbGljaz0oKT0+cmVtb3ZlRm9sZGVyKGZvbGRlci5pZCk7Y2FyZC5hcHBlbmQodGl0bGUsbWV0YSxvcGVuLGRlbCk7ZmMuYXBwZW5kQ2hpbGQoY2FyZCl9CiBjb25zdCBmaWxlcz1hbGwuZmlsdGVyKGQ9PmQuZm9sZGVySWQ9PT1hY3RpdmVGb2xkZXJJZCkuc29ydCgoYSxiKT0+Yi51cGRhdGVkLWEudXBkYXRlZCk7CiBmb3IoY29uc3QgZmlsZSBvZiBmaWxlcyl7Y29uc3QgY2FyZD1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCdkaXYnKTtjYXJkLmNsYXNzTmFtZT0nZmlsZUNhcmQnO2NvbnN0IHRpdGxlPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ3N0cm9uZycpO3RpdGxlLnRleHRDb250ZW50PSfwn5OEICcrZmlsZS5uYW1lO2NvbnN0IG1ldGE9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7bWV0YS5jbGFzc05hbWU9J211dGVkJztjb25zdCBrZXk9ZmlsZS5uYW1lKyctJytmaWxlLnNpemU7bGV0IG49MDt0cnl7bj1KU09OLnBhcnNlKGxvY2FsU3RvcmFnZS5nZXRJdGVtKCd0aWRsZXR0LW1lYXN1cmUtJytrZXkpfHwne30nKS5tZWFzdXJlbWVudHM/Lmxlbmd0aHx8MH1jYXRjaHt9bWV0YS50ZXh0Q29udGVudD1uKycgc3BhcmFkZSBtw6V0dCDCtyBTZW5hc3QgJytsb2NhbERhdGUoZmlsZS51cGRhdGVkKTtjb25zdCBvcGVuPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2J1dHRvbicpO29wZW4udGV4dENvbnRlbnQ9J8OWcHBuYSAvIGZvcnRzw6R0dCc7b3Blbi5vbmNsaWNrPWFzeW5jKCk9Pnt0cnl7Y29uc3QgZD1hd2FpdCBnZXRPbmUoJ2RyYXdpbmdzJyxmaWxlLmlkKTthY3RpdmVEcmF3aW5nSWQ9ZC5pZDthd2FpdCBvcGVuRHJhd2luZ0ZpbGUobmV3IEZpbGUoW2QuYmxvYl0sZC5uYW1lLHt0eXBlOidhcHBsaWNhdGlvbi9wZGYnfSkpO2F3YWl0IHRvdWNoRHJhd2luZyhkLmlkKTthd2FpdCBzaG93RHJhd2luZ3MoKX1jYXRjaChlKXthbGVydCgnS3VuZGUgaW50ZSDDtnBwbmEgcml0bmluZ2VuOiAnK2UubWVzc2FnZSl9fTtjb25zdCBkZWw9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnYnV0dG9uJyk7ZGVsLnRleHRDb250ZW50PSdUYSBib3J0IHJpdG5pbmcnO2RlbC5vbmNsaWNrPWFzeW5jKCk9PntpZighY29uZmlybSgnVGEgYm9ydCAiJytmaWxlLm5hbWUrJyIgZnLDpW4gcml0bmluZ3Nwcm9qZWt0ZXQ/IE3DpHRuaW5nYXIgZmlubnMga3ZhciBpIHdlYmJsw6RzYXJlbnMgc8Oka2VyaGV0c2RhdGEuJykpcmV0dXJuO2F3YWl0IGRlbGV0ZU9uZSgnZHJhd2luZ3MnLGZpbGUuaWQpO2lmKGFjdGl2ZURyYXdpbmdJZD09PWZpbGUuaWQpe2FjdGl2ZURyYXdpbmdJZD1udWxsO2NsZWFyRHJhd2luZygpfWF3YWl0IHNob3dEcmF3aW5ncygpfTtjYXJkLmFwcGVuZCh0aXRsZSxtZXRhLG9wZW4sZGVsKTtsYy5hcHBlbmRDaGlsZChjYXJkKX0KIGlmKCFmaWxlcy5sZW5ndGgpe2NvbnN0IHA9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgncCcpO3AuY2xhc3NOYW1lPSdtdXRlZCc7cC50ZXh0Q29udGVudD0nSW5nYSByaXRuaW5nYXIgaMOkciDDpG5udS4gS2xpY2thIHDDpSArIEzDpGdnIHRpbGwgcml0bmluZy4nO2xjLmFwcGVuZENoaWxkKHApfQogfWNhdGNoKGUpeyQoJ2xpYnJhcnlGaWxlcycpLnRleHRDb250ZW50PSdLdW5kZSBpbnRlIGzDpHNhIGxva2FsdCBmaWxiaWJsaW90ZWs6ICcrZS5tZXNzYWdlfX0KZnVuY3Rpb24gY2xlYXJEcmF3aW5nKCl7cGRmPW51bGw7b3JpZ2luYWxQZGZGaWxlPW51bGw7cGRmS2V5PScnO21lYXN1cmVtZW50cz1bXTtncm91cHM9W107Y2FsPXt9O3BvaW50cz1bXTtwYWdlPTE7Y29uc3QgYz0kKCdjYW52YXMnKTtjLmdldENvbnRleHQoJzJkJykuY2xlYXJSZWN0KDAsMCxjLndpZHRoLGMuaGVpZ2h0KTskKCdvdmVybGF5JykucmVwbGFjZUNoaWxkcmVuKCk7JCgncGFnZUxhYmVsJykudGV4dENvbnRlbnQ9J1NpZGEg4oCTJztkcmF3TGlzdCgpO3N0YXR1cygnVsOkbGogZW4gcml0bmluZyBmw7ZyIGF0dCBmb3J0c8OkdHRhLicpfQphc3luYyBmdW5jdGlvbiByZW1vdmVGb2xkZXIoaWQpe2NvbnN0IGZvbGRlcnM9YXdhaXQgZ2V0QWxsKCdmb2xkZXJzJyksZm9sZGVyPWZvbGRlcnMuZmluZChmPT5mLmlkPT09aWQpO2lmKCFmb2xkZXIpcmV0dXJuO2NvbnN0IGZpbGVzPShhd2FpdCBnZXRBbGwoJ2RyYXdpbmdzJykpLmZpbHRlcihkPT5kLmZvbGRlcklkPT09aWQpO2lmKCFjb25maXJtKCdUYSBib3J0IHJpdG5pbmdzcHJvamVrdGV0ICInK2ZvbGRlci5uYW1lKyciIG9jaCBkZXNzICcrZmlsZXMubGVuZ3RoKycgcml0bmluZ2FyPyBEZXR0YSBnw6VyIGludGUgYXR0IMOlbmdyYS4gU3BhcmFkZSBtw6R0ZGF0YSBsw6RtbmFzIGt2YXIgc29tIHPDpGtlcmhldHNrb3BpYS4nKSlyZXR1cm47Zm9yKGNvbnN0IGZpbGUgb2YgZmlsZXMpe2F3YWl0IGRlbGV0ZU9uZSgnZHJhd2luZ3MnLGZpbGUuaWQpO2lmKGFjdGl2ZURyYXdpbmdJZD09PWZpbGUuaWQpe2FjdGl2ZURyYXdpbmdJZD1udWxsO2NsZWFyRHJhd2luZygpfX1hd2FpdCBkZWxldGVPbmUoJ2ZvbGRlcnMnLGlkKTtpZihhY3RpdmVGb2xkZXJJZD09PWlkKXthY3RpdmVGb2xkZXJJZD1udWxsO2ZvbGRlclZpZXdPcGVuPWZhbHNlfWF3YWl0IHNob3dEcmF3aW5ncygpfQokKCdjcmVhdGVGb2xkZXInKS5vbmNsaWNrPWFzeW5jKCk9Pntjb25zdCBuYW1lPSQoJ25ld0ZvbGRlck5hbWUnKS52YWx1ZS50cmltKCk7aWYoIW5hbWUpcmV0dXJuIGFsZXJ0KCdTa3JpdiBldHQgbmFtbiBww6Ugcml0bmluZ3Nwcm9qZWt0ZXQuJyk7dHJ5e2NvbnN0IGY9e2lkOm5ld0lkKCksbmFtZSxjcmVhdGVkOkRhdGUubm93KCl9O2F3YWl0IHB1dE9uZSgnZm9sZGVycycsZik7YWN0aXZlRm9sZGVySWQ9Zi5pZDtmb2xkZXJWaWV3T3Blbj10cnVlO2xpYnJhcnlPcGVuPXRydWU7JCgnbmV3Rm9sZGVyTmFtZScpLnZhbHVlPScnO2F3YWl0IHNob3dEcmF3aW5ncygpfWNhdGNoKGUpe2FsZXJ0KCdLdW5kZSBpbnRlIHNrYXBhIHJpdG5pbmdzcHJvamVrdGV0OiAnK2UubWVzc2FnZSl9fTsKJCgnc2hvd0xpYnJhcnknKS5vbmNsaWNrPSgpPT57bGlicmFyeU9wZW49IWxpYnJhcnlPcGVuO2lmKGxpYnJhcnlPcGVuKWZvbGRlclZpZXdPcGVuPWZhbHNlO3Nob3dEcmF3aW5ncygpfTsKJCgnYmFja1RvRm9sZGVycycpLm9uY2xpY2s9KCk9Pntmb2xkZXJWaWV3T3Blbj1mYWxzZTtzaG93RHJhd2luZ3MoKX07CiQoJ2RlbGV0ZUZvbGRlcicpLm9uY2xpY2s9KCk9PnJlbW92ZUZvbGRlcihhY3RpdmVGb2xkZXJJZCk7CiQoJ2xpYnJhcnlQZGYnKS5vbmNoYW5nZT1hc3luYyBlPT57Y29uc3QgZj1lLnRhcmdldC5maWxlc1swXTtpZighZilyZXR1cm47dHJ5e2F3YWl0IGFkZERyYXdpbmdUb0ZvbGRlcihmKX1jYXRjaChlcnIpe2FsZXJ0KCdLdW5kZSBpbnRlIGzDpGdnYSB0aWxsIHJpdG5pbmdlbjogJytlcnIubWVzc2FnZSl9ZmluYWxseXtlLnRhcmdldC52YWx1ZT0nJ319Owphc3luYyBmdW5jdGlvbiBhZGREcmF3aW5nVG9Gb2xkZXIoZil7YXdhaXQgZW5zdXJlRm9sZGVyKCk7Y29uc3QgYWxsPWF3YWl0IGdldEFsbCgnZHJhd2luZ3MnKTtsZXQgZD1hbGwuZmluZCh4PT54LmZvbGRlcklkPT09YWN0aXZlRm9sZGVySWQmJngubmFtZT09PWYubmFtZSYmeC5zaXplPT09Zi5zaXplKTtpZighZCl7ZD17aWQ6bmV3SWQoKSxmb2xkZXJJZDphY3RpdmVGb2xkZXJJZCxuYW1lOmYubmFtZSxzaXplOmYuc2l6ZSxibG9iOmYsdXBkYXRlZDpEYXRlLm5vdygpfTthd2FpdCBwdXRPbmUoJ2RyYXdpbmdzJyxkKX1hY3RpdmVEcmF3aW5nSWQ9ZC5pZDthd2FpdCBvcGVuRHJhd2luZ0ZpbGUoZik7Zm9sZGVyVmlld09wZW49dHJ1ZTtsaWJyYXJ5T3Blbj10cnVlO2F3YWl0IHNob3dEcmF3aW5ncygpfQokKCdwZGYnKS5vbmNoYW5nZT1hc3luYyBlPT57Y29uc3QgZj1lLnRhcmdldC5maWxlc1swXTtpZighZilyZXR1cm47dHJ5e2F3YWl0IGFkZERyYXdpbmdUb0ZvbGRlcihmKX1jYXRjaChlcnIpe2NvbnNvbGUuZXJyb3IoZXJyKTthbGVydCgnUERGLWZpbGVuIGt1bmRlIGludGUgc3BhcmFzOiAnK2Vyci5tZXNzYWdlKX1maW5hbGx5e2UudGFyZ2V0LnZhbHVlPScnfX07CnNob3dEcmF3aW5ncygpOwoKaW5pdFByb2plY3RzKCk7YWRkR3JvdXAoJ1V0c2lkYSBiYWxrJyk7dXBkYXRlVW5kb0J1dHRvbnMoKTt9KSgpOwo8L3NjcmlwdD48L2JvZHk+PC9odG1sPgo='))); function mountDrawing(){const frame=document.getElementById('tidlettDrawingFrame');if(frame&&!frame.srcdoc)frame.srcdoc=tidlettDrawingHtml} function openDrawingFullScreen(){const frame=document.getElementById('tidlettDrawingFrame');if(frame&&frame.requestFullscreen)frame.requestFullscreen().catch(()=>alert('Helskärmsläge kunde inte öppnas.'))} ueInit(); document.getElementById('viewRole').addEventListener('change',render);render();