first commit

This commit is contained in:
2025-08-27 18:55:45 +02:00
commit 1dce2ebf2c
34 changed files with 52590 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EVE Web Helper Archiv</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.active{background:#111827;color:#fff;border-color:#111827}
.card{border:1px solid #e5e7eb;border-radius:.8rem;padding:1rem 1.2rem;margin-bottom:1rem;background:#fff}
.full{width:100%}
.section-title{font-weight:700;margin-bottom:.6rem}
.table-wrap{overflow:auto;border:1px solid #e5e7eb;border-radius:.6rem}
.table-wrap table{width:100%;border-collapse:collapse;margin:0;table-layout:fixed}
th,td{padding:.5rem .45rem;border-bottom:1px solid #e5e7eb;text-align:left;vertical-align:top;word-break:break-word;overflow-wrap:anywhere}
th{font-weight:700}
.id{font-family:ui-monospace,SFMono-Regular,Consolas,Menlo,monospace;font-size:.85em;max-width:90px}
.num{text-align:right}
.empty{padding:.65rem .8rem;border:1px dashed #d1d5db;border-radius:.6rem;color:#6b7280;background:transparent}
/* Modal (nur lesen) */
.modal{position:fixed;inset:0;display:none;align-items:flex-start;justify-content:center;padding:4vh 1rem;background:rgba(0,0,0,.45);z-index:999}
.modal:target{display:flex}
.dialog{max-width:1100px;width:min(92vw,1100px);background:#fff;border-radius:14px;padding:1rem 1.2rem;box-shadow:0 20px 45px rgba(0,0,0,.35);max-height:92vh;overflow:auto;position:relative}
.close{position:sticky;top:0;float:right;font-size:1.4rem;text-decoration:none;color:#111827;padding:.2rem .5rem;border-radius:.4rem}
.kv{display:grid;grid-template-columns:180px 1fr;gap:.35rem .8rem}
.pill{display:inline-block;background:#111827;color:#fff;border-radius:999px;padding:.2rem .55rem;font-size:.85rem;margin-bottom:.5rem}
.result{border:1px solid #e5e7eb;border-radius:.6rem;padding:.7rem .8rem;background:#fafafa;margin-top:.6rem}
@media (prefers-color-scheme:dark){
body{background:#0b0f19;color:#e5e7eb}
.tab{border-color:#374151;color:#e5e7eb}
.tab.active{background:#1f2937;border-color:#374151}
.card,.dialog{background:#0f1423;border-color:#374151}
th,td{border-bottom-color:#374151}
.table-wrap{border-color:#374151}
.result{background:#111318;border-color:#374151}
.close{color:#e5e7eb}
}
</style>
</head>
<body>
<div class="container">
<nav class="nav">
<a class="tab" href="/">Home</a>
<a class="tab" href="/strukturen">Strukturen</a>
<a class="tab active" href="/archiv">Archiv</a>
</nav>
<section class="card full">
<div class="section-title">Archivierte / fertige Aufträge</div>
{% if archived_orders %}
<div class="table-wrap">
<table aria-label="Archivierte Aufträge">
<thead>
<tr>
<th>ID</th>
<th>Struktur</th>
<th>System</th>
<th>Ind.-Struktur</th>
<th>Ind.-Rig</th>
<th>Reakt.-Struktur</th>
<th>Reakt.-Rig</th>
<th class="num">Menge</th>
<th class="num">ME&nbsp;%</th>
<th>Erstellt</th>
<th>Fertig/Archiv</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for o in archived_orders %}
<tr>
<td class="id">{{ o.id[:8] }}…</td>
<td>{{ o.structure }}</td>
<td>{{ o.system|default('Jita') }}</td>
<td>{{ o.industry_structure|default('Station') }}</td>
<td>{{ o.industry_rig|default('None') }}</td>
<td>{{ o.reaction_structure|default('Athanor') }}</td>
<td>{{ o.reaction_rig|default('None') }}</td>
<td class="num">{{ o.quantity }}</td>
<td class="num">{{ o.me }}</td>
<td>{{ o.created_at|fmt_ts }}</td>
<td>{{ (o.archived_at or o.done_at)|fmt_ts }}</td>
<td><a class="tab" style="padding:.35rem .6rem" href="#m-{{ o.id }}">Details</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty">Keine archivierten Aufträge vorhanden.</div>
{% endif %}
</section>
<!-- Readonly-Details pro Auftrag -->
{% for o in archived_orders %}
<div id="m-{{ o.id }}" class="modal" aria-hidden="true">
<div class="dialog" role="dialog" aria-modal="true" aria-labelledby="h-{{ o.id }}">
<a class="close" href="#" aria-label="Schließen">×</a>
<h2 id="h-{{ o.id }}">Auftrag {{ o.structure }}</h2>
<div class="kv">
<div><strong>ID</strong></div><div class="id">{{ o.id }}</div>
<div><strong>Status</strong></div><div>{{ o.status }}</div>
<div><strong>Struktur</strong></div><div>{{ o.structure }}</div>
<div><strong>System</strong></div><div>{{ o.system|default('Jita') }}</div>
<div><strong>Industrie-Struktur</strong></div><div>{{ o.industry_structure|default('Station') }}</div>
<div><strong>Industrie-Rig</strong></div><div>{{ o.industry_rig|default('None') }}</div>
<div><strong>Reaktions-Struktur</strong></div><div>{{ o.reaction_structure|default('Athanor') }}</div>
<div><strong>Reaktions-Rig</strong></div><div>{{ o.reaction_rig|default('None') }}</div>
<div><strong>Menge</strong></div><div>{{ o.quantity }}</div>
<div><strong>ME&nbsp;%</strong></div><div>{{ o.me }}</div>
<div><strong>Notizen</strong></div><div>{{ o.notes|default('') }}</div>
<div><strong>Erstellt</strong></div><div>{{ o.created_at|fmt_ts }}</div>
<div><strong>Fertig/Archiv</strong></div><div>{{ (o.archived_at or o.done_at)|fmt_ts }}</div>
<div><strong>Blueprint&nbsp;ID</strong></div><div>{{ o.blueprint_type_id or '—' }}</div>
</div>
{% set cb = o.cookbook and (o.cookbook.message or o.cookbook) %}
{% if cb %}
<div class="pill">Gesamtkosten:
{{ (cb.totalCost or cb.total or cb.buildCostPerUnit) | default('—') }}
</div>
<div class="result">
<details>
<summary>CookbookRohdaten</summary>
<pre style="white-space:pre-wrap">{{ o.cookbook | tojson(indent=2) }}</pre>
</details>
</div>
{% endif %}
{% if o.materials %}
<div class="result">
<h4 style="margin:.2rem 0 .5rem">Materialien</h4>
<table style="width:100%;border-collapse:collapse">
<thead><tr><th>Item</th><th class="num">Menge</th><th class="num">Kosten</th></tr></thead>
<tbody>
{% set mats = o.materials.materials or o.materials.message and o.materials.message.materials or [] %}
{% for m in mats %}
{% set tid = m.type_id or m.typeId %}
{% set name = m.name or ('Type ' ~ tid) %}
{% set qty = m.quantity or m.qty or m.amount or m.count %}
{% set cost = m.cost or m.cost_per_unit or m.unit_cost %}
<tr>
<td>{{ name }} {% if tid %}<span class="id">({{ tid }})</span>{% endif %}</td>
<td class="num">{{ qty }}</td>
<td class="num">{% if cost is not none %}<strong>{{ cost }}</strong>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<details style="margin-top:.4rem">
<summary>MaterialRohdaten</summary>
<pre style="white-space:pre-wrap">{{ o.materials | tojson(indent=2) }}</pre>
</details>
</div>
{% endif %}
<div style="margin-top:.8rem"><a class="tab" href="#" style="padding:.4rem .7rem">Schließen</a></div>
</div>
</div>
{% endfor %}
</div>
</body>
</html>

View File

@@ -0,0 +1,78 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EVE Web Helper {% block title %}{% endblock %}</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}
/* Einheitliche Tabs */
.nav{display:flex;gap:.6rem;margin-bottom:1.2rem}
.tab{padding:.5rem .8rem;border:1px solid #e5e7eb;border-radius:.6rem;text-decoration:none;color:#111827}
.tab.active{background:#111827;color:#fff;border-color:#111827}
/* Grund-UI */
.card{border:1px solid #e5e7eb;border-radius:.8rem;padding:1rem 1.2rem;margin-bottom:1rem;background:#fff}
.wide{max-width:1200px}
.full{width:100%}
h1{font-size:1.35rem;margin:.2rem 0 1rem}
label{font-weight:600;display:block;margin-bottom:.35rem}
select,input,textarea,button,a.button{padding:.5rem .7rem;border:1px solid #d1d5db;border-radius:.45rem}
textarea{width:100%;min-height:90px;resize:vertical}
button,a.button{background:#111827;color:#fff;cursor:pointer;text-decoration:none;display:inline-block}
.muted{color:#6b7280;font-size:.9rem}
table{width:100%;border-collapse:collapse;table-layout:fixed}
th,td{padding:.5rem .45rem;border-bottom:1px solid #e5e7eb;text-align:left;vertical-align:top;word-break:break-word;overflow-wrap:anywhere}
th{font-weight:700}
.id{font-family:ui-monospace,SFMono-Regular,Consolas,Menlo,monospace;font-size:.85em;max-width:90px}
.num{text-align:right}
.actions form{display:inline}
.empty{padding:.65rem .8rem;border:1px dashed #d1d5db;border-radius:.6rem;color:#6b7280;background:transparent}
.table-wrap{overflow:auto;border:1px solid #e5e7eb;border-radius:.6rem}
.table-wrap thead th{position:sticky;top:0;background:#fff;z-index:1}
/* Modal (global) */
.modal{position:fixed;inset:0;display:none;align-items:flex-start;justify-content:center;padding:4vh 1rem;background:rgba(0,0,0,.45);z-index:999}
.modal:target{display:flex}
.dialog{max-width:1100px;width:min(92vw,1100px);background:#fff;border-radius:14px;padding:1rem 1.2rem;box-shadow:0 20px 45px rgba(0,0,0,.35);max-height:92vh;overflow:auto;position:relative;z-index:1000;pointer-events:auto}
.modal *{pointer-events:auto}
.modal h2{margin:.2rem 0 1rem;font-size:1.2rem}
.close{position:sticky;top:0;float:right;font-size:1.4rem;text-decoration:none;color:#111827;padding:.2rem .5rem;border-radius:.4rem;z-index:2}
.kv{display:grid;grid-template-columns:180px 1fr;gap:.35rem .8rem}
.pill{display:inline-block;background:#111827;color:#fff;border-radius:999px;padding:.2rem .55rem;font-size:.85rem}
.result{border:1px solid #e5e7eb;border-radius:.6rem;padding:.7rem .8rem;background:#fafafa;margin-top:.6rem}
.err{color:#b91c1c}
@media (prefers-color-scheme:dark){
body{background:#0b0f19;color:#e5e7eb}
.tab{border-color:#374151;color:#e5e7eb}
.tab.active{background:#1f2937;border-color:#374151}
.card,.dialog{background:#0f1423;border-color:#374151}
th,td{border-bottom-color:#374151}
.table-wrap{border-color:#374151}
.table-wrap thead th{background:#0f1423}
.result{background:#111318;border-color:#374151}
.close{color:#e5e7eb}
}
</style>
{% block head_extra %}{% endblock %}
</head>
<body>
<div class="container">
<nav class="nav">
{% set p = request.path %}
<a class="tab {{ 'active' if p == '/' else '' }}" href="/">Home</a>
<a class="tab {{ 'active' if p.startswith('/strukturen') else '' }}" href="/strukturen">Strukturen</a>
<a class="tab {{ 'active' if p.startswith('/archiv') else '' }}" href="/archiv">Archiv</a>
</nav>
{% block content %}{% endblock %}
</div>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<h1>EVE Web Helper</h1>
<p>Willkommen! Nutze die Tabs oben, um zu den Strukturen oder zum Archiv zu wechseln.</p>
{% endblock %}

View File

@@ -0,0 +1,440 @@
<!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>

View File

@@ -0,0 +1,537 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EVE Web Helper Strukturen</title>
<style>
:root { color-scheme: light dark; }
html{scroll-behavior:smooth}
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}
/* Haupt-Tabs oben */
.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.active{background:#111827;color:#fff;border-color:#111827}
/* Linke Unterreiter (untergeordnete Seite) */
.subtabs-left{
position:fixed; left:.8rem; top:96px; display:flex; flex-direction:column; gap:.5rem;
width:170px; 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}}
.card{border:1px solid #e5e7eb;border-radius:.8rem;padding:1rem 1.2rem;margin-bottom:1rem;background:#fff}
.wide{max-width:1200px}
.full{width:100%}
h1{font-size:1.35rem;margin:.2rem 0 1rem}
label{font-weight:600;display:block;margin-bottom:.35rem}
select,input,textarea,button,a.button{padding:.5rem .7rem;border:1px solid #d1d5db;border-radius:.45rem}
textarea{width:100%;min-height:90px;resize:vertical}
button,a.button{background:#111827;color:#fff;cursor:pointer;text-decoration:none;display:inline-block}
.muted{color:#6b7280;font-size:.9rem}
.row{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.8rem;margin-top:.8rem}
@media (max-width:1000px){.row{grid-template-columns:1fr 1fr}}
table{width:100%;border-collapse:collapse;table-layout:fixed}
th,td{padding:.5rem .45rem;border-bottom:1px solid #e5e7eb;text-align:left;vertical-align:top;word-break:break-word;overflow-wrap:anywhere}
th{font-weight:700}
.id{font-family:ui-monospace,SFMono-Regular,Consolas,Menlo,monospace;font-size:.85em;max-width:90px}
.num{text-align:right}
.actions form{display:inline}
.section-title{font-weight:700;margin-bottom:.6rem}
.empty{padding:.65rem .8rem;border:1px dashed #d1d5db;border-radius:.6rem;color:#6b7280;background:transparent}
.table-wrap{overflow:auto;border:1px solid #e5e7eb;border-radius:.6rem}
.table-wrap thead th{position:sticky;top:0;background:#fff;z-index:1}
.scroll-5{max-height:260px}
@media (max-width:1600px){ .col-notes{display:none} }
@media (max-width:1450px){ .col-react-rig{display:none} }
@media (max-width:1350px){ .col-ind-rig{display:none} }
@media (max-width:1250px){ .col-ind-struct,.col-react-struct{display:none} }
/* Modal */
.modal{position:fixed;inset:0;display:none;align-items:flex-start;justify-content:center;padding:4vh 1rem;background:rgba(0,0,0,.45);z-index:999}
.modal:target{display:flex}
.dialog{max-width:1100px;width:min(92vw,1100px);background:#fff;border-radius:14px;padding:1rem 1.2rem;box-shadow:0 20px 45px rgba(0,0,0,.35);max-height:92vh;overflow:auto;position:relative;z-index:1000;pointer-events:auto}
.modal *{pointer-events:auto}
.modal h2{margin:.2rem 0 1rem;font-size:1.2rem}
.close{position:sticky;top:0;float:right;font-size:1.4rem;text-decoration:none;color:#111827;padding:.2rem .5rem;border-radius:.4rem;z-index:2}
.kv{display:grid;grid-template-columns:180px 1fr;gap:.35rem .8rem}
hr.sep{border:none;border-top:1px solid #e5e7eb;margin:.8rem 0}
.pill{display:inline-block;background:#111827;color:#fff;border-radius:999px;padding:.2rem .55rem;font-size:.85rem}
.result{border:1px solid #e5e7eb;border-radius:.6rem;padding:.7rem .8rem;background:#fafafa;margin-top:.6rem}
.err{color:#b91c1c}
@media (prefers-color-scheme:dark){
body{color:#e5e7eb;background:#0b0f19}
.tab{border-color:#374151;color:#e5e7eb}
.tab.active{background:#1f2937;border-color:#374151}
.card,.dialog,.subtabs-left{background:#0f1423;border-color:#374151}
th,td{border-bottom-color:#374151}
.table-wrap{border-color:#374151}
.table-wrap thead th{background:#0f1423}
.result{background:#111318;border-color:#374151}
.close{color:#e5e7eb}
.subtabs-left a{color:#e5e7eb;border-color:#374151;background:#0f1423}
.subtabs-left a.active{background:#1f2937}
}
body{overflow-x:hidden}
</style>
</head>
<body>
<!-- Unterreiter links AKTIV-Status serverseitig über request.path -->
<nav class="subtabs-left" aria-label="Unterreiter">
<a href="/strukturen"
class="{{ 'active' if request.path == '/strukturen' else '' }}">Aufträge</a>
<a href="/strukturen/mineralien"
class="{{ 'active' if request.path.startswith('/strukturen/mineralien') else '' }}">Mineralien</a>
</nav>
<div class="container">
<nav class="nav">
<a class="tab" href="/">Home</a>
<a class="tab active" href="/strukturen">Strukturen</a>
<a class="tab" href="/archiv">Archiv</a>
</nav>
<!-- Formular -->
<section class="card wide">
<h1>Struktur-Auftrag anlegen</h1>
<form method="post">
<div class="row">
<div>
<label for="structure-select">Struktur</label>
<select id="structure-select" name="structure" required>
<option value="" disabled selected>— bitte wählen —</option>
{% for s in structures %}<option value="{{ s }}">{{ s }}</option>{% endfor %}
</select>
</div>
<div>
<label for="quantity">Menge</label>
<input id="quantity" name="quantity" type="number" min="1" step="1" value="{{ quantity or 1 }}" required />
</div>
<div>
<label for="me">ME&nbsp;%</label>
<input id="me" name="me" type="number" min="0" max="10" step="1" value="{{ me_percent or 0 }}" />
</div>
<div>
<label for="system">System</label>
<select id="system" name="system">
{% for sys in systems %}<option value="{{ sys }}" {{ 'selected' if selected_system == sys else '' }}>{{ sys }}</option>{% endfor %}
</select>
</div>
</div>
<div class="row">
<div>
<label for="industry_structure">Industrie-Struktur</label>
<select id="industry_structure" name="industry_structure">
{% for x in ind_structs %}<option value="{{ x }}" {{ 'selected' if selected_ind_struct == x else '' }}>{{ x }}</option>{% endfor %}
</select>
</div>
<div>
<label for="industry_rig">Industrie-Rig</label>
<select id="industry_rig" name="industry_rig">
{% for x in ind_rigs %}<option value="{{ x }}" {{ 'selected' if selected_ind_rig == x else '' }}>{{ x }}</option>{% endfor %}
</select>
</div>
<div>
<label for="reaction_structure">Reaktions-Struktur</label>
<select id="reaction_structure" name="reaction_structure">
{% for x in reac_structs %}<option value="{{ x }}" {{ 'selected' if selected_reac_struct == x else '' }}>{{ x }}</option>{% endfor %}
</select>
</div>
<div>
<label for="reaction_rig">Reaktions-Rig</label>
<select id="reaction_rig" name="reaction_rig">
{% for x in reac_rigs %}<option value="{{ x }}" {{ 'selected' if selected_reac_rig == x else '' }}>{{ x }}</option>{% endfor %}
</select>
</div>
</div>
<div class="row">
<div>
<label for="facility_tax">Steuersatz&nbsp;% (facilityTax)</label>
<input id="facility_tax" name="facility_tax" type="number" min="0" max="50" step="0.1" value="0" />
</div>
<div style="grid-column: span 3">
<label for="notes">Notizen</label>
<textarea id="notes" name="notes" placeholder="Optionale Notizen zum Auftrag (max. ~2000 Zeichen)"></textarea>
<div class="muted" style="margin-top:.4rem">ME in 1er Schritten (010). Steuersatz in Prozent.</div>
</div>
</div>
<button type="submit" style="margin-top:.8rem">Auftrag hinzufügen</button>
</form>
</section>
<!-- Offene Aufträge -->
<section class="card full">
<div class="section-title">Offene Aufträge</div>
{% if open_orders %}
<div class="table-wrap scroll-5">
<table aria-label="Offene Aufträge">
<thead>
<tr>
<th>ID</th>
<th>Struktur</th>
<th>System</th>
<th class="col-ind-struct">Ind.-Struktur</th>
<th class="col-ind-rig">Ind.-Rig</th>
<th class="col-react-struct">Reakt.-Struktur</th>
<th class="col-react-rig">Reakt.-Rig</th>
<th class="num">Menge</th>
<th class="num">ME&nbsp;%</th>
<th class="col-notes">Notizen</th>
<th>Erstellt</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for o in open_orders %}
<tr>
<td class="id">{{ o.id[:8] }}…</td>
<td>{{ o.structure }}</td>
<td>{{ o.system|default('Jita') }}</td>
<td class="col-ind-struct">{{ o.industry_structure|default('Station') }}</td>
<td class="col-ind-rig">{{ o.industry_rig|default('None') }}</td>
<td class="col-react-struct">{{ o.reaction_structure|default('Athanor') }}</td>
<td class="col-react-rig">{{ o.reaction_rig|default('None') }}</td>
<td class="num">{{ o.quantity }}</td>
<td class="num">{{ o.me }}</td>
<td class="col-notes">{{ o.notes|default('') }}</td>
<td>{{ o.created_at|fmt_ts }}</td>
<td class="actions">
<a class="button" href="#m-{{ o.id }}">Details</a>
<button type="button" onclick="refreshOne('{{ o.id }}')">Aktualisieren</button>
<form method="post" action="/strukturen/done/{{ o.id }}"><button type="submit">Fertig</button></form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty">Keine offenen Aufträge.</div>
{% endif %}
</section>
<!-- Fertige Aufträge -->
<section class="card full">
<div class="section-title">Fertige Aufträge</div>
{% if done_orders %}
<div class="table-wrap scroll-5">
<table aria-label="Fertige Aufträge">
<thead>
<tr>
<th>ID</th>
<th>Struktur</th>
<th>System</th>
<th class="col-ind-struct">Ind.-Struktur</th>
<th class="col-ind-rig">Ind.-Rig</th>
<th class="col-react-struct">Reakt.-Struktur</th>
<th class="col-react-rig">Reakt.-Rig</th>
<th class="num">Menge</th>
<th class="num">ME&nbsp;%</th>
<th class="col-notes">Notizen</th>
<th>Erstellt</th>
<th>Fertig am</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for o in done_orders %}
<tr>
<td class="id">{{ o.id[:8] }}…</td>
<td>{{ o.structure }}</td>
<td>{{ o.system|default('Jita') }}</td>
<td class="col-ind-struct">{{ o.industry_structure|default('Station') }}</td>
<td class="col-ind-rig">{{ o.industry_rig|default('None') }}</td>
<td class="col-react-struct">{{ o.reaction_structure|default('Athanor') }}</td>
<td class="col-react-rig">{{ o.reaction_rig|default('None') }}</td>
<td class="num">{{ o.quantity }}</td>
<td class="num">{{ o.me }}</td>
<td class="col-notes">{{ o.notes|default('') }}</td>
<td>{{ o.created_at|fmt_ts }}</td>
<td>{{ o.done_at|fmt_ts }}</td>
<td class="actions">
<a class="button" href="#m-{{ o.id }}">Details</a>
<form method="post" action="/archiv/add/{{ o.id }}"><button type="submit">Archivieren</button></form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty">Noch keine fertigen Aufträge.</div>
{% endif %}
</section>
<!-- Kostenübersicht -->
<section class="card">
<div class="section-title" style="display:flex;justify-content:space-between;align-items:center">
<span>Kostenübersicht Offene Aufträge</span>
<button id="btn-refresh-all" type="button" onclick="refreshAll()">Aktualisieren</button>
</div>
<div id="costs-box" class="empty">Lade Kosten …</div>
<div id="costs-table" style="display:none">
<div class="table-wrap">
<table aria-label="Kostenübersicht">
<thead>
<tr>
<th>Struktur</th><th class="num">M</th><th class="num">ME</th><th>System</th><th>Industrie</th><th>Reaktion</th><th class="num">€/E</th><th class="num">Gesamt</th>
</tr>
</thead>
<tbody id="costs-body"></tbody>
<tfoot>
<tr><th colspan="7" class="num">Summe</th><th class="num"><strong id="costs-total">0 ISK</strong></th></tr>
</tfoot>
</table>
</div>
<div class="muted" id="costs-ts" style="margin-top:.4rem"></div>
</div>
</section>
<!-- Materialübersicht -->
<section class="card">
<div class="section-title" style="display:flex;justify-content:space-between;align-items:center">
<span>Materialübersicht Offene Aufträge</span>
<button id="btn-refresh-mats" type="button" onclick="refreshAll()">Aktualisieren</button>
</div>
<div id="mats-box" class="empty">Lade Materialien …</div>
<div id="mats-table" style="display:none">
<div class="table-wrap">
<table aria-label="Materialübersicht">
<thead>
<tr><th>Item</th><th class="num">Menge</th><th class="num">Kosten</th></tr>
</thead>
<tbody id="mats-body"></tbody>
<tfoot>
<tr><th colspan="2" class="num">Gesamtkosten</th><th class="num"><strong id="mats-total">0 ISK</strong></th></tr>
</tfoot>
</table>
</div>
<div class="muted" id="mats-ts" style="margin-top:.4rem"></div>
</div>
</section>
<!-- Modals -->
{% for o in open_orders + done_orders %}
<div id="m-{{ o.id }}" class="modal" aria-hidden="true">
<div class="dialog" role="dialog" aria-modal="true" aria-labelledby="h-{{ o.id }}">
<a class="close" href="#" aria-label="Schließen">×</a>
<h2 id="h-{{ o.id }}">Auftrag {{ o.structure }}</h2>
<div class="kv">
<div><strong>ID</strong></div><div class="id">{{ o.id }}</div>
<div><strong>Struktur</strong></div><div data-field="structure">{{ o.structure }}</div>
<div><strong>System</strong></div><div data-field="system">{{ o.system|default('Jita') }}</div>
<div><strong>Industrie-Struktur</strong></div><div data-field="industry_structure">{{ o.industry_structure|default('Station') }}</div>
<div><strong>Industrie-Rig</strong></div><div data-field="industry_rig">{{ o.industry_rig|default('None') }}</div>
<div><strong>Reaktions-Struktur</strong></div><div data-field="reaction_structure">{{ o.reaction_structure|default('Athanor') }}</div>
<div><strong>Reaktions-Rig</strong></div><div data-field="reaction_rig">{{ o.reaction_rig|default('None') }}</div>
<div><strong>Menge</strong></div><div data-field="quantity">{{ o.quantity }}</div>
<div><strong>ME&nbsp;%</strong></div><div data-field="me">{{ o.me }}</div>
<div><strong>Steuersatz&nbsp;%</strong></div><div data-field="facility_tax">{{ o.facility_tax|default(0) }}</div>
<div><strong>Notizen</strong></div><div class="note">{{ o.notes|default('') }}</div>
<div><strong>Erstellt</strong></div><div>{{ o.created_at|fmt_ts }}</div>
<div><strong>Blueprint&nbsp;ID</strong></div>
<div>
<input id="bp-{{ o.id }}" type="text" style="width:11rem"
value="{{ o.blueprint_type_id or '' }}" {{ '' if not o.blueprint_type_id else 'readonly' }} />
<button type="button" onclick="toggleEditBp('{{ o.id }}')">Bearbeiten</button>
<button type="button" onclick="saveBp('{{ o.id }}')">Speichern</button>
</div>
</div>
<hr class="sep" />
<h3 style="margin:.2rem 0 .4rem">Cookbook Kosten & Materialien</h3>
<div class="muted" style="margin-bottom:.4rem">Die Blueprint-ID ist vorausgefüllt und kann bei Bedarf überschrieben werden.</div>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;align-items:center;margin-bottom:.4rem">
<button type="button" onclick="fetchCookbook('{{ o.id }}')">Kosten abrufen</button>
<button type="button" onclick="fetchMaterials('{{ o.id }}')">Materialien abrufen</button>
<button type="button" onclick="refreshOne('{{ o.id }}')">Aktualisieren &amp; speichern</button>
<span id="cb-status-{{ o.id }}" class="muted"></span>
</div>
<div id="cb-total-{{ o.id }}" class="pill" style="display:none;margin-bottom:.5rem"></div>
<div id="cb-result-{{ o.id }}" class="result" style="display:none"></div>
<div id="cb-mats-{{ o.id }}" class="result" style="display:none"></div>
<div style="margin-top:.8rem">
{% if o.status == 'open' %}
<form method="post" action="/strukturen/done/{{ o.id }}" style="display:inline"><button type="submit">Als fertig markieren</button></form>
{% elif o.status == 'done' %}
<form method="post" action="/archiv/add/{{ o.id }}" style="display:inline"><button type="submit">Archivieren</button></form>
{% endif %}
<a class="button" href="#" style="margin-left:.4rem">Schließen</a>
</div>
</div>
</div>
{% endfor %}
</div>
<script>
function formatISK(x){const n=Number(x);if(!isFinite(n))return String(x);return n.toLocaleString('de-DE')+" ISK";}
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 textOf(c,s){const el=c.querySelector(s);return el?el.textContent.trim():"";}
function toggleEditBp(id){const i=document.getElementById("bp-"+id);i.readOnly=!i.readOnly;i.focus();}
function saveBp(id){
const i=document.getElementById("bp-"+id), v=(i.value||"").trim();
if(!/^[0-9]+$/.test(v)){alert("Bitte gültige Blueprint-TypeID eingeben.");return;}
fetch("/api/order/"+encodeURIComponent(id)+"/bp",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({blueprint_type_id:v})}).then(()=>{i.readOnly=true;});
}
/* ===== Kosten ===== */
function pullTotal(d){
if(d&&d.total!=null) return d.total;
if(d&&d.totalCost!=null) return d.totalCost;
if(d&&d.message&&d.message.totalCost!=null) return d.message.totalCost;
return null;
}
function renderCost(root,data){
const m=(data&&data.message&&typeof data.message==='object')?data.message:data;
const rows=[
["Kosten / Einheit", m.buildCostPerUnit],
["Produzierte Menge", m.producedQuantity],
["Materialkosten", m.materialCost],
["Jobkosten", m.jobCost],
["Überschüssige Materialien", m.excessMaterialsValue],
["Zusatzkosten", m.additionalCost],
["Gesamtkosten", pullTotal(data)]
].filter(r=>r[1]!=null);
let html='<table style="width:100%;border-collapse:collapse"><tbody>';
rows.forEach(([k,v],i)=>{const strong=i===rows.length-1?' style="font-weight:700"':'';html+=`<tr><th style="text-align:left;padding:.25rem .35rem">${k}</th><td class="num" style="padding:.25rem .35rem"${strong}>${typeof v==='number'?formatISK(v):String(v)}</td></tr>`;});
html+='</tbody></table>';
const det=document.createElement('details'); const sum=document.createElement('summary'); sum.textContent='Rohdaten';
const pre=document.createElement('pre'); pre.style.whiteSpace='pre-wrap'; pre.textContent=JSON.stringify(data,null,2);
det.appendChild(sum); det.appendChild(pre);
root.innerHTML=html; root.appendChild(det);
}
/* ===== Materialien ===== (robustes Parsing) */
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 dictRowsToArray(obj){
const vals = Object.values(obj||{});
if(!vals.length) return null;
if(vals.every(looksLikeMatRow)) return vals;
return null;
}
function extractMaterials(data){
if(!data) return null;
if(data.materials){
if(Array.isArray(data.materials)) return data.materials;
const arr = dictRowsToArray(data.materials); if(arr) return arr;
}
if(data.message && typeof data.message==='object'){
const m = data.message;
if(m.materials){
if(Array.isArray(m.materials)) return m.materials;
const arr = dictRowsToArray(m.materials); if(arr) return arr;
}
}
let found=null;
(function walk(x){
if(found||!x) return;
if(Array.isArray(x)){
if(x.length && looksLikeMatRow(x[0])){ found=x; return; }
for(const y of x){ walk(y); if(found) return; }
}else if(typeof x==='object'){
const asArr = dictRowsToArray(x);
if(asArr){ found=asArr; return; }
for(const v of Object.values(x)){ walk(v); if(found) return; }
}
})(data);
return found;
}
function renderMaterials(root,data){
const mats = extractMaterials(data);
let html = "";
if(!mats || !mats.length){
html += "Keine Materialien gefunden.";
} else {
const rows=mats.map(m=>{
const typeId = m.type_id ?? m.typeId ?? "";
const name = m.name ?? m.item_name ?? m.typeName ?? ('Type '+typeId);
const qty = (m.quantity ?? m.qty ?? m.amount ?? m.count ?? "");
const cost = (m.cost ?? m.cost_per_unit ?? m.unit_cost);
const costHtml = (cost!=null)?('<strong>'+formatISK(cost)+'</strong>'):"";
return `<tr><td>${name} ${typeId?`<span class="muted">(${typeId})</span>`:""}</td><td class="num">${qty}</td><td class="num">${costHtml}</td></tr>`;
}).join("");
html += '<h4 style="margin:.2rem 0 .5rem">Materialien (Manufacturing)</h4>'+
`<table style="width:100%;border-collapse:collapse"><thead><tr><th>Item</th><th class="num">Menge</th><th class="num">Kosten</th></tr></thead><tbody>${rows}</tbody></table>`;
}
const det=document.createElement('details'); const sum=document.createElement('summary'); sum.textContent='Rohdaten';
const pre=document.createElement('pre'); pre.style.whiteSpace='pre-wrap'; pre.textContent=JSON.stringify(data,null,2);
det.appendChild(sum); det.appendChild(pre);
root.innerHTML=html; root.appendChild(det);
}
function saveCache(id,payload){ fetch("/api/order/"+encodeURIComponent(id)+"/cache",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)}).catch(()=>{}); }
function loadCostsOverview(){
const box=document.getElementById("costs-box"), tbl=document.getElementById("costs-table"), body=document.getElementById("costs-body"), total=document.getElementById("costs-total"), ts=document.getElementById("costs-ts");
box.style.display="block"; box.textContent="Lade Kosten …"; tbl.style.display="none";
fetch("/api/cookbook/open_costs").then(r=>{if(!r.ok){return r.text().then(t=>{throw new Error('HTTP '+r.status+' - '+t.slice(0,200));});}return r.json();})
.then(d=>{
const items=d.items||[]; if(!items.length){box.textContent="Keine offenen Aufträge (oder keine Blueprint-IDs).";return;}
body.innerHTML=items.map(it=>{
const unit=(it.unit_cost!=null)?formatISK(it.unit_cost):"", tot=(it.total_cost!=null)?"<strong>"+formatISK(it.total_cost)+"</strong>":"";
const ind=(it.industry_structure||"")+((it.industry_rig&&it.industry_rig!=="None")?" / "+it.industry_rig:"");
const reac=(it.reaction_structure||"")+((it.reaction_rig&&it.reaction_rig!=="None")?" / "+it.reaction_rig:"");
const err=it.error?` <span class="err">(${it.error})</span>`:"";
return `<tr><td>${(it.structure||"")+err}</td><td class="num">${it.quantity||""}</td><td class="num">${it.me||""}</td><td>${it.system||""}</td><td>${ind}</td><td>${reac}</td><td class="num">${unit}</td><td class="num">${tot}</td></tr>`;
}).join("");
total.textContent=formatISK(d.total_cost||0);
ts.textContent=d.refreshed_at?("Stand: "+formatTS(d.refreshed_at)):"";
box.style.display="none"; tbl.style.display="block";
}).catch(err=>{box.innerHTML='<span class="err">Fehler: '+(err&&err.message?err.message:err)+'</span>'; tbl.style.display="none";});
}
function loadMatsOverview(){
const box=document.getElementById("mats-box"), tbl=document.getElementById("mats-table"), body=document.getElementById("mats-body"), total=document.getElementById("mats-total"), ts=document.getElementById("mats-ts");
box.style.display="block"; box.textContent="Lade Materialien …"; tbl.style.display="none";
fetch("/api/cookbook/open_materials").then(r=>{if(!r.ok){return r.text().then(t=>{throw new Error('HTTP '+r.status+' - '+t.slice(0,200));});}return r.json();})
.then(d=>{
const items=d.items||[]; if(!items.length){box.textContent="Keine Daten vorhanden.";return;}
body.innerHTML=items.map(it=>`<tr><td>${it.name||("Type "+it.type_id)} <span class="muted">(${it.type_id})</span></td><td class="num">${it.quantity!=null?it.quantity:""}</td><td class="num">${it.cost!=null?("<strong>"+formatISK(it.cost)+"</strong>"):""}</td></tr>`).join("");
total.textContent=formatISK(d.total_cost||0);
ts.textContent=d.refreshed_at?("Stand: "+formatTS(d.refreshed_at)):"";
box.style.display="none"; tbl.style.display="block";
}).catch(err=>{box.innerHTML='<span class="err">Fehler: '+(err&&err.message?err.message:err)+'</span>'; tbl.style.display="none";});
}
function refreshAll(){
const b1=document.getElementById("btn-refresh-all"), b2=document.getElementById("btn-refresh-mats");
if(b1) b1.disabled=true; if(b2) b2.disabled=true;
fetch("/api/orders/refresh_all",{method:"POST"}).then(r=>r.json()).then(()=>{
loadCostsOverview(); loadMatsOverview();
}).finally(()=>{ if(b1) b1.disabled=false; if(b2) b2.disabled=false; });
}
function refreshOne(id){
fetch("/api/order/"+encodeURIComponent(id)+"/refresh",{method:"POST"}).then(r=>r.json()).then(()=>{ loadCostsOverview(); loadMatsOverview(); });
}
document.addEventListener("DOMContentLoaded",function(){ loadCostsOverview(); loadMatsOverview(); });
</script>
</body>
</html>