441 lines
19 KiB
HTML
441 lines
19 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>EVE Web Helper – Materialien</title>
|
||
<style>
|
||
:root { color-scheme: light dark; }
|
||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:2rem}
|
||
.container{max-width:1440px;margin:0 auto}
|
||
.nav{display:flex;gap:.6rem;margin-bottom:1rem}
|
||
.tab{padding:.5rem .8rem;border:1px solid #e5e7eb;border-radius:.6rem;text-decoration:none;color:#111827}
|
||
.tab.struct{background:#111827;color:#fff;border-color:#111827}
|
||
|
||
/* Unterreiter links */
|
||
.subtabs-left{position:fixed;left:.8rem;top:96px;display:flex;flex-direction:column;gap:.5rem;
|
||
width:190px;padding:.6rem;border:1px solid #e5e7eb;border-radius:.6rem;background:#fff;
|
||
box-shadow:0 2px 8px rgba(0,0,0,.05);z-index:50}
|
||
.subtabs-left a{display:block;text-decoration:none;padding:.5rem .6rem;border:1px solid #e5e7eb;border-radius:.6rem;color:#111827;background:#fff}
|
||
.subtabs-left a.active{background:#111827;color:#fff;border-color:#111827}
|
||
@media (max-width:1200px){.subtabs-left{display:none}}
|
||
|
||
.row{display:flex;gap:.6rem;flex-wrap:wrap;align-items:center}
|
||
.select, .button, input[type="number"]{padding:.5rem .7rem;border:1px solid #d1d5db;border-radius:.45rem}
|
||
.button{background:#111827;color:#fff;cursor:pointer}
|
||
.muted{color:#6b7280;font-size:.9rem}
|
||
.bad{color:#b91c1c}
|
||
|
||
.card{border:1px solid #e5e7eb;border-radius:.8rem;padding:1rem 1.2rem;margin-bottom:1rem;background:#fff}
|
||
.table-wrap{overflow:auto;border:1px solid #e5e7eb;border-radius:.6rem}
|
||
table{width:100%;border-collapse:collapse}
|
||
th,td{padding:.5rem .45rem;border-bottom:1px solid #e5e7eb;text-align:left;vertical-align:top;word-break:break-word}
|
||
th{font-weight:700}
|
||
.num{text-align:right}
|
||
|
||
details{border:1px solid #e5e7eb;border-radius:.6rem;margin:.6rem 0;background:#fff}
|
||
details>summary{cursor:pointer;padding:.5rem .6rem;font-weight:600}
|
||
details .inner{padding:.5rem .6rem;border-top:1px solid #e5e7eb}
|
||
|
||
@media (prefers-color-scheme:dark){
|
||
body{background:#0b0f19;color:#e5e7eb}
|
||
.tab{border-color:#374151;color:#e5e7eb}
|
||
.card,details{background:#0f1423;border-color:#374151}
|
||
th,td{border-bottom-color:#374151}
|
||
.table-wrap{border-color:#374151}
|
||
.button{background:#1f2937;border-color:#374151}
|
||
.subtabs-left{background:#0f1423;border-color:#374151}
|
||
.subtabs-left a{color:#e5e7eb;border-color:#374151;background:#0f1423}
|
||
.subtabs-left a.active{background:#1f2937}
|
||
details .inner{border-top-color:#374151}
|
||
.select, input, .button{border-color:#374151}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav class="subtabs-left" aria-label="Unterreiter">
|
||
<a href="{{ url_for('structures.structures') }}">Aufträge</a>
|
||
<a class="active" href="{{ url_for('structures.mineralien') }}">Mineralien</a>
|
||
</nav>
|
||
|
||
<div class="container">
|
||
<nav class="nav">
|
||
<a class="tab" href="/">Home</a>
|
||
<a class="tab struct" href="{{ url_for('structures.structures') }}">Strukturen</a>
|
||
<a class="tab" href="/archiv">Archiv</a>
|
||
</nav>
|
||
|
||
<section class="card">
|
||
<div class="row" style="justify-content:space-between">
|
||
<h1 style="margin:0">Material-Übersicht (aus offenen Aufträgen)</h1>
|
||
<div class="row">
|
||
<label class="muted">Ansicht:
|
||
<select id="view-mode" class="select">
|
||
<option value="all" selected>Alle Materialien</option>
|
||
<option value="minerals">Nur Mineralien</option>
|
||
</select>
|
||
</label>
|
||
<label class="muted" id="morphite-wrap" style="display:none">
|
||
<input type="checkbox" id="include-morphite" checked> Morphite einschließen
|
||
</label>
|
||
<button id="btn-refresh" class="button" type="button" onclick="refreshAll()">Aktualisieren</button>
|
||
<button id="btn-csv" class="button" type="button" onclick="exportCSV()">CSV</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Mini-Einstellungen NUR für Mineralien-Abfragen (persistiert via localStorage) -->
|
||
<details class="card" style="margin-top:.8rem">
|
||
<summary>Einstellungen für Berechnung (werden gespeichert)</summary>
|
||
<div class="row" style="margin-top:.6rem">
|
||
<label class="muted">System
|
||
<select id="set-system" class="select" style="min-width:12rem">
|
||
<option>Jita</option>
|
||
<option>Perimeter</option>
|
||
<option>Amarr</option>
|
||
<option>Dodixie</option>
|
||
<option>Rens</option>
|
||
<option>Hek</option>
|
||
</select>
|
||
</label>
|
||
<label class="muted">ME % (Blueprint)
|
||
<input id="set-baseMe" type="number" min="0" max="10" step="1" class="select" style="width:6rem" />
|
||
</label>
|
||
<label class="muted">Steuersatz % (facilityTax)
|
||
<input id="set-facilityTax" type="number" min="0" max="100" step="1" class="select" style="width:7rem" />
|
||
</label>
|
||
<button class="button" type="button" onclick="saveSettingsFromForm()">Speichern</button>
|
||
</div>
|
||
<p class="muted" style="margin:.6rem 0 0">Eine ME-Einstellung reicht – sie wird auch für Komponenten verwendet. Werte werden im Browser gespeichert.</p>
|
||
</details>
|
||
|
||
<p class="muted">Wir lösen die Zwischenprodukte in <em>Blatt-Materialien</em> auf (nur echte Eingänge, keine Doppelzählung).</p>
|
||
|
||
<div id="box" class="card" style="padding:.6rem 1rem;margin:.8rem 0">Lade Materialien …</div>
|
||
|
||
<!-- 1) Pro-Item-Aufschlüsselung -->
|
||
<div id="wrap-breakdown" style="display:none">
|
||
<h3>Aufschlüsselung pro Zwischenprodukt</h3>
|
||
<div id="breakdown"></div>
|
||
</div>
|
||
|
||
<!-- 2) Gesamtübersicht -->
|
||
<div id="wrap-total" style="display:none;margin-top:1rem">
|
||
<h3>Gesamtübersicht (aggregiert)</h3>
|
||
<div class="table-wrap">
|
||
<table aria-label="Materialien – Gesamt">
|
||
<thead>
|
||
<tr><th>Item</th><th class="num">Gesamtmenge</th><th class="num">Ø Preis / Einh.</th><th class="num">Gesamtkosten</th></tr>
|
||
</thead>
|
||
<tbody id="tbody-total"></tbody>
|
||
<tfoot>
|
||
<tr><th colspan="3" class="num">Summe</th><th class="num"><strong id="sum-total">0 ISK</strong></th></tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
<div class="muted" id="ts" style="margin-top:.4rem"></div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<script>
|
||
/* ===== Persistente Einstellungen NUR für Mineralien ===== */
|
||
const SETTINGS_KEY = "sp_global_mats_settings";
|
||
// nur 1 ME, System & facilityTax
|
||
let SETTINGS = { system:"Jita", baseMe:0, facilityTax:0 };
|
||
|
||
function loadLocalSettings(){
|
||
try{
|
||
const raw = localStorage.getItem(SETTINGS_KEY);
|
||
if(raw){
|
||
const s = JSON.parse(raw);
|
||
if(s && typeof s === 'object'){
|
||
// migriert alte Objekte (componentsMe ggf. vorhanden -> ignoriert)
|
||
SETTINGS.system = s.system ?? SETTINGS.system;
|
||
SETTINGS.baseMe = (s.baseMe ?? s.componentsMe ?? SETTINGS.baseMe);
|
||
SETTINGS.facilityTax = s.facilityTax ?? SETTINGS.facilityTax;
|
||
}
|
||
}
|
||
}catch(_){}
|
||
// UI füllen
|
||
document.getElementById('set-system').value = SETTINGS.system;
|
||
document.getElementById('set-baseMe').value = SETTINGS.baseMe;
|
||
document.getElementById('set-facilityTax').value = SETTINGS.facilityTax;
|
||
}
|
||
function saveLocalSettings(){
|
||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(SETTINGS));
|
||
}
|
||
function saveSettingsFromForm(){
|
||
SETTINGS.system = document.getElementById('set-system').value || SETTINGS.system;
|
||
SETTINGS.baseMe = parseInt(document.getElementById('set-baseMe').value || SETTINGS.baseMe, 10);
|
||
SETTINGS.facilityTax = parseInt(document.getElementById('set-facilityTax').value || SETTINGS.facilityTax, 10);
|
||
saveLocalSettings();
|
||
loadMaterialsDeep(); // neu berechnen
|
||
}
|
||
|
||
/* ===== Konstanten / Helfer ===== */
|
||
const MINERAL_TYPES={34:"Tritanium",35:"Pyerite",36:"Mexallon",37:"Isogen",38:"Nocxium",39:"Zydrine",40:"Megacyte",11399:"Morphite"};
|
||
const MINERAL_ORDER=[34,35,36,37,38,39,40,11399];
|
||
const sleep=(ms)=>new Promise(r=>setTimeout(r,ms));
|
||
function formatISK(x){const n=Number(x);if(!isFinite(n))return String(x);return n.toLocaleString('de-DE')+" ISK";}
|
||
function safeNum(v){const n=Number(v);return isFinite(n)?n:0;}
|
||
function formatTS(s){if(!s)return"";const d=new Date(s);if(isNaN(d.getTime())){const t=String(s);return t.slice(0,16);}const y=d.getFullYear(),m=String(d.getMonth()+1).padStart(2,'0'),da=String(d.getDate()).padStart(2,'0'),hh=String(d.getHours()).padStart(2,'0'),mm=String(d.getMinutes()).padStart(2,'0');return `${y}-${m}-${da} ${hh}:${mm}`;}
|
||
|
||
function looksLikeMatRow(o){
|
||
if(!o||typeof o!=='object')return false;
|
||
const hasId=('type_id'in o)||('typeId'in o);
|
||
const hasQty=('quantity'in o)||('qty'in o)||('amount'in o)||('count'in o);
|
||
return hasId && hasQty;
|
||
}
|
||
function hasChildren(o){
|
||
for(const k of ['materials','components','inputs','children','parts','steps','subMaterials']){
|
||
if(Array.isArray(o[k]) && o[k].length) return true;
|
||
}
|
||
return false;
|
||
}
|
||
// sammelt NUR Blatt-Materialien
|
||
function collectLeafMatRowsDeep(x,out){
|
||
if(!x) return;
|
||
if(Array.isArray(x)){ for(const v of x) collectLeafMatRowsDeep(v,out); return; }
|
||
if(typeof x==='object'){
|
||
if(looksLikeMatRow(x) && !hasChildren(x)) out.push(x);
|
||
for(const v of Object.values(x)) collectLeafMatRowsDeep(v,out);
|
||
}
|
||
}
|
||
|
||
function normalizeRows(rows){
|
||
return rows.map(r=>{
|
||
const id=Number(r.type_id??r.typeId??0);
|
||
const name=r.name??r.item_name??r.typeName??("Type "+id);
|
||
const qty=safeNum(r.quantity??r.qty??r.amount??r.count);
|
||
const cost=safeNum(r.cost??r.total_cost);
|
||
const unit=safeNum(r.unit_cost??r.cost_per_unit??(qty?cost/qty:0));
|
||
return {type_id:id,name,quantity:qty,cost,unit};
|
||
});
|
||
}
|
||
function isMineralId(id){return MINERAL_ORDER.includes(Number(id));}
|
||
function filterByViewMode(normRows){
|
||
const mode=document.getElementById('view-mode').value;
|
||
if(mode==='minerals'){
|
||
const includeMorphite=document.getElementById('include-morphite').checked;
|
||
const allowed=new Set(includeMorphite?MINERAL_ORDER:MINERAL_ORDER.filter(x=>x!==11399));
|
||
return normRows.filter(r=>allowed.has(r.type_id));
|
||
}
|
||
return normRows; // 'all'
|
||
}
|
||
function aggregateRows(rows){
|
||
const map=new Map();
|
||
for(const r of rows){
|
||
const k=r.type_id;
|
||
if(!map.has(k)) map.set(k,{type_id:k,name:r.name,quantity:0,cost:0,unitSum:0,unitWeight:0});
|
||
const a=map.get(k);
|
||
a.quantity+=safeNum(r.quantity);
|
||
a.cost+=safeNum(r.cost);
|
||
if(r.unit>0 && r.quantity>0){a.unitSum+=r.unit*r.quantity; a.unitWeight+=r.quantity;}
|
||
}
|
||
const all=[...map.values()];
|
||
const minerals=all.filter(x=>isMineralId(x.type_id)).sort((a,b)=>MINERAL_ORDER.indexOf(a.type_id)-MINERAL_ORDER.indexOf(b.type_id));
|
||
const others=all.filter(x=>!isMineralId(x.type_id)).sort((a,b)=>a.name.localeCompare(b.name,'de'));
|
||
return [...minerals,...others].map(a=>{
|
||
const avg=a.unitWeight>0 ? a.unitSum/a.unitWeight : (a.quantity ? a.cost/a.quantity : 0);
|
||
return {type_id:a.type_id,name:a.name,quantity:a.quantity,unit:avg,cost:a.cost};
|
||
});
|
||
}
|
||
|
||
/* ===== API + Blueprint-Lookup ===== */
|
||
async function fetchJSON(url,{retries=2,backoff=700}={}){let last=null;for(let i=0;i<=retries;i++){try{const r=await fetch(url);const retr=r.status>=500;if(!r.ok){const t=await r.text();last=new Error(`HTTP ${r.status} – ${t.slice(0,200)}`);if(!retr)throw last;}else{return await r.json();}}catch(e){last=e;}if(i<retries)await sleep(backoff*Math.pow(1.6,i));}throw last||new Error('Unbekannter Fehler');}
|
||
function q(obj){const p=new URLSearchParams();for(const [k,v] of Object.entries(obj)){if(v!==undefined&&v!==null)p.append(k,String(v));}return p.toString();}
|
||
const BP_CACHE=new Map();
|
||
async function getBlueprintId(productName, productTypeId){
|
||
const key=(productName||"")+":"+String(productTypeId||"");
|
||
if(BP_CACHE.has(key)) return BP_CACHE.get(key);
|
||
const url="/api/blueprint_id?"+q({name:productName,productTypeId:productTypeId});
|
||
const j=await fetchJSON(url,{retries:1,backoff:400});
|
||
const id=j && j.blueprint_type_id;
|
||
if(!id) throw new Error("Blueprint-ID nicht gefunden");
|
||
BP_CACHE.set(key,id);
|
||
return id;
|
||
}
|
||
|
||
// *** eine ME-Einstellung -> für baseMe UND componentsMe nutzen ***
|
||
async function callMaterialsEndpoint(blueprintTypeId, qty){
|
||
const params={
|
||
blueprintTypeId,
|
||
quantity:qty,
|
||
priceMode:"buy",
|
||
baseMe:SETTINGS.baseMe,
|
||
componentsMe:SETTINGS.baseMe, // gleicher Wert
|
||
system:SETTINGS.system,
|
||
facilityTax:SETTINGS.facilityTax
|
||
};
|
||
return await fetchJSON("/api/cookbook/materials?"+q(params),{retries:2,backoff:800});
|
||
}
|
||
async function callBuildCostEndpoint(blueprintTypeId, qty){
|
||
const params={
|
||
blueprintTypeId,
|
||
quantity:qty,
|
||
priceMode:"buy",
|
||
baseMe:SETTINGS.baseMe,
|
||
componentsMe:SETTINGS.baseMe, // gleicher Wert
|
||
system:SETTINGS.system,
|
||
facilityTax:SETTINGS.facilityTax,
|
||
detailed:"Yes",steps:"Yes",showDetailed:"Yes",showDetailedBuildSteps:"Yes"
|
||
};
|
||
return await fetchJSON("/api/cookbook/build_cost?"+q(params),{retries:2,backoff:900});
|
||
}
|
||
async function loadLeafMaterialsFor(productName, productTypeId, qty){
|
||
try{
|
||
const bpId=await getBlueprintId(productName, productTypeId);
|
||
let rows=[], r=await callMaterialsEndpoint(bpId, qty);
|
||
collectLeafMatRowsDeep(r, rows);
|
||
if(!rows.length){ r=await callBuildCostEndpoint(bpId, qty); collectLeafMatRowsDeep(r, rows); }
|
||
return rows;
|
||
}catch(e){ console.warn("Materialermittlung fehlgeschlagen:", e); return []; }
|
||
}
|
||
|
||
/* ===== Rendering ===== */
|
||
function renderTotal(rows, metaTs){
|
||
const tbody=document.getElementById('tbody-total');
|
||
const wrap=document.getElementById('wrap-total');
|
||
const box=document.getElementById('box');
|
||
const sumEl=document.getElementById('sum-total');
|
||
const ts=document.getElementById('ts');
|
||
|
||
if(!rows.length){ box.textContent='Keine Materialien ermittelt.'; wrap.style.display='none'; return; }
|
||
|
||
tbody.innerHTML=rows.map(r=>`
|
||
<tr>
|
||
<td>${r.name} <span class="muted">(${r.type_id})</span></td>
|
||
<td class="num">${r.quantity.toLocaleString('de-DE')}</td>
|
||
<td class="num">${r.unit?formatISK(r.unit):'—'}</td>
|
||
<td class="num"><strong>${formatISK(r.cost)}</strong></td>
|
||
</tr>`).join('');
|
||
|
||
const total=rows.reduce((s,r)=>s+safeNum(r.cost),0);
|
||
sumEl.textContent=formatISK(total);
|
||
ts.textContent = metaTs ? ('Stand: '+formatTS(metaTs)) : '';
|
||
|
||
box.style.display='none';
|
||
wrap.style.display='block';
|
||
}
|
||
|
||
function renderBreakdown(perItem){
|
||
const host=document.getElementById('breakdown');
|
||
const wrap=document.getElementById('wrap-breakdown');
|
||
if(!perItem.length){ wrap.style.display='none'; return; }
|
||
|
||
host.innerHTML=perItem.map(it=>{
|
||
const inner = it.rows.length
|
||
? `<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Item</th><th class="num">Menge</th><th class="num">Ø Preis / Einh.</th><th class="num">Kosten</th></tr></thead>
|
||
<tbody>
|
||
${it.rows.map(r=>`<tr>
|
||
<td>${r.name} <span class="muted">(${r.type_id})</span></td>
|
||
<td class="num">${r.quantity.toLocaleString('de-DE')}</td>
|
||
<td class="num">${r.unit?formatISK(r.unit):'—'}</td>
|
||
<td class="num">${formatISK(r.cost)}</td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>`
|
||
: `<div class="muted inner">Keine (Blatt-)Materialien benötigt oder nicht ermittelbar.</div>`;
|
||
|
||
const err = it.error ? `<div class="bad inner">Fehler: ${it.error}</div>` : '';
|
||
|
||
return `<details>
|
||
<summary>${it.label} <span class="muted">(${it.type_id})</span> – Menge: ${it.qty.toLocaleString('de-DE')}</summary>
|
||
<div class="inner">${inner}${err}</div>
|
||
</details>`;
|
||
}).join('');
|
||
|
||
wrap.style.display='block';
|
||
}
|
||
|
||
/* ===== Hauptlogik ===== */
|
||
async function loadMaterialsDeep(){
|
||
const box=document.getElementById('box');
|
||
const totalWrap=document.getElementById('wrap-total');
|
||
const breakdownWrap=document.getElementById('wrap-breakdown');
|
||
box.textContent='Lade offene Materialien …';
|
||
box.style.display='block';
|
||
totalWrap.style.display='none';
|
||
breakdownWrap.style.display='none';
|
||
|
||
try{
|
||
const open=await fetchJSON("/api/cookbook/open_materials",{retries:1,backoff:500});
|
||
const items=Array.isArray(open.items)?open.items:[];
|
||
if(!items.length){ box.textContent='Keine offenen Zwischenprodukte vorhanden.'; return; }
|
||
|
||
let done=0; const progress=()=> box.textContent=`Ermittle Eingangs-Materialien … (${done}/${items.length})`;
|
||
|
||
const perItem=[]; // für Breakdown
|
||
const allRows=[]; // für Gesamt
|
||
|
||
for(const it of items){
|
||
progress();
|
||
const typeId=it.type_id??it.typeId;
|
||
const qty=safeNum(it.quantity??it.qty??it.amount??0);
|
||
const label=(it.name??it.item_name??it.typeName??("Type "+typeId));
|
||
if(!typeId || qty<=0){ done++; continue; }
|
||
|
||
try{
|
||
const rowsRaw = await loadLeafMaterialsFor(label, typeId, qty); // echte Blätter
|
||
const norm = normalizeRows(rowsRaw);
|
||
const filtered= filterByViewMode(norm);
|
||
const aggr = aggregateRows(filtered);
|
||
|
||
perItem.push({type_id:typeId,label,qty,rows:aggr});
|
||
aggr.forEach(r=>allRows.push(r));
|
||
}catch(err){
|
||
perItem.push({type_id:typeId,label,qty,rows:[],error:String(err && err.message ? err.message : err)});
|
||
}
|
||
|
||
done++; progress();
|
||
await sleep(300); // Upstream schonen
|
||
}
|
||
|
||
// 1) Breakdown pro Item
|
||
renderBreakdown(perItem);
|
||
// 2) Gesamt-Übersicht
|
||
const total = aggregateRows(allRows);
|
||
renderTotal(total, open.refreshed_at);
|
||
}catch(err){
|
||
box.innerHTML = `<span class="bad">Fehler: ${err && err.message ? err.message : err}</span>`;
|
||
}
|
||
}
|
||
|
||
function refreshAll(){
|
||
const b=document.getElementById("btn-refresh"); if(b) b.disabled=true;
|
||
fetch("/api/orders/refresh_all",{method:"POST"})
|
||
.then(()=>loadMaterialsDeep())
|
||
.finally(()=>{ if(b) b.disabled=false; });
|
||
}
|
||
|
||
function exportCSV(){
|
||
const rows=[["Item","Gesamtmenge","Ø Preis / Einh.","Gesamtkosten"]];
|
||
document.querySelectorAll("#tbody-total tr").forEach(tr=>{
|
||
const tds=[...tr.querySelectorAll("td")].map(td=>td.textContent);
|
||
rows.push(tds);
|
||
});
|
||
const csv=rows.map(r=>r.map(v=>`"${String(v).replace(/"/g,'""')}"`).join(";")).join("\r\n");
|
||
const blob=new Blob([csv],{type:"text/csv;charset=utf-8;"});
|
||
const a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download="materialien_summe.csv"; a.click(); URL.revokeObjectURL(a.href);
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded",()=>{
|
||
loadLocalSettings(); // Einstellungen laden
|
||
const modeSel=document.getElementById('view-mode');
|
||
const morphiteWrap=document.getElementById('morphite-wrap');
|
||
modeSel.addEventListener('change',()=>{
|
||
morphiteWrap.style.display = modeSel.value==='minerals' ? 'inline-flex' : 'none';
|
||
loadMaterialsDeep();
|
||
});
|
||
document.getElementById('include-morphite')?.addEventListener('change', loadMaterialsDeep);
|
||
morphiteWrap.style.display = modeSel.value==='minerals' ? 'inline-flex' : 'none';
|
||
loadMaterialsDeep();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|