Sto GAEB Studio v1.1.6

Professional Check · Vollbackup · Recovery · Sto Match · GAEB 3.2/3.3
Keine GAEB-Datei geöffnetGAEB DA XML 3.xBieter-Modus · X83→X84 geschütztXSD: nicht geprüft
📋

GAEB-Datei öffnen

Ziehe eine GAEB-DA-XML-Datei hier hinein oder wähle sie aus. GAEB Studio liest die LV-Struktur lokal im Browser – die Datei wird nicht hochgeladen.

Alle Dateien sind im macOS-Dateidialog auswählbar. Die GAEB-Prüfung erfolgt erst nach der Auswahl.
v1.1.6 Professional Check: Vollbackup/Wiederherstellung · automatische Recovery · offizielle XSD-Prüfung · Roundtrip · BVBS-orientierter Selbsttest.
OZLeistungMengeEinheitEP €GP €Status
0 Positionen
Angebotssumme 0,00 €
`} function pauschalPdfFileName(cfg){const base=(cfg.offerNo||cfg.objectName||state.meta.name||'Pauschalangebot').replace(/[\\/:*?"<>|]+/g,'-').replace(/\s+/g,' ').trim();return 'Pauschalangebot - '+base}function openPauschalPrint(cfg,action='preview'){const w=window.open('','_blank');if(!w){toast('Druckansicht wurde vom Browser blockiert. Pop-ups für diese Seite zulassen.');return}w.document.open();w.document.write(pauschalPrintDocument(cfg));w.document.close();try{w.document.title=pauschalPdfFileName(cfg)}catch{}if(action==='print'||action==='pdf'){const msg=action==='pdf'?'Im Druckdialog als Ziel „Als PDF speichern“ wählen.':'Druckdialog wird geöffnet.';setTimeout(()=>{try{const h=w.document.getElementById('pauPreviewHint');if(h)h.textContent=msg;w.focus();w.print()}catch{}},300)}} function pauschalSourceStatus(){const el=$('#pauSourceStatus');if(!el)return;const c=pauschalSourceCache?.storeKey===state.storeKey?pauschalSourceCache:null;if(c)el.innerHTML=`✅ Original-LV zugeordnet: ${esc(c.label)} · ${c.matches}/${c.total} Positionen sicher zugeordnet.`;else if(pauschalNeedsOriginal())el.innerHTML='⚠️ Für diese X84 fehlen Teile der ursprünglichen LV-Texte/Mengen. Ich habe lokal noch keine passende X83 gefunden. Bitte unten einmal die ursprüngliche X83 auswählen.';else el.innerHTML='✅ Das geladene LV enthält die notwendigen Texte und Mengen direkt.'} async function pauschalPdfModal(){if(!state.file)return;const c=loadPauschalSettings(),calc=pauschalCurrentTotal();$('#modalTitle').textContent='Pauschalangebot · PDF';$('#modalBody').innerHTML=`
Sicher getrennt: Diese Funktion liest das geladene LV nur aus. Die GAEB-Datei, X84-Exportlogik und Preise im Arbeitsstand werden nicht verändert.
Prüfe vollständigen LV-Inhalt …
Bei einer von GAEB Studio erzeugten X84 sucht das Programm zuerst automatisch im Professional-Projekt/Recovery nach der passenden X83.
Angebotssumme aus der geladenen GAEB-Datei: ${esc(money(calc))}. Die Pauschalsumme darf bewusst davon abweichen.
„PDF speichern“ öffnet den Edge/Chrome-Druckdialog gezielt für die PDF-Ausgabe. Dort als Ziel „Als PDF speichern“ wählen und nur noch den Speicherort bestätigen. Ein sinnvoller Dateiname wird vorbereitet. Positionspreise bleiben grundsätzlich ausgeblendet.
`;$('#modal').classList.add('open');$('#pauSourceFile').onchange=async e=>{const f=e.target.files?.[0];if(!f)return;try{const xml=decodeBuffer(await f.arrayBuffer());pauschalSetSource(xml,f.name);pauschalSourceStatus();toast('Original-LV für die PDF-Ausgabe zugeordnet.')}catch(err){toast(err.message||'Original-LV konnte nicht zugeordnet werden.')}};const makePauschalCfg=()=>{if(pauschalNeedsOriginal()&&!(pauschalSourceCache?.storeKey===state.storeKey)){toast('Für ein vollständiges PDF bitte zuerst die ursprüngliche X83 zuordnen.');return null}const sum=num($('#pauSum').value),vat=num($('#pauVat').value);if(sum==null||sum<0){toast('Bitte eine gültige Pauschalsumme eingeben.');return null}if(vat==null||vat<0||vat>100){toast('Bitte einen gültigen MwSt.-Satz eingeben.');return null}const cfg={title:$('#pauTitle').value.trim()||'Pauschalangebot',offerNo:$('#pauNo').value.trim(),date:$('#pauDate').value||new Date().toISOString().slice(0,10),customer:$('#pauCustomer').value.trim(),objectName:$('#pauObject').value.trim(),providerName:$('#pauProviderName').value.trim(),providerStreet:$('#pauProviderStreet').value.trim(),providerPCode:$('#pauProviderPCode').value.trim(),providerCity:$('#pauProviderCity').value.trim(),lumpSum:sum,vat,includeQty:$('#pauQty').checked,includeLong:$('#pauLong').checked,note:$('#pauNote').value.trim()};savePauschalSettings(cfg);return cfg};$('#pauPrintNow').onclick=()=>{const cfg=makePauschalCfg();if(cfg)openPauschalPrint(cfg,'print')};$('#pauSaveNow').onclick=()=>{const cfg=makePauschalCfg();if(cfg)openPauschalPrint(cfg,'pdf')};$('#pauPreview').onclick=()=>{const cfg=makePauschalCfg();if(cfg)openPauschalPrint(cfg,'preview')};const auto=await pauschalFindStoredOriginal();pauschalSourceStatus();if(auto)toast('Passende Original-X83 automatisch gefunden.')} function bidderModal(){ $('#modalTitle').textContent='Bieterdaten für X84'; const b=state.bidder||{}; $('#modalBody').innerHTML=`
Diese Angaben werden beim X84-Export als Bieter/CTR verwendet. Interne Arbeitsnotizen bleiben draußen.
`; $('#bidSave').onclick=()=>{state.bidder={name1:$('#bidName1').value.trim(),street:$('#bidStreet').value.trim(),pcode:$('#bidPCode').value.trim(),city:$('#bidCity').value.trim(),cntryType:$('#bidCntryType').value||'EEA'};saveBidder();$('#modal').classList.remove('open');toast('Bieterdaten gespeichert.')}; $('#modal').classList.add('open') } function renderTextComplementX84(tc,value){const v=String(value??'').trim();if(!v)return '';const attrs=[`MarkLbl="${xesc(tc.markLbl)}"`,`Kind="Bidder"`,tc.id?`ID="${xesc(tc.id)}"`:'',tc.artChrIdent?`ArtChrIdent="${xesc(tc.artChrIdent)}"`:''].filter(Boolean).join(' ');let typed='';if(tc.bodyType==='decimal'&&num(v)!=null)typed=``;else if(tc.bodyType==='integer'&&Number.isInteger(num(v)))typed=``;return `${typed}${xesc(v)}`} function renderBidderDescription(descriptionId,comps,valueFn){const bid=(comps||[]).filter(x=>/^bidder$/i.test(x.kind));if(!bid.length)return '';const body=bid.map(tc=>renderTextComplementX84(tc,valueFn(tc))).filter(Boolean).join('');if(!body)return '';return `Yes${body}`} function renderSubDescrX84(item){return (item.subDescrs||[]).map(sd=>{const q=effectiveSubQty(item,sd),p=effectiveSubPrice(item,sd);const desc=renderBidderDescription(sd.descriptionId,sd.bidderTextComplements||[],tc=>tcValue(item,tc,sd));return `${xesc(sd.subDNo)}${desc}${dec3(q)}${dec3(p)}`}).join('')} function renderMarkupX84(m){const basis=markupBasis(m),pct=markupPct(m),it=markupTotal(m),desc=renderBidderDescription(m.descriptionId,m.bidderTextComplements||[],tc=>markupTcValue(m,tc));return `${dec2(basis)}${Number(pct).toFixed(6)}${dec2(it)}${desc}`} function renderBidComm(text){if(!text)return '';const body=String(text).split(/\r?\n/).map(line=>`

${xesc(line)}

`).join('');return `${body}`} function buildX84Node(node,depth=0){ let body=''; for(const c of node.children||[]){const inner=buildX84Node(c,depth+1); const total=nodeTotal(c); body+=`${inner}${dec2(total)}`} const entries=(node.entries&&node.entries.length)?node.entries:(node.items||[]).map(i=>({kind:'item',ref:i}));const transferable=entries.filter(e=>e.kind==='item'||e.kind==='markup');if(transferable.length){body+='';for(const e0 of transferable){if(e0.kind==='markup'){body+=renderMarkupX84(e0.ref);continue}const i=e0.ref,p=effectivePrice(i),it=itemTotal(i),q=effectiveQty(i),e=state.edits[i.id]||{};const desc=renderBidderDescription(i.descriptionId,i.bidderTextComplements||[],tc=>tcValue(i,tc));body+=``;if(i.flags?.qtyTBD&&q!=null)body+=`${dec3(q)}`;body+=`${dec3(p)}${dec2(it)}${desc}${renderBidComm(e.bidComm||'')}${renderSubDescrX84(i)}`}body+=''} return body } function boQBreakdownXml(){if(state.meta.boQBreakdown?.length)return state.meta.boQBreakdown.map(b=>`${xesc(b.type||'Item')}${b.length?`${xesc(b.length)}`:''}${b.num?`${xesc(b.num)}`:''}${b.alignment?`${xesc(b.alignment)}`:''}`).join('');return 'Item4Yes'} function buildX84(){ const prof=gaebProfile();if(!prof)throw new Error(`GAEB-Version ${state.meta.version||'unbekannt'} wird für X84 nicht unterstützt.`); const total=offerTotal(); const b=state.bidder||{}; const project=(state.meta.name||state.file.name).slice(0,60); const boqName=state.meta.boqName||'LV'; const cur=(state.meta.currency||'EUR').toUpperCase()==='EUR'?'EUR':(state.meta.currency||'EUR').slice(0,4); const address=`
${xesc((b.name1||'').slice(0,40))}${xesc((b.street||'').slice(0,40))}${xesc((b.pcode||'').slice(0,20))}${xesc((b.city||'').slice(0,40))}
${xesc(b.cntryType==='Other'?'Other':'EEA')}
`; const awardInfo=`${xesc(cur)}${xesc((state.meta.curLbl||'Euro').slice(0,50))}${isoDate()}`; return ` ${prof.version}${prof.versDate}${isoDate()}Sto GAEB Studio v${APP_VERSION}GAEB Studio v${APP_VERSION}${xesc(project)}84${awardInfo}${address}${xesc(boqName.slice(0,20))}${boQBreakdownXml()}${dec2(total)}${buildX84Node(state.tree)}` } function validateX84(xml){ const problems=[];let d;try{d=new DOMParser().parseFromString(xml,'application/xml')}catch(e){problems.push('XML kann nicht geparst werden.');return problems} if(descendants(d.documentElement,'parsererror').length)problems.push('XML-Parserfehler.'); const badP=descendants(d.documentElement,'p').filter(p=>Array.from(p.childNodes||[]).some(n=>n.nodeType===3&&String(n.nodeValue||'').trim()));if(badP.length)problems.push(`${badP.length} formatierte Textabsätze enthalten unzulässigen Direkttext; Text muss in stehen.`); if(val(d.documentElement,['DP'])!=='84')problems.push('DP ist nicht 84.'); const prof=gaebProfile();if(!prof)problems.push(`GAEB-Version ${state.meta.version||'unbekannt'} wird nicht unterstützt.`);else{if(val(d.documentElement,['Version'])!==prof.version)problems.push(`Version ist nicht ${prof.version}.`);if(val(d.documentElement,['VersDate'])!==prof.versDate)problems.push(`VersDate ist nicht ${prof.versDate}.`);} const ps=val(d.documentElement,['ProgSystem']),pn=val(d.documentElement,['ProgName']);const expectedVersion='v'+APP_VERSION;if(!/Sto GAEB Studio/i.test(ps)||!ps.includes(expectedVersion)||!/GAEB Studio/i.test(pn)||!pn.includes(expectedVersion))problems.push(`Programminformation unvollständig (erwartet: Sto GAEB Studio ${expectedVersion}).`); const aw=firstDesc(d.documentElement,['Award']);const ai=aw?firstDirect(aw,['AwardInfo']):null;if(!ai||directVal(ai,['Cur'])!=='EUR')problems.push('Währung EUR fehlt in AwardInfo.'); const boq=aw?firstDirect(aw,['BoQ']):null,boqInfo=boq?firstDirect(boq,['BoQInfo']):null,boqTotals=boqInfo?firstDirect(boqInfo,['Totals']):null,totalOut=num(boqTotals?directVal(boqTotals,['Total']):null);if(totalOut==null||Math.abs(totalOut-offerTotal())>0.01)problems.push(`Angebotssumme stimmt nicht (${totalOut==null?'fehlt':totalOut} / ${offerTotal()}).`); const directTcMarks=(host)=>{const desc=host?firstDirect(host,['Description']):null;return desc?descendants(desc,'TextComplement').map(x=>attr(x,['MarkLbl'])):[]}; const items=descendants(d.documentElement,'Item'); if(items.length!==state.items.length)problems.push(`Positionsanzahl stimmt nicht (${items.length}/${state.items.length}).`); const byId=new Map(items.map(x=>[attr(x,['ID']),x])); for(const i of state.items){ const out=byId.get(i.elementId);if(!out){problems.push(`Positions-ID ${i.elementId||i.oz} fehlt im Export.`);continue} if((attr(out,['RNoIndex'])||'')!==(i.rnoIndex||''))problems.push(`OZ-Index bei ${i.oz} wurde nicht unverändert übernommen.`); if(i.flags?.qtyTBD){const q=num(directVal(out,['Qty']));if(q==null||Math.abs(q-effectiveQty(i))>0.0005)problems.push(`Freie Bietermenge bei ${i.oz} fehlt oder stimmt nicht.`)} const mainMarks=directTcMarks(out);for(const tc of i.bidderTextComplements||[])if(!mainMarks.includes(tc.markLbl))problems.push(`Bieter-Textergänzung ${tc.markLbl} bei ${i.oz} fehlt.`); const outs=children(out,'SubDescr');if(outs.length!==(i.subDescrs||[]).length)problems.push(`Unterbeschreibungen bei ${i.oz} stimmen nicht (${outs.length}/${i.subDescrs?.length||0}).`); const subByNo=new Map(outs.map(sd=>[directVal(sd,['SubDNo']),sd])); for(const sd of i.subDescrs||[]){const osd=subByNo.get(sd.subDNo);if(!osd){problems.push(`Unterbeschreibung ${i.oz}/${sd.subDNo} fehlt.`);continue}const oq=num(directVal(osd,['Qty'])),op=num(directVal(osd,['UP']));if(oq==null||Math.abs(oq-effectiveSubQty(i,sd))>0.0005)problems.push(`Menge Unterbeschreibung ${i.oz}/${sd.subDNo} stimmt nicht.`);if(op==null||Math.abs(op-effectiveSubPrice(i,sd))>0.0005)problems.push(`EP Unterbeschreibung ${i.oz}/${sd.subDNo} stimmt nicht.`);const marks=directTcMarks(osd);for(const tc of sd.bidderTextComplements||[])if(!marks.includes(tc.markLbl))problems.push(`Bieter-Textergänzung ${tc.markLbl} bei ${i.oz}/UB ${sd.subDNo} fehlt.`)} } for(const it of items){if(!directVal(it,['UP']))problems.push(`UP fehlt bei ${attr(it,['RNoPart'])||attr(it,['ID'])}.`);if(!directVal(it,['IT']))problems.push(`IT fehlt bei ${attr(it,['RNoPart'])||attr(it,['ID'])}.`)} const mus=descendants(d.documentElement,'MarkupItem');if(mus.length!==state.markups.length)problems.push(`Zuschlagspositionsanzahl stimmt nicht (${mus.length}/${state.markups.length}).`);const mById=new Map(mus.map(x=>[attr(x,['ID']),x])); for(const m of state.markups){const out=mById.get(m.id);if(!out){problems.push(`Zuschlags-ID ${m.id||m.oz} fehlt.`);continue}if((attr(out,['RNoIndex'])||'')!==(m.rnoIndex||''))problems.push(`OZ-Index bei Zuschlag ${m.oz} wurde nicht unverändert übernommen.`);if(Math.abs((num(directVal(out,['ITMarkup']))??Infinity)-markupBasis(m))>0.01)problems.push(`Bezugsbetrag bei Zuschlag ${m.oz} stimmt nicht.`);if(Math.abs((num(directVal(out,['Markup']))??Infinity)-(markupPct(m)??0))>0.000001)problems.push(`Zuschlagsprozentsatz bei ${m.oz} stimmt nicht.`);if(Math.abs((num(directVal(out,['IT']))??Infinity)-markupTotal(m))>0.01)problems.push(`Zuschlagsbetrag bei ${m.oz} stimmt nicht.`);const marks=directTcMarks(out);for(const tc of m.bidderTextComplements||[])if(!marks.includes(tc.markLbl))problems.push(`Bieter-Textergänzung ${tc.markLbl} bei Zuschlag ${m.oz} fehlt.`)} return [...new Set(problems)] } function x84Blockers(){ const blockers=[];const prof=gaebProfile(); if(state.editorMode||state.structuralDirty)blockers.push('LV-Editor/Arbeitskopie ist aktiv. Für X84 bitte in den geschützten Original-/Bietermodus zurückkehren.'); if(state.meta.phase!=='83')blockers.push('Als Quelle muss eine X83 Angebotsaufforderung geöffnet sein.'); if(!prof)blockers.push(`GAEB DA XML ${state.meta.version||'unbekannt'} wird für X84 noch nicht unterstützt. Unterstützt werden 3.2 und 3.3.`); else if(state.meta.versDate!==prof.versDate)blockers.push(`Schema-Ausgabe passt nicht zur Version: geladen ${state.meta.version} / ${state.meta.versDate||'ohne VersDate'}, erwartet ${prof.version} / ${prof.versDate}.`); const sourceDeep=state.xmlText&&state.meta.phase==='83'?deepGaebChecks(state.xmlText,'83').filter(x=>!x.ok):[];if(sourceDeep.length)blockers.push(`Quell-X83 hat formale Vorprüfungsfehler: ${sourceDeep.slice(0,2).map(x=>x.t+': '+x.d).join(' · ')}`); const missing=state.items.filter(i=>effectivePrice(i)==null);if(missing.length)blockers.push(`${missing.length} Hauptposition(en) haben noch keinen Einheitspreis.`); const missingFreeQty=state.items.filter(i=>i.flags?.qtyTBD&&effectiveQty(i)==null);if(missingFreeQty.length)blockers.push(`${missingFreeQty.length} freie Bietermenge(n) sind noch nicht eingetragen.`); const tcOpen=missingBidderTextComplements();if(tcOpen.length)blockers.push(`${tcOpen.length} Bieter-Textergänzung(en) sind noch leer: ${tcOpen.slice(0,3).join(' · ')}${tcOpen.length>3?' …':''}`); const subOpen=incompleteSubDescrs();if(subOpen.length)blockers.push(`${subOpen.length} Unterbeschreibungsangabe(n) sind offen oder noch nicht unterstützt: ${subOpen.slice(0,3).join(' · ')}${subOpen.length>3?' …':''}`); const markupOpen=incompleteMarkups();if(markupOpen.length)blockers.push(`${markupOpen.length} Zuschlagsangabe(n) sind offen bzw. der Bezugsbetrag ist nicht berechenbar: ${markupOpen.slice(0,3).join(' · ')}${markupOpen.length>3?' …':''}`); if(!state.bidder?.name1)blockers.push('Bieterdaten: Firma/Name fehlt.');if(!state.bidder?.street)blockers.push('Bieterdaten: Straße/Hausnummer fehlt.');if(!state.bidder?.pcode)blockers.push('Bieterdaten: PLZ fehlt.');if(!state.bidder?.city)blockers.push('Bieterdaten: Ort fehlt.'); if(state.items.some(i=>!i.elementId))blockers.push('Mindestens eine Position hat keine GAEB-ID.'); if((state.meta.currency||'EUR').toUpperCase()!=='EUR')blockers.push(`Währung ${state.meta.currency} erkannt. Der BVBS-orientierte X84-Export ist derzeit nur für EUR freigegeben.`); if(state.unsupported.length)blockers.push(`${state.unsupported.length} weiterhin nicht freigegebene GAEB-Sonderstruktur(en) erkannt.`); if(state.items.some(i=>i.flags?.upBkdn))blockers.push('EP-Aufgliederung auf Hauptpositionsebene erkannt; diese wird noch nicht sicher exportiert.'); return blockers } async function exportX84(){ saveEdits(true);const blockers=x84Blockers(); if(blockers.length){$('#modalTitle').textContent='X84-Export blockiert';$('#modalBody').innerHTML=blockers.map(x=>`
⛔
${esc(x)}
`).join('')+'
v1.1.6 exportiert X83 versionsgetreu: 3.2 → X84 3.2 / 2013-10 und 3.3 → X84 3.3 / 2021-05. Sonderfelder werden nur bei vollständiger, verlustfreier Abbildung freigegeben.
';$('#modal').classList.add('open');return} const xml=buildX84();const probs=validateX84(xml);const deepBad=deepGaebChecks(xml,'84').filter(x=>!x.ok);if(probs.length||deepBad.length){$('#modalTitle').textContent='X84 interne Prüfung fehlgeschlagen';$('#modalBody').innerHTML=[...probs.map(x=>({message:x})),...deepBad.map(x=>({message:`${x.t}: ${x.d}`}))].map(x=>`
⛔
${esc(x.message)}
`).join('');$('#modal').classList.add('open');return} toast(`Prüfe X84 ${gaebProfile()?.version||''} gegen offizielles GAEB-XSD …`);const checked=await callXsdValidator(xml,'84');if(!checked.valid){$('#modalTitle').textContent='X84-Export · XSD blockiert';$('#modalBody').innerHTML=requireXsdResultHtml(checked.result);$('#modal').classList.add('open');return} const base=state.file.name.replace(/\.[^.]+$/,'');const blob=new Blob([xml],{type:'application/xml;charset=utf-8'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=base+'.x84';a.click();setTimeout(()=>URL.revokeObjectURL(a.href),800);$('#modalTitle').textContent=`X84 ${gaebProfile()?.version||''} XSD-geprüft erzeugt`;$('#modalBody').innerHTML=`
✅
${esc(gaebProfileLabel())}Versionsgetreue Angebotsabgabe aus der geladenen X83.
✅
${state.items.length} Positionen · ${state.markups.length} Zuschlagsposition(en)IDs, RNoPart und RNoIndex aus der X83 übernommen.
✅
${esc(money(offerTotal()))}Angebotssumme inklusive freigegebener Zuschläge.
✅
Offizielle XSD-Schemaprüfung bestanden${esc(state.xsd.schema||gaebProfileLabel())}
Abgabesicherheit: Exportiert wird in derselben GAEB-Version wie die Angebotsaufforderung. Formal XSD-geprüft ist dennoch nicht gleich BVBS-zertifiziert.
`;$('#modal').classList.add('open');toast('X84 versionsgetreu und XSD-geprüft exportiert.') } function markupTypeLabel(t){return ({IdentAsMark:'markierte vorherige Positionen',AllInCat:'alle vorherigen Positionen im Titel',ListInSubQty:'explizite Positionen/Teilmengen'})[t]||t||'unbekannt'} function markupTcFieldsHtml(m){const comps=m.bidderTextComplements||[];if(!comps.length)return '';return `
${comps.map(tc=>{const context=[tc.caption,tc.tail].filter(Boolean).join(' … ');return `
${context?`
${esc(context)}
`:''}
`}).join('')}
`} function specialsModal(){if(!state.file)return;$('#modalTitle').textContent='GAEB-Sonderfälle · v1.1.6';const markups=state.markups||[],subCount=state.items.reduce((n,i)=>n+(i.subDescrs?.length||0),0),tcCount=state.items.reduce((n,i)=>n+(i.bidderTextComplements?.length||0)+(i.subDescrs||[]).reduce((m,sd)=>m+(sd.bidderTextComplements?.length||0),0),0)+markups.reduce((n,m)=>n+(m.bidderTextComplements?.length||0),0);let html=`
${markups.length}Zuschläge
${subCount}Unterbeschreibungen
${tcCount}Bieter-TA
${state.unsupported.length}weiter blockiert
`;if(markups.length){html+=markups.map(m=>{const b=markupBasis(m),pct=markupPct(m),it=markupTotal(m);return `

${esc(m.oz||'–')} · Zuschlagsposition

${esc(markupTypeLabel(m.markupType))}${m.text?' · '+esc(m.text.slice(0,180)):''}
${esc(money(b))}
${esc(money(it))}
${markupTcFieldsHtml(m)}
`}).join('');html+='
'}else html+='
Keine Zuschlagspositionen in dieser Datei.
';if(state.unsupported.length)html+=`
Weiter blockiert: ${esc(state.unsupported.map(x=>x.type+' '+(x.oz||'')).join(', '))}
`;html+=`
v1.1.6 aktiv: Positionstexte und Unterbeschreibungen speisen Sto Match und den Materialplan. Produkt-/Materialzuordnungen bleiben als Arbeitsdaten getrennt vom GAEB-Export.
`;$('#modalBody').innerHTML=html;$('#modal').classList.add('open');const save=$('#saveMarkups');if(save)save.onclick=()=>{$$('.markup-pct').forEach(el=>{const m=state.markups.find(x=>x.id===el.dataset.markup);if(!m)return;const v=num(el.value),e=markupEditFor(m);if(v==null)delete e.percent;else e.percent=v});$$('.markup-tc').forEach(el=>{const m=state.markups.find(x=>x.id===el.dataset.markup);if(!m)return;const e=markupEditFor(m);if(!e.textComplements)e.textComplements={};e.textComplements[el.dataset.tcKey]=el.value.trim()});resetXsdStatus(state.meta.phase);saveEdits(true);renderSummary();$('#modal').classList.remove('open');toast('Zuschläge gespeichert.')}} function bvbsStatusIcon(status){return status==='pass'?'✅':status==='fail'?'⛔':'🟡'} function itemByOz(oz){return state.items.find(i=>i.oz===oz)||null} function specialByOz(type,oz){return (state.specials||[]).find(x=>x.type===type&&x.oz===oz)||null} function categoryByOz(oz){let found=null;function rec(node,prefix=[]){for(const c of node.children||[]){const p=[...prefix,c.part].filter(Boolean);if(p.join('.')===oz)found=c;rec(c,p)}}rec(state.tree,[]);return found} function officialBvbsLike(){return ['001.001.0010','001.002.0030','999.999.9999'].filter(x=>itemByOz(x)).length>=2} function sourceElementById(id){if(!id||!state.doc)return null;return descendants(state.doc.documentElement,'Item').find(x=>attr(x,['ID'])===id)||null} function countTextLines(text){return String(text||'').split(/\n+/).map(x=>x.trim()).filter(Boolean).length} function bvbsCheckRows(){ const rows=[];const official=officialBvbsLike();const add=(status,title,detail,code='')=>rows.push({status,title,detail,code}); add(state.meta.phase==='83'?'pass':'fail','X83-Quelle',state.meta.phase==='83'?'Angebotsaufforderung X83 erkannt.':`Geladene Phase: X${state.meta.phase||'?'}.`,'1.1'); const prof=gaebProfile();add(prof&&state.meta.versDate===prof.versDate?'pass':'fail',prof?prof.label:'GAEB-Version / LV-Schema',`Version ${state.meta.version} · VersDate ${state.meta.versDate||'?'}.`,'2.3'); add(state.items.every(i=>i.elementId&&i.part)?'pass':'fail','IDs und Teil-OZ',`${state.items.length} reguläre Position(en) geprüft.`,'OZ'); const idx=state.items.filter(i=>i.rnoIndex);add(idx.every(i=>/^[_0-9A-Za-z]$/.test(i.rnoIndex))?'pass':'fail','Indexpositionen',`${idx.length} RNoIndex-Position(en) erkannt und roundtrip-fähig.`,'1.5/1.6'); const free=state.items.filter(i=>i.flags?.qtyTBD);add(free.every(i=>effectiveQty(i)!=null)?'pass':(free.length?'manual':'pass'),'Freie Bietermengen',free.length?`${free.length} QtyTBD-Position(en) · ${free.filter(i=>effectiveQty(i)==null).length} noch ohne Bietermenge.`:'Keine freien Bietermengen in dieser Datei.','QtyTBD'); add((state.meta.currency||'EUR').toUpperCase()==='EUR'?'pass':'fail','Währung',`Erkannt: ${(state.meta.currency||'EUR').toUpperCase()} · X84 muss EUR enthalten.`,'2.7'); add(state.unsupported.length===0?'pass':'manual','Sonderpositionen',state.unsupported.length?`${state.unsupported.length} weiterhin blockierte Sonderstruktur(en).`:`Zuschlagspositionen werden in v1.1.6 separat kalkuliert; ${state.markups.length} erkannt.`,'1.14'); if(official){ const normal=itemByOz('001.001.0010');add(normal?'pass':'fail','BVBS-Normalposition 001.001.0010',normal?'Position vorhanden.':'Position fehlt.','1.2'); for(const oz of ['001.001.0010.1','001.001.0010.A']){const it=itemByOz(oz);add(it&&it.rnoIndex?'pass':'fail',`Index/Bedarf ${oz}`,it?`RNoIndex=${it.rnoIndex||'fehlt'} · Bedarf=${it.flags?.provis?'Ja':'nicht eindeutig erkannt'}.`:'Position fehlt.','1.3–1.6')} const graphic=itemByOz('001.002.0010');add(graphic?(graphic.flags?.hasImage?'pass':'manual'):'fail','Grafikposition 001.002.0010',graphic?(graphic.flags?.hasImage?'Grafik-Element erkannt.':'Position vorhanden; eingebettete Grafik bitte visuell gegenprüfen.'):'Position fehlt.','1.7'); const maxq=itemByOz('001.002.0030');add(maxq&&Math.abs((maxq.qty||0)-12345678.012)<0.0005?'pass':'fail','Maximalmenge 001.002.0030',maxq?`Menge ${fmt(maxq.qty,3)}.`:'Position fehlt.','1.8'); add(itemByOz('001.002.0040')?'manual':'fail','Grundposition 001.002.0040',itemByOz('001.002.0040')?'Position vorhanden; Zuordnungskennzeichen wird angezeigt und sollte fachlich gegengeprüft werden.':'Position fehlt.','1.10'); add(itemByOz('001.002.0050')?'manual':'fail','Alternativposition 001.002.0050',itemByOz('001.002.0050')?'Position vorhanden; Zuordnungskennzeichen wird angezeigt und sollte fachlich gegengeprüft werden.':'Position fehlt.','1.11'); add(categoryByOz('002.000')?'pass':'fail','Titel 002.000',categoryByOz('002.000')?'Titel vorhanden.':'Titel fehlt.','1.12'); add(itemByOz('002.000.0000')?'pass':'fail','Position 002.000.0000',itemByOz('002.000.0000')?'Position 0000 vorhanden.':'Position fehlt.','1.13'); {const mk=specialByOz('MarkupItem','002.001.0030');add(mk?(markupBasis(mk)!=null?'pass':'manual'):'fail','Zuschlagsposition 002.001.0030',mk?`Typ ${mk.markupType} erkannt · Bezugsbetrag ${markupBasis(mk)!=null?money(markupBasis(mk)):'noch nicht berechenbar'}.`:'Zuschlagsposition fehlt.','1.14')}; const tx=itemByOz('002.001.0040');add(tx?'manual':'fail','Mehrzeiliger Text 002.001.0040',tx?`Kurz-/Langtext importiert (${countTextLines(tx.short)} / ${countTextLines(tx.long)} erkannte Textzeilen). Visueller Abgleich empfohlen.`:'Position fehlt.','1.15'); const tc=itemByOz('002.002.0020');add(tc?(tc.bidderTextComplements?.length?'pass':'manual'):'fail','Bieter-Textergänzungen 002.002.0020',tc?`${tc.flags?.textComplements||0} TextComplement-Element(e), davon ${tc.bidderTextComplements?.length||0} Bieterfeld(er), strukturiert erkannt.`:'Position fehlt.','1.16'); const sd=itemByOz('002.003.0010');add(sd&&sd.flags?.sumDescr&&sd.subDescrs?.length===2?'pass':(sd?'manual':'fail'),'Leit-/Unterbeschreibung 002.003.0010',sd?`Leitbeschreibung=${sd.flags?.sumDescr?'Ja':'nicht erkannt'} · ${sd.subDescrs?.length||0} Unterbeschreibung(en) mit Menge/EP/Textergänzungen strukturiert.`:'Position fehlt.','1.17'); add(categoryByOz('999.999')?'pass':'fail','Maximaltitel 999.999',categoryByOz('999.999')?'Titel vorhanden.':'Titel fehlt.','1.18'); add(itemByOz('999.999.9999')?'pass':'fail','Maximale numerische OZ',itemByOz('999.999.9999')?'999.999.9999 vorhanden.':'Position fehlt.','1.19'); add(itemByOz('999.999.9999.z')?'pass':'fail','Maximale OZ mit Index z',itemByOz('999.999.9999.z')?'Index z wurde erhalten.':'Position fehlt oder Index ging verloren.','1.20'); add((state.specials||[]).some(x=>x.type==='Remark')?'pass':'manual','Hinweistext am LV-Ende',(state.specials||[]).some(x=>x.type==='Remark')?'Remark/Hinweistext erkannt.':'Kein Remark-Element automatisch erkannt; bei Prüfdatei visuell gegenprüfen.','1.21'); } const ready=state.meta.phase==='83'&&supportedGaebVersion()&&state.meta.versDate===expectedVersDate()&&state.unsupported.length===0&&state.items.every(i=>effectivePrice(i)!=null)&&state.items.filter(i=>i.flags?.qtyTBD).every(i=>effectiveQty(i)!=null)&&missingBidderTextComplements().length===0&&incompleteSubDescrs().length===0&&incompleteMarkups().length===0&&!!state.bidder?.name1&&!!state.bidder?.street&&!!state.bidder?.pcode&&!!state.bidder?.city; add(ready?'pass':'manual','X84-Exportbereitschaft',ready?'Preise, Bieterdaten, Zuschläge, Textergänzungen und Unterbeschreibungen vollständig.':'Noch nicht vollständig exportbereit (Preise, Bieterdaten, freie Mengen oder Sonderfelder prüfen).','1.22'); if(ready){const x=buildX84(),p=validateX84(x);add(p.length?'fail':'pass','Interner X84-Roundtrip',p.length?p.join(' · '):'Version, VersDate, Programmversion, IDs, Indizes, UP/IT, Zuschläge, Sonderfelder und EUR intern geprüft.','2.1–2.7')} else add('manual','Interner X84-Roundtrip','Wird automatisch geprüft, sobald der X84-Export fachlich freigegeben werden kann.','2.1–2.7'); return {official,rows} } function bvbsReportText(report){const r=report||state.bvbs.report||bvbsCheckRows();const lines=[`Sto GAEB Studio v1.1.6 · BVBS-orientierter Selbsttest`,`Datei: ${state.file?.name||'-'}`,`Projekt: ${state.meta.name||'-'}`,`Erstellt: ${new Date().toLocaleString('de-DE')}`,`Modus: ${r.official?'BVBS-Prüfdatei-Merkmale erkannt':'allgemeiner Kompatibilitätscheck'}`,''];for(const x of r.rows)lines.push(`${x.status==='pass'?'OK':x.status==='fail'?'FEHLER':'PRÜFEN'}${x.code?' ['+x.code+']':''} ${x.title}: ${x.detail}`);lines.push('','Hinweis: Dies ist ein interner, BVBS-orientierter Kompatibilitätstest. Er ersetzt weder den offiziellen GAEB-XML-Checker noch eine BVBS-Zertifizierung.');return lines.join('\n')} function saveBvbsReport(){const t=bvbsReportText();const b=new Blob([t],{type:'text/plain;charset=utf-8'}),a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=(state.file?.name||'GAEB').replace(/\.[^.]+$/,'')+'-BVBS-Selbsttest.txt';a.click();setTimeout(()=>URL.revokeObjectURL(a.href),600);toast('BVBS-Selbsttestprotokoll gespeichert.')} function runBvbsCheck(){if(!state.file)return;const report=bvbsCheckRows();state.bvbs.report=report;const pass=report.rows.filter(x=>x.status==='pass').length,fail=report.rows.filter(x=>x.status==='fail').length,manual=report.rows.filter(x=>x.status==='manual').length;$('#modalTitle').textContent='BVBS-Prüfmodus · '+gaebProfileLabel();$('#modalBody').innerHTML=`
${pass}bestanden
${manual}gegenprüfen
${fail}Fehler
${report.official?'BVBS':'GAEB'}${report.official?'Prüfmerkmale erkannt':'Kompatibilitätsmodus'}
`+report.rows.map(x=>`
${bvbsStatusIcon(x.status)}
${x.code?esc(x.code)+' · ':''}${esc(x.title)}${esc(x.detail)}
`).join('')+`
Keine Zertifizierungsbehauptung: Dieser Modus orientiert sich an den veröffentlichten BVBS-Prüfpunkten für Bauausführung. Maßgeblich bleiben die offizielle Prüfdatei, der GAEB-XML-Checker und das formale Zertifizierungsverfahren.
`;$('#modal').classList.add('open');$('#btnSaveBvbsReport').onclick=saveBvbsReport;$('#btnRunXsdFromBvbs').onclick=runXsdCheck} let toastTimer;function toast(t){const el=$('#toast');el.textContent=t;el.classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>el.classList.remove('show'),2200)} $('#btnChoose').addEventListener('click',()=>{$('#fileInput').value='';$('#fileInput').click()});$('#btnDemo').addEventListener('click',openDemo);$('#btnProjects').addEventListener('click',openProjectsModal);$('#fileInput').addEventListener('change',e=>openFile(e.target.files[0]));$('#backupInput').addEventListener('change',async e=>{try{await handleBackupFile(e.target.files[0]);$('#modal').classList.remove('open')}catch(err){console.error(err);alert('Sicherung konnte nicht eingelesen werden:\n\n'+err.message)}});const dz=$('#dropzone');['dragenter','dragover'].forEach(n=>dz.addEventListener(n,e=>{e.preventDefault();dz.classList.add('drag')}));['dragleave','drop'].forEach(n=>dz.addEventListener(n,e=>{e.preventDefault();dz.classList.remove('drag')}));dz.addEventListener('drop',e=>openFile(e.dataTransfer.files[0]));$('#search').addEventListener('input',applyFilters);$('#filter').addEventListener('change',applyFilters);$('#sort').addEventListener('change',applyFilters);$('#btnNew').onclick=reset;$('#btnEditor').onclick=()=>state.editorMode?leaveEditor():enterEditor();$('#btnCheck').onclick=checkWork;$('#btnXsd').onclick=runProfessionalCheck;$('#btnValidator').onclick=openValidatorDiagnostics;$('#btnBvbs').onclick=runBvbsCheck;$('#btnSpecials').onclick=specialsModal;$('#btnStoMatch').onclick=stoLvAnalysis;$('#btnMaterial').onclick=openMaterialPlan;$('#btnDashboard').onclick=proDashboard;$('#btnProject').onclick=projectModal;$('#btnReport').onclick=projectReportModal;$('#btnPauschal').onclick=pauschalPdfModal;$('#btnBidder').onclick=bidderModal;$('#btnBackup').onclick=backupModal;$('#btnSave').onclick=exportWork;$('#btnExport80').onclick=exportX80;$('#btnExport84').onclick=exportX84;$('#btnAddTitle').onclick=addTitle;$('#btnAddItem').onclick=addItem;$('#btnDuplicate').onclick=duplicateSelected;$('#btnMoveUp').onclick=()=>moveSelected(-1);$('#btnMoveDown').onclick=()=>moveSelected(1);$('#btnDelete').onclick=deleteSelected;$('#modalClose').onclick=()=>{$('#modal').classList.remove('open');if(state.selectedId){const i=state.items.find(x=>x.id===state.selectedId);if(i)renderDetail(i)}};$('#modal').addEventListener('click',e=>{if(e.target===$('#modal'))$('#modal').classList.remove('open')});$('#btnBack').onclick=()=>{window.location.href='../../index.html#sto'}; })();