first commit
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
[
|
||||
{
|
||||
"id": "b94528cb-ccf5-41d3-af05-dbd4a75b758e",
|
||||
"structure": "Athanor",
|
||||
"quantity": 1,
|
||||
"me": 0,
|
||||
"status": "done",
|
||||
"created_at": "2025-08-21T16:02:00.451895+00:00",
|
||||
"done_at": "2025-08-21T16:02:20.363756+00:00",
|
||||
"system": "Jita",
|
||||
"industry_structure": "Station",
|
||||
"industry_rig": "None",
|
||||
"reaction_structure": "Athanor",
|
||||
"reaction_rig": "None"
|
||||
},
|
||||
{
|
||||
"id": "e0c631c3-0a64-44ac-a64a-cc0647b2a2b9",
|
||||
"structure": "Raitaru",
|
||||
"quantity": 6,
|
||||
"me": 4,
|
||||
"status": "done",
|
||||
"created_at": "2025-08-21T16:02:17.240266+00:00",
|
||||
"done_at": "2025-08-21T16:02:21.570702+00:00",
|
||||
"system": "Jita",
|
||||
"industry_structure": "Station",
|
||||
"industry_rig": "None",
|
||||
"reaction_structure": "Athanor",
|
||||
"reaction_rig": "None"
|
||||
},
|
||||
{
|
||||
"id": "0bdf2545-b129-4856-bcd8-23837c4a04bf",
|
||||
"structure": "Athanor",
|
||||
"quantity": 5,
|
||||
"me": 10,
|
||||
"notes": "Test Auftrag",
|
||||
"status": "done",
|
||||
"created_at": "2025-08-21T16:13:36.220731+00:00",
|
||||
"done_at": "2025-08-21T16:13:42.494046+00:00",
|
||||
"system": "Jita",
|
||||
"industry_structure": "Station",
|
||||
"industry_rig": "None",
|
||||
"reaction_structure": "Athanor",
|
||||
"reaction_rig": "None"
|
||||
},
|
||||
{
|
||||
"id": "09bed2df-89a0-475a-a0bb-3e942f594e11",
|
||||
"structure": "Raitaru",
|
||||
"quantity": 2,
|
||||
"me": 3,
|
||||
"notes": "",
|
||||
"status": "done",
|
||||
"created_at": "2025-08-21T16:22:08.029669+00:00",
|
||||
"done_at": "2025-08-21T17:27:58.230171+00:00",
|
||||
"system": "Jita",
|
||||
"industry_structure": "Station",
|
||||
"industry_rig": "None",
|
||||
"reaction_structure": "Athanor",
|
||||
"reaction_rig": "None"
|
||||
},
|
||||
{
|
||||
"id": "32aea266-0f57-4101-b17e-051cca285381",
|
||||
"structure": "Astrahus",
|
||||
"quantity": 6,
|
||||
"me": 0,
|
||||
"notes": "",
|
||||
"status": "done",
|
||||
"created_at": "2025-08-21T17:28:04.199917+00:00",
|
||||
"done_at": "2025-08-21T19:56:26.687066+00:00",
|
||||
"system": "Jita",
|
||||
"industry_structure": "Station",
|
||||
"industry_rig": "None",
|
||||
"reaction_structure": "Athanor",
|
||||
"reaction_rig": "None"
|
||||
},
|
||||
{
|
||||
"id": "38c32e75-ca24-463f-924c-665731736541",
|
||||
"structure": "Athanor",
|
||||
"quantity": 2,
|
||||
"me": 6,
|
||||
"notes": "Test",
|
||||
"system": "Jita",
|
||||
"industry_structure": "Sotiyo",
|
||||
"industry_rig": "T1",
|
||||
"reaction_structure": "Tatara",
|
||||
"reaction_rig": "T1",
|
||||
"status": "done",
|
||||
"created_at": "2025-08-21T19:56:21.284564+00:00",
|
||||
"done_at": "2025-08-22T07:54:33+0100",
|
||||
"cookbook": {
|
||||
"error": 0,
|
||||
"status": 200,
|
||||
"message": {
|
||||
"materialCost": 1469368496.29,
|
||||
"jobCost": 450873981.87,
|
||||
"additionalCost": 0,
|
||||
"totalCost": 1920242478.16,
|
||||
"producedQuantity": 2,
|
||||
"buildCostPerUnit": 960121239.08,
|
||||
"excessMaterialsValue": 0,
|
||||
"blueprintTypeId": 36977,
|
||||
"blueprintName": "Athanor Blueprint"
|
||||
}
|
||||
},
|
||||
"last_updated": "2025-08-22T07:33:15+0100",
|
||||
"materials": {
|
||||
"materials": [
|
||||
{
|
||||
"type_id": 21959,
|
||||
"name": "Structure Reprocessing Plant",
|
||||
"quantity": 6.0,
|
||||
"cost_per_unit": 84744021.74,
|
||||
"cost": 508464130.44
|
||||
},
|
||||
{
|
||||
"type_id": 36957,
|
||||
"name": "Structure Acceleration Coils",
|
||||
"quantity": 2.0,
|
||||
"cost_per_unit": 152817857.14,
|
||||
"cost": 305635714.28
|
||||
},
|
||||
{
|
||||
"type_id": 36956,
|
||||
"name": "Structure Electromagnetic Sensor",
|
||||
"quantity": 2.0,
|
||||
"cost_per_unit": 141638461.54,
|
||||
"cost": 283276923.08
|
||||
},
|
||||
{
|
||||
"type_id": 21951,
|
||||
"name": "Structure Storage Bay",
|
||||
"quantity": 2.0,
|
||||
"cost_per_unit": 73250833.33,
|
||||
"cost": 146501666.66
|
||||
},
|
||||
{
|
||||
"type_id": 21949,
|
||||
"name": "Structure Hangar Array",
|
||||
"quantity": 2.0,
|
||||
"cost_per_unit": 53526739.13,
|
||||
"cost": 107053478.26
|
||||
},
|
||||
{
|
||||
"type_id": 21947,
|
||||
"name": "Structure Construction Parts",
|
||||
"quantity": 2.0,
|
||||
"cost_per_unit": 52829037.04,
|
||||
"cost": 105658074.08
|
||||
},
|
||||
{
|
||||
"type_id": 21961,
|
||||
"name": "Structure Docking Bay",
|
||||
"quantity": 2.0,
|
||||
"cost_per_unit": 48298064.52,
|
||||
"cost": 96596129.04
|
||||
},
|
||||
{
|
||||
"type_id": 21957,
|
||||
"name": "Structure Repair Facility",
|
||||
"quantity": 2.0,
|
||||
"cost_per_unit": 46331224.49,
|
||||
"cost": 92662448.98
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "6cbd23eaca8941319037b3ac14a4970e",
|
||||
"status": "done",
|
||||
"created_at": "2025-08-22T07:54:47+0100",
|
||||
"structure": "Raitaru",
|
||||
"system": "Jita",
|
||||
"industry_structure": "Sotiyo",
|
||||
"industry_rig": "T1",
|
||||
"reaction_structure": "Athanor",
|
||||
"reaction_rig": "T1",
|
||||
"quantity": 1,
|
||||
"me": 5,
|
||||
"notes": "Test",
|
||||
"blueprint_type_id": 36971,
|
||||
"cookbook": {
|
||||
"error": 0,
|
||||
"status": 200,
|
||||
"message": {
|
||||
"materialCost": 494601665.69,
|
||||
"jobCost": 169775434.21,
|
||||
"additionalCost": 0,
|
||||
"totalCost": 664377099.9,
|
||||
"producedQuantity": 1,
|
||||
"buildCostPerUnit": 664377099.9,
|
||||
"excessMaterialsValue": 0,
|
||||
"blueprintTypeId": 36971,
|
||||
"blueprintName": "Raitaru Blueprint"
|
||||
}
|
||||
},
|
||||
"last_updated": "2025-08-22T07:59:54+0100",
|
||||
"materials": {
|
||||
"materials": [
|
||||
{
|
||||
"type_id": 21955,
|
||||
"name": "Structure Factory",
|
||||
"quantity": 1.0,
|
||||
"cost_per_unit": 106032424.24,
|
||||
"cost": 106032424.24
|
||||
},
|
||||
{
|
||||
"type_id": 21959,
|
||||
"name": "Structure Reprocessing Plant",
|
||||
"quantity": 1.0,
|
||||
"cost_per_unit": 84744021.74,
|
||||
"cost": 84744021.74
|
||||
},
|
||||
{
|
||||
"type_id": 21951,
|
||||
"name": "Structure Storage Bay",
|
||||
"quantity": 1.0,
|
||||
"cost_per_unit": 73250833.33,
|
||||
"cost": 73250833.33
|
||||
},
|
||||
{
|
||||
"type_id": 21953,
|
||||
"name": "Structure Laboratory",
|
||||
"quantity": 1.0,
|
||||
"cost_per_unit": 71164821.43,
|
||||
"cost": 71164821.43
|
||||
},
|
||||
{
|
||||
"type_id": 21967,
|
||||
"name": "Structure Office Center",
|
||||
"quantity": 1.0,
|
||||
"cost_per_unit": 67570149.25,
|
||||
"cost": 67570149.25
|
||||
},
|
||||
{
|
||||
"type_id": 21949,
|
||||
"name": "Structure Hangar Array",
|
||||
"quantity": 1.0,
|
||||
"cost_per_unit": 53526739.13,
|
||||
"cost": 53526739.13
|
||||
},
|
||||
{
|
||||
"type_id": 21947,
|
||||
"name": "Structure Construction Parts",
|
||||
"quantity": 1.0,
|
||||
"cost_per_unit": 52829037.04,
|
||||
"cost": 52829037.04
|
||||
},
|
||||
{
|
||||
"type_id": 21961,
|
||||
"name": "Structure Docking Bay",
|
||||
"quantity": 1.0,
|
||||
"cost_per_unit": 48298064.52,
|
||||
"cost": 48298064.52
|
||||
},
|
||||
{
|
||||
"type_id": 21957,
|
||||
"name": "Structure Repair Facility",
|
||||
"quantity": 1.0,
|
||||
"cost_per_unit": 46331224.49,
|
||||
"cost": 46331224.49
|
||||
}
|
||||
]
|
||||
},
|
||||
"done_at": "2025-08-22T08:00:14+0100"
|
||||
},
|
||||
{
|
||||
"id": "e4661b4a72064437a7f33ced2d446cde",
|
||||
"status": "done",
|
||||
"created_at": "2025-08-22T08:00:05+0100",
|
||||
"structure": "Athanor",
|
||||
"system": "Jita",
|
||||
"industry_structure": "Sotiyo",
|
||||
"industry_rig": "T1",
|
||||
"reaction_structure": "Athanor",
|
||||
"reaction_rig": "T1",
|
||||
"quantity": 1,
|
||||
"me": 0,
|
||||
"notes": "",
|
||||
"blueprint_type_id": 36977,
|
||||
"done_at": "2025-08-22T08:00:15+0100"
|
||||
},
|
||||
{
|
||||
"id": "a453ec6587c44d8ebdbb5156c58c9ec7",
|
||||
"status": "done",
|
||||
"created_at": "2025-08-22T08:15:52+0100",
|
||||
"structure": "Keepstar",
|
||||
"system": "Jita",
|
||||
"industry_structure": "Sotiyo",
|
||||
"industry_rig": "T1",
|
||||
"reaction_structure": "Athanor",
|
||||
"reaction_rig": "T1",
|
||||
"quantity": 1,
|
||||
"me": 10,
|
||||
"notes": "",
|
||||
"blueprint_type_id": 36968,
|
||||
"cookbook": {
|
||||
"error": 0,
|
||||
"status": 200,
|
||||
"message": {
|
||||
"materialCost": 156812981803.45,
|
||||
"jobCost": 74872180070.6,
|
||||
"additionalCost": 0,
|
||||
"totalCost": 231685161874.05,
|
||||
"producedQuantity": 1,
|
||||
"buildCostPerUnit": 231685161874.05,
|
||||
"excessMaterialsValue": 0,
|
||||
"blueprintTypeId": 36968,
|
||||
"blueprintName": "Keepstar Blueprint"
|
||||
}
|
||||
},
|
||||
"last_updated": "2025-08-22T08:37:05+0100",
|
||||
"materials": {
|
||||
"materials": [
|
||||
{
|
||||
"type_id": 21963,
|
||||
"name": "Structure Market Network",
|
||||
"quantity": 713.0,
|
||||
"cost_per_unit": 166346666.67,
|
||||
"cost": 118605173335.71
|
||||
},
|
||||
{
|
||||
"type_id": 21951,
|
||||
"name": "Structure Storage Bay",
|
||||
"quantity": 179.0,
|
||||
"cost_per_unit": 73250833.33,
|
||||
"cost": 13111899166.07
|
||||
},
|
||||
{
|
||||
"type_id": 21967,
|
||||
"name": "Structure Office Center",
|
||||
"quantity": 179.0,
|
||||
"cost_per_unit": 67570149.25,
|
||||
"cost": 12095056715.75
|
||||
},
|
||||
{
|
||||
"type_id": 21949,
|
||||
"name": "Structure Hangar Array",
|
||||
"quantity": 179.0,
|
||||
"cost_per_unit": 53526739.13,
|
||||
"cost": 9581286304.27
|
||||
},
|
||||
{
|
||||
"type_id": 21947,
|
||||
"name": "Structure Construction Parts",
|
||||
"quantity": 179.0,
|
||||
"cost_per_unit": 52829037.04,
|
||||
"cost": 9456397630.16
|
||||
},
|
||||
{
|
||||
"type_id": 21965,
|
||||
"name": "Structure Medical Center",
|
||||
"quantity": 179.0,
|
||||
"cost_per_unit": 50834772.73,
|
||||
"cost": 9099424318.67
|
||||
},
|
||||
{
|
||||
"type_id": 21961,
|
||||
"name": "Structure Docking Bay",
|
||||
"quantity": 179.0,
|
||||
"cost_per_unit": 48298064.52,
|
||||
"cost": 8645353549.08
|
||||
},
|
||||
{
|
||||
"type_id": 21957,
|
||||
"name": "Structure Repair Facility",
|
||||
"quantity": 179.0,
|
||||
"cost_per_unit": 46331224.49,
|
||||
"cost": 8293289183.71
|
||||
},
|
||||
{
|
||||
"type_id": 21955,
|
||||
"name": "Structure Factory",
|
||||
"quantity": 72.0,
|
||||
"cost_per_unit": 106032424.24,
|
||||
"cost": 7634334545.28
|
||||
},
|
||||
{
|
||||
"type_id": 36958,
|
||||
"name": "Structure Advertisement Nexus",
|
||||
"quantity": 72.0,
|
||||
"cost_per_unit": 86114000.0,
|
||||
"cost": 6200208000.0
|
||||
},
|
||||
{
|
||||
"type_id": 21959,
|
||||
"name": "Structure Reprocessing Plant",
|
||||
"quantity": 72.0,
|
||||
"cost_per_unit": 84744021.74,
|
||||
"cost": 6101569565.28
|
||||
},
|
||||
{
|
||||
"type_id": 21953,
|
||||
"name": "Structure Laboratory",
|
||||
"quantity": 72.0,
|
||||
"cost_per_unit": 71164821.43,
|
||||
"cost": 5123867142.96
|
||||
},
|
||||
{
|
||||
"type_id": 36957,
|
||||
"name": "Structure Acceleration Coils",
|
||||
"quantity": 8.0,
|
||||
"cost_per_unit": 152817857.14,
|
||||
"cost": 1222542857.12
|
||||
},
|
||||
{
|
||||
"type_id": 36956,
|
||||
"name": "Structure Electromagnetic Sensor",
|
||||
"quantity": 8.0,
|
||||
"cost_per_unit": 141638461.54,
|
||||
"cost": 1133107692.32
|
||||
},
|
||||
{
|
||||
"type_id": 3810,
|
||||
"name": "Marines",
|
||||
"quantity": 891.0,
|
||||
"cost_per_unit": 7474.2,
|
||||
"cost": 6659512.2
|
||||
},
|
||||
{
|
||||
"type_id": 13267,
|
||||
"name": "Janitor",
|
||||
"quantity": 223.0,
|
||||
"cost_per_unit": 6525.04,
|
||||
"cost": 1455083.92
|
||||
},
|
||||
{
|
||||
"type_id": 21969,
|
||||
"name": "Structure Mission Network",
|
||||
"quantity": 8.0,
|
||||
"cost_per_unit": 0.0,
|
||||
"cost": 0.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"done_at": "2025-08-22T11:25:47+0100"
|
||||
},
|
||||
{
|
||||
"id": "f14623326be343e790e769a0de50d43a",
|
||||
"status": "done",
|
||||
"created_at": "2025-08-22T09:45:25+0100",
|
||||
"structure": "Athanor",
|
||||
"system": "Jita",
|
||||
"industry_structure": "Sotiyo",
|
||||
"industry_rig": "T1",
|
||||
"reaction_structure": "Athanor",
|
||||
"reaction_rig": "None",
|
||||
"quantity": 1,
|
||||
"me": 5,
|
||||
"notes": "",
|
||||
"blueprint_type_id": 36977,
|
||||
"done_at": "2025-08-22T11:46:35+0100"
|
||||
},
|
||||
{
|
||||
"id": "bfa32ea583ba44e58872ceb84518c546",
|
||||
"status": "done",
|
||||
"created_at": "2025-08-22T11:45:30+0100",
|
||||
"structure": "Keepstar",
|
||||
"system": "Jita",
|
||||
"industry_structure": "Sotiyo",
|
||||
"industry_rig": "T2",
|
||||
"reaction_structure": "Tatara",
|
||||
"reaction_rig": "T2",
|
||||
"quantity": 1,
|
||||
"me": 6,
|
||||
"notes": "Test",
|
||||
"blueprint_type_id": 36968,
|
||||
"done_at": "2025-08-22T11:46:27+0100"
|
||||
},
|
||||
{
|
||||
"id": "63eedb769b854322a75606380954b748",
|
||||
"status": "done",
|
||||
"created_at": "2025-08-22T11:46:53+0100",
|
||||
"structure": "Fortizar",
|
||||
"system": "Amarr",
|
||||
"industry_structure": "Sotiyo",
|
||||
"industry_rig": "T2",
|
||||
"reaction_structure": "Athanor",
|
||||
"reaction_rig": "None",
|
||||
"quantity": 1,
|
||||
"me": 0,
|
||||
"notes": "",
|
||||
"blueprint_type_id": 36967,
|
||||
"done_at": "2025-08-22T12:13:41+0100"
|
||||
},
|
||||
{
|
||||
"id": "644be14fb305436ca035eaeea7d7a4e7",
|
||||
"status": "done",
|
||||
"created_at": "2025-08-22T11:47:47+0100",
|
||||
"structure": "Raitaru",
|
||||
"system": "Jita",
|
||||
"industry_structure": "Sotiyo",
|
||||
"industry_rig": "T1",
|
||||
"reaction_structure": "Athanor",
|
||||
"reaction_rig": "None",
|
||||
"quantity": 5,
|
||||
"me": 0,
|
||||
"notes": "",
|
||||
"blueprint_type_id": 36971,
|
||||
"done_at": "2025-08-22T12:13:43+0100"
|
||||
},
|
||||
{
|
||||
"id": "6a1c0282910d4f9d95b0fac76438f725",
|
||||
"status": "open",
|
||||
"created_at": "2025-08-22T12:13:50+0100",
|
||||
"structure": "Astrahus",
|
||||
"system": "Jita",
|
||||
"industry_structure": "Sotiyo",
|
||||
"industry_rig": "T1",
|
||||
"reaction_structure": "Athanor",
|
||||
"reaction_rig": "None",
|
||||
"quantity": 1,
|
||||
"me": 5,
|
||||
"notes": "",
|
||||
"blueprint_type_id": 36966,
|
||||
"cache": {
|
||||
"materials": {
|
||||
"error": 0,
|
||||
"message": {
|
||||
"additionalCost": 0,
|
||||
"blueprintName": "Astrahus Blueprint",
|
||||
"blueprintTypeId": 36966,
|
||||
"buildCostPerUnit": 1180755487.44,
|
||||
"excessMaterialsValue": 0,
|
||||
"jobCost": 346965820.03,
|
||||
"materialCost": 833789667.41,
|
||||
"producedQuantity": 1,
|
||||
"totalCost": 1180755487.44
|
||||
},
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
from webapp import create_app
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = create_app()
|
||||
app.run(host="127.0.0.1", port=5000, debug=True)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
Flask==3.1.2
|
||||
Requests==2.32.5
|
||||
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
from flask import Flask, render_template
|
||||
from datetime import datetime
|
||||
|
||||
# bestehende Blueprints
|
||||
from .routes.structures import bp as structures_bp
|
||||
from .routes.cookbook import bp as cookbook_bp
|
||||
from .routes.archive import bp as archive_bp
|
||||
# NEU: Blueprint-ID API
|
||||
from .routes.blueprints import bp as blueprints_api_bp
|
||||
|
||||
def create_app() -> Flask:
|
||||
app = Flask(__name__, template_folder="templates")
|
||||
|
||||
# Jinja-Filter für Timestamps (wird in Templates genutzt)
|
||||
def fmt_ts(value):
|
||||
if not value:
|
||||
return ""
|
||||
try:
|
||||
# akzeptiert "YYYY-mm-ddTHH:MM:SS" etc.
|
||||
if isinstance(value, (int, float)):
|
||||
return datetime.fromtimestamp(value).strftime("%Y-%m-%d %H:%M")
|
||||
s = str(value).replace("T", " ").split("+", 1)[0]
|
||||
return s[:16]
|
||||
except Exception:
|
||||
return str(value)
|
||||
app.jinja_env.filters["fmt_ts"] = fmt_ts
|
||||
|
||||
# Blueprints registrieren
|
||||
app.register_blueprint(structures_bp)
|
||||
app.register_blueprint(cookbook_bp)
|
||||
app.register_blueprint(archive_bp)
|
||||
app.register_blueprint(blueprints_api_bp) # <-- neu
|
||||
|
||||
# Home + Favicon
|
||||
@app.get("/")
|
||||
def home():
|
||||
return render_template("index.html")
|
||||
|
||||
@app.get("/favicon.ico")
|
||||
def _favicon():
|
||||
return ("", 204)
|
||||
|
||||
return app
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
+48130
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,27 @@
|
||||
# webapp/routes/archive.py
|
||||
from __future__ import annotations
|
||||
import time
|
||||
from flask import Blueprint, render_template, redirect, url_for
|
||||
from webapp.storage.orders import load_orders, update_order
|
||||
|
||||
bp = Blueprint("archive", __name__)
|
||||
|
||||
@bp.get("/archiv")
|
||||
def archive_index():
|
||||
"""Zeigt ausschließlich Aufträge mit status == 'archived'."""
|
||||
archived = [o for o in load_orders() if o.get("status") == "archived"]
|
||||
archived.sort(
|
||||
key=lambda o: (o.get("archived_at") or o.get("done_at") or o.get("created_at") or ""),
|
||||
reverse=True,
|
||||
)
|
||||
return render_template("archive.html", archived_orders=archived)
|
||||
|
||||
@bp.post("/archiv/add/<order_id>")
|
||||
def archive_add(order_id: str):
|
||||
"""
|
||||
Markiert einen Auftrag als archiviert.
|
||||
Danach BLEIBEN wir auf der Strukturen-Seite (kein Wechsel zur Archiv-Seite).
|
||||
"""
|
||||
now = time.strftime("%Y-%m-%dT%H:%M:%S%z", time.gmtime())
|
||||
update_order(order_id, {"status": "archived", "archived_at": now})
|
||||
return redirect(url_for("structures.structures")) # zurück zu /strukturen
|
||||
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
from flask import Blueprint, request, jsonify
|
||||
from webapp.services.blueprints import (
|
||||
resolve_blueprint_id,
|
||||
resolve_blueprint_id_by_product_id,
|
||||
)
|
||||
|
||||
bp = Blueprint("blueprints_api", __name__)
|
||||
|
||||
@bp.get("/api/blueprint_id")
|
||||
def api_blueprint_id():
|
||||
"""
|
||||
Liefert die Blueprint-TypeID.
|
||||
Query-Parameter:
|
||||
- name: Produktname (z. B. "Structure Market Network")
|
||||
- productTypeId: Produkt-TypeID (optional, wenn 'name' fehlt)
|
||||
Antwort: { ok: true, blueprint_type_id: 12345 } oder { ok:false, error: "..." }
|
||||
"""
|
||||
name = (request.args.get("name") or "").strip()
|
||||
pid = request.args.get("productTypeId")
|
||||
|
||||
bp_tid = None
|
||||
if name:
|
||||
bp_tid = resolve_blueprint_id(name)
|
||||
elif pid:
|
||||
try:
|
||||
bp_tid = resolve_blueprint_id_by_product_id(int(pid))
|
||||
except Exception:
|
||||
bp_tid = None
|
||||
|
||||
if not bp_tid:
|
||||
return jsonify({"ok": False, "error": "not-found"}), 404
|
||||
|
||||
return jsonify({"ok": True, "blueprint_type_id": int(bp_tid)})
|
||||
@@ -0,0 +1,293 @@
|
||||
from __future__ import annotations
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from flask import Blueprint, request, jsonify
|
||||
import time
|
||||
|
||||
# Services
|
||||
from webapp.services.ccookbook import (
|
||||
call_cookbook_build_cost,
|
||||
call_eve_ref_materials,
|
||||
)
|
||||
# Storage
|
||||
from webapp.storage.orders import (
|
||||
load_orders, update_order,
|
||||
cache_costs, cache_materials,
|
||||
)
|
||||
|
||||
bp = Blueprint("cookbook", __name__, url_prefix="/api")
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%S%z", time.gmtime())
|
||||
|
||||
# ----------------------------- Helpers ----------------------------------------
|
||||
|
||||
def _arg(name: str, default: str = "") -> str:
|
||||
v = request.args.get(name)
|
||||
return v if v is not None else default
|
||||
|
||||
def _to_int(s: str, default: int = 0) -> int:
|
||||
try:
|
||||
return int(float(s))
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
def _order_by_id(order_id: str) -> Dict[str, Any] | None:
|
||||
for o in load_orders():
|
||||
if o.get("id") == order_id:
|
||||
return o
|
||||
return None
|
||||
|
||||
# ----------------------------- Endpoints – Raw --------------------------------
|
||||
|
||||
@bp.get("/cookbook/build_cost")
|
||||
def api_build_cost():
|
||||
"""Kosten via evecookbook (Preise, Gesamt etc.)."""
|
||||
bps = request.args.getlist("blueprintTypeId")
|
||||
if not bps and request.args.get("blueprintTypeId"):
|
||||
bps = [request.args.get("blueprintTypeId")]
|
||||
|
||||
if not bps:
|
||||
return jsonify({"error": "missing blueprintTypeId"}), 400
|
||||
|
||||
try:
|
||||
data = call_cookbook_build_cost(
|
||||
blueprint_type_ids=bps,
|
||||
quantity=_to_int(_arg("quantity", "1")),
|
||||
price_mode=_arg("priceMode", "buy"),
|
||||
additional_costs=_to_int(_arg("additionalCosts", "0")),
|
||||
base_me=_to_int(_arg("baseMe", "0")),
|
||||
components_me=_to_int(_arg("componentsMe", "0")),
|
||||
system=_arg("system", "Jita"),
|
||||
facility_tax=_to_int(_arg("facilityTax", "0")),
|
||||
industry_structure_type=_arg("industryStructureType", "Station"),
|
||||
industry_rig=_arg("industryRig", "None"),
|
||||
reaction_structure_type=_arg("reactionStructureType", "Athanor"),
|
||||
reaction_rig=_arg("reactionRig", "None"),
|
||||
reaction_flag=_arg("reactionFlag", ""),
|
||||
blueprint_version=_arg("blueprintVersion", "tq"),
|
||||
)
|
||||
return jsonify(data)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 502
|
||||
|
||||
|
||||
@bp.get("/cookbook/materials")
|
||||
def api_materials():
|
||||
"""
|
||||
Materialliste via EVE Ref.
|
||||
Erwartet: blueprintTypeId, optional quantity/baseMe/industryStructureType.
|
||||
"""
|
||||
if not request.args.get("blueprintTypeId"):
|
||||
return jsonify({"error": "missing blueprintTypeId"}), 400
|
||||
try:
|
||||
data = call_eve_ref_materials(dict(request.args))
|
||||
# Sicherstellen, dass immer eine Array-Liste unter "materials" steht
|
||||
mats = data.get("materials") or []
|
||||
if isinstance(mats, dict):
|
||||
mats = list(mats.values())
|
||||
data["materials"] = mats
|
||||
return jsonify(data)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 502
|
||||
|
||||
# ----------------------------- Endpoints – Cache/Order ------------------------
|
||||
|
||||
@bp.post("/order/<order_id>/bp")
|
||||
def api_set_bp(order_id: str):
|
||||
payload = request.get_json(silent=True) or {}
|
||||
bp_id = payload.get("blueprint_type_id")
|
||||
if not bp_id:
|
||||
return jsonify({"error": "missing blueprint_type_id"}), 400
|
||||
ok = update_order(order_id, {"blueprint_type_id": int(bp_id), "last_updated": _now()})
|
||||
return jsonify({"ok": bool(ok)})
|
||||
|
||||
@bp.post("/order/<order_id>/cache")
|
||||
def api_cache(order_id: str):
|
||||
payload = request.get_json(silent=True) or {}
|
||||
ok = False
|
||||
if "costs" in payload:
|
||||
ok = cache_costs(order_id, payload["costs"])
|
||||
if "materials" in payload:
|
||||
ok = cache_materials(order_id, payload["materials"])
|
||||
return jsonify({"ok": bool(ok)})
|
||||
|
||||
@bp.post("/order/<order_id>/refresh")
|
||||
def api_refresh_one(order_id: str):
|
||||
o = _order_by_id(order_id)
|
||||
if not o:
|
||||
return jsonify({"error": "order not found"}), 404
|
||||
bp_id = o.get("blueprint_type_id")
|
||||
if not bp_id:
|
||||
return jsonify({"error": "blueprint_type_id missing"}), 400
|
||||
|
||||
# Basis-Parameter aus Order
|
||||
p = {
|
||||
"blueprintTypeId": str(bp_id),
|
||||
"quantity": str(o.get("quantity", 1)),
|
||||
"priceMode": "buy",
|
||||
"additionalCosts": "0",
|
||||
"baseMe": str(o.get("me", 0)),
|
||||
"componentsMe": str(o.get("me", 0)),
|
||||
"system": o.get("system", "Jita"),
|
||||
"facilityTax": str(o.get("facility_tax", 0)),
|
||||
"industryStructureType": o.get("industry_structure", "Station"),
|
||||
"industryRig": o.get("industry_rig", "None"),
|
||||
"reactionStructureType": o.get("reaction_structure", "Athanor"),
|
||||
"reactionRig": o.get("reaction_rig", "None"),
|
||||
"reactionFlag": "",
|
||||
"blueprintVersion": "tq",
|
||||
}
|
||||
|
||||
# Kosten
|
||||
try:
|
||||
costs = call_cookbook_build_cost([bp_id],
|
||||
quantity=int(p["quantity"]),
|
||||
price_mode=p["priceMode"],
|
||||
additional_costs=int(p["additionalCosts"]),
|
||||
base_me=int(p["baseMe"]),
|
||||
components_me=int(p["componentsMe"]),
|
||||
system=p["system"],
|
||||
facility_tax=int(float(p["facilityTax"])),
|
||||
industry_structure_type=p["industryStructureType"],
|
||||
industry_rig=p["industryRig"],
|
||||
reaction_structure_type=p["reactionStructureType"],
|
||||
reaction_rig=p["reactionRig"],
|
||||
reaction_flag=p["reactionFlag"],
|
||||
blueprint_version=p["blueprintVersion"],
|
||||
)
|
||||
cache_costs(order_id, costs)
|
||||
except Exception as e:
|
||||
costs = {"error": str(e)}
|
||||
|
||||
# Materialien
|
||||
try:
|
||||
mats = call_eve_ref_materials(p)
|
||||
if isinstance(mats.get("materials"), dict):
|
||||
mats["materials"] = list(mats["materials"].values())
|
||||
cache_materials(order_id, mats)
|
||||
except Exception as e:
|
||||
mats = {"error": str(e)}
|
||||
|
||||
return jsonify({"ok": True, "costs": costs, "materials": mats})
|
||||
|
||||
@bp.post("/orders/refresh_all")
|
||||
def api_refresh_all():
|
||||
orders = [o for o in load_orders() if o.get("status") == "open" and o.get("blueprint_type_id")]
|
||||
done = 0
|
||||
for o in orders:
|
||||
try:
|
||||
request.environ["werkzeug.server.shutdown"] # no-op
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_ = api_refresh_one(o["id"])
|
||||
done += 1
|
||||
except Exception:
|
||||
continue
|
||||
return jsonify({"ok": True, "refreshed": done})
|
||||
|
||||
# ----------------------------- Endpoints – Aggregation ------------------------
|
||||
|
||||
@bp.get("/cookbook/open_costs")
|
||||
def api_open_costs():
|
||||
"""
|
||||
Aggregiert Kosten je Order (nutzt Cache; fehlt er, wird berechnet).
|
||||
"""
|
||||
items: List[Dict[str, Any]] = []
|
||||
total = 0.0
|
||||
for o in load_orders():
|
||||
if o.get("status") != "open" or not o.get("blueprint_type_id"):
|
||||
continue
|
||||
|
||||
entry: Dict[str, Any] = {
|
||||
"structure": o.get("structure"),
|
||||
"quantity": o.get("quantity"),
|
||||
"me": o.get("me"),
|
||||
"system": o.get("system"),
|
||||
"industry_structure": o.get("industry_structure"),
|
||||
"industry_rig": o.get("industry_rig"),
|
||||
"reaction_structure": o.get("reaction_structure"),
|
||||
"reaction_rig": o.get("reaction_rig"),
|
||||
}
|
||||
|
||||
costs = o.get("cookbook")
|
||||
if not costs:
|
||||
try:
|
||||
costs = call_cookbook_build_cost([o["blueprint_type_id"]],
|
||||
quantity=int(o.get("quantity", 1)),
|
||||
price_mode="buy",
|
||||
additional_costs=0,
|
||||
base_me=int(o.get("me", 0)),
|
||||
components_me=int(o.get("me", 0)),
|
||||
system=o.get("system", "Jita"),
|
||||
facility_tax=int(float(o.get("facility_tax", 0))),
|
||||
industry_structure_type=o.get("industry_structure", "Station"),
|
||||
industry_rig=o.get("industry_rig", "None"),
|
||||
reaction_structure_type=o.get("reaction_structure", "Athanor"),
|
||||
reaction_rig=o.get("reaction_rig", "None"),
|
||||
reaction_flag="",
|
||||
blueprint_version="tq",
|
||||
)
|
||||
cache_costs(o["id"], costs)
|
||||
except Exception as e:
|
||||
entry["error"] = str(e)
|
||||
items.append(entry)
|
||||
continue
|
||||
|
||||
msg = costs.get("message") or costs
|
||||
unit = msg.get("buildCostPerUnit") or msg.get("unitCost")
|
||||
tot = msg.get("totalCost") or msg.get("total") or 0
|
||||
entry["unit_cost"] = unit
|
||||
entry["total_cost"] = tot
|
||||
total += float(tot or 0)
|
||||
items.append(entry)
|
||||
|
||||
return jsonify({"items": items, "total_cost": total, "refreshed_at": _now()})
|
||||
|
||||
@bp.get("/cookbook/open_materials")
|
||||
def api_open_materials():
|
||||
"""
|
||||
Aggregiert Materialien über alle offenen Orders.
|
||||
"""
|
||||
agg: Dict[int, Dict[str, Any]] = {}
|
||||
total_cost = 0.0
|
||||
|
||||
def _add(tid: int, name: str, qty: float, cost: float | None):
|
||||
row = agg.setdefault(tid, {"type_id": tid, "name": name, "quantity": 0, "cost": 0})
|
||||
row["quantity"] += float(qty or 0)
|
||||
if cost is not None:
|
||||
row["cost"] += float(cost or 0)
|
||||
|
||||
for o in load_orders():
|
||||
if o.get("status") != "open" or not o.get("blueprint_type_id"):
|
||||
continue
|
||||
|
||||
mats = o.get("materials")
|
||||
if not mats:
|
||||
try:
|
||||
mats = call_eve_ref_materials({
|
||||
"blueprintTypeId": str(o["blueprint_type_id"]),
|
||||
"quantity": str(o.get("quantity", 1)),
|
||||
"baseMe": str(o.get("me", 0)),
|
||||
"industryStructureType": o.get("industry_structure", "Station"),
|
||||
"facilityTax": str(o.get("facility_tax", 0)),
|
||||
})
|
||||
if isinstance(mats.get("materials"), dict):
|
||||
mats["materials"] = list(mats["materials"].values())
|
||||
cache_materials(o["id"], mats)
|
||||
except Exception:
|
||||
mats = {"materials": []}
|
||||
|
||||
for m in mats.get("materials") or []:
|
||||
tid = int(m.get("type_id") or m.get("typeId") or 0)
|
||||
if not tid:
|
||||
continue
|
||||
name = m.get("name") or f"Type {tid}"
|
||||
qty = m.get("quantity") or m.get("qty") or m.get("amount") or 0
|
||||
cost = m.get("cost") or m.get("cost_per_unit") or m.get("unit_cost")
|
||||
_add(tid, name, qty, cost)
|
||||
if cost:
|
||||
total_cost += float(cost or 0)
|
||||
|
||||
items = sorted(agg.values(), key=lambda r: (r.get("cost") or 0), reverse=True)
|
||||
return jsonify({"items": items, "total_cost": total_cost, "refreshed_at": _now()})
|
||||
@@ -0,0 +1,86 @@
|
||||
from __future__ import annotations
|
||||
import uuid, time
|
||||
from typing import List, Dict, Any
|
||||
from flask import Blueprint, render_template, request, redirect, url_for
|
||||
|
||||
from webapp.storage.orders import load_orders, add_order, mark_done
|
||||
from webapp.services.blueprints import resolve_blueprint_id
|
||||
|
||||
bp = Blueprint("structures", __name__)
|
||||
|
||||
STRUCTURES = ["Athanor", "Raitaru", "Astrahus", "Fortizar", "Keepstar", "Tatara", "Sotiyo"]
|
||||
SYSTEMS = ["Jita", "Perimeter", "Amarr", "Dodixie", "Rens", "Hek", "OGV-AS", "B-9C24", "K-6K16", "PUIG-F"]
|
||||
IND_STRUCTS = ["Station", "Raitaru", "Azbel", "Sotiyo"]
|
||||
IND_RIGS = ["None", "T1", "T2"]
|
||||
REAC_STRUCTS= ["Athanor", "Tatara"]
|
||||
REAC_RIGS = ["None", "T1", "T2"]
|
||||
|
||||
def _split_orders() -> Dict[str, List[Dict[str, Any]]]:
|
||||
orders = load_orders()
|
||||
open_orders = [o for o in orders if o.get("status") == "open"]
|
||||
done_orders = [o for o in orders if o.get("status") == "done"]
|
||||
open_orders.sort(key=lambda o: o.get("created_at",""), reverse=True)
|
||||
done_orders.sort(key=lambda o: o.get("done_at","") or "", reverse=True)
|
||||
return {"open": open_orders, "done": done_orders}
|
||||
|
||||
@bp.route("/strukturen", methods=["GET", "POST"])
|
||||
def structures():
|
||||
if request.method == "POST":
|
||||
structure = request.form.get("structure","").strip()
|
||||
quantity = int(request.form.get("quantity") or 1)
|
||||
me = int(request.form.get("me") or 0)
|
||||
system = request.form.get("system","Jita")
|
||||
ind_struct= request.form.get("industry_structure","Station")
|
||||
ind_rig = request.form.get("industry_rig","None")
|
||||
reac_struct = request.form.get("reaction_structure","Athanor")
|
||||
reac_rig = request.form.get("reaction_rig","None")
|
||||
notes = request.form.get("notes","")
|
||||
|
||||
oid = uuid.uuid4().hex
|
||||
now = time.strftime("%Y-%m-%dT%H:%M:%S%z", time.gmtime())
|
||||
bp_id = resolve_blueprint_id(structure)
|
||||
|
||||
add_order({
|
||||
"id": oid,
|
||||
"status": "open",
|
||||
"created_at": now,
|
||||
"structure": structure,
|
||||
"system": system,
|
||||
"industry_structure": ind_struct,
|
||||
"industry_rig": ind_rig,
|
||||
"reaction_structure": reac_struct,
|
||||
"reaction_rig": reac_rig,
|
||||
"quantity": quantity,
|
||||
"me": me,
|
||||
"notes": notes,
|
||||
"blueprint_type_id": bp_id
|
||||
})
|
||||
return redirect(url_for("structures.structures"))
|
||||
|
||||
parts = _split_orders()
|
||||
return render_template(
|
||||
"structures.html",
|
||||
structures=STRUCTURES,
|
||||
systems=SYSTEMS,
|
||||
ind_structs=IND_STRUCTS,
|
||||
ind_rigs=IND_RIGS,
|
||||
reac_structs=REAC_STRUCTS,
|
||||
reac_rigs=REAC_RIGS,
|
||||
open_orders=parts["open"],
|
||||
done_orders=parts["done"],
|
||||
selected_system="Jita",
|
||||
selected_ind_struct="Station",
|
||||
selected_ind_rig="None",
|
||||
selected_reac_struct="Athanor",
|
||||
selected_reac_rig="None",
|
||||
)
|
||||
|
||||
# ←← NEU/ WICHTIG: exakte Unterseiten-Route
|
||||
@bp.get("/strukturen/mineralien")
|
||||
def mineralien():
|
||||
return render_template("minerals.html")
|
||||
|
||||
@bp.post("/strukturen/done/<order_id>")
|
||||
def done(order_id: str):
|
||||
mark_done(order_id)
|
||||
return redirect(url_for("structures.structures"))
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
import csv
|
||||
import os
|
||||
import json
|
||||
from typing import Optional, Dict
|
||||
from flask import current_app
|
||||
|
||||
# Cache: "name in lowercase" -> type_id
|
||||
_BP_NAME2ID: Dict[str, int] = {}
|
||||
# Cache: product type id -> (product name)
|
||||
_ID2NAME: Dict[int, str] = {}
|
||||
# Optional benutzerdefinierte Überschreibungen
|
||||
# Format: { "Structure Market Network": 123456, ... }
|
||||
_USER_MAP: Dict[str, int] = {}
|
||||
|
||||
|
||||
def _data_path(filename: str) -> str:
|
||||
return os.path.join(current_app.root_path, "data", filename)
|
||||
|
||||
|
||||
def _ensure_loaded() -> None:
|
||||
"""Liest typeIDs.csv einmalig ein und baut schnelle Nachschlage-Maps auf."""
|
||||
global _BP_NAME2ID, _ID2NAME, _USER_MAP
|
||||
if _BP_NAME2ID and _ID2NAME:
|
||||
return
|
||||
|
||||
# Benutzer-Overrides (optional)
|
||||
try:
|
||||
override = _data_path("blueprints.json")
|
||||
if os.path.isfile(override):
|
||||
with open(override, "r", encoding="utf-8") as f:
|
||||
_USER_MAP = json.load(f) or {}
|
||||
except Exception:
|
||||
_USER_MAP = {}
|
||||
|
||||
csv_path = _data_path("typeIDs.csv")
|
||||
if not os.path.isfile(csv_path):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(csv_path, "r", encoding="utf-8", newline="") as f:
|
||||
# Datei ist ;-separiert: id;name;group_id;iconID;graphicID
|
||||
reader = csv.reader(f, delimiter=";")
|
||||
header = next(reader, None) # "id;name;group_id;iconID;graphicID"
|
||||
for row in reader:
|
||||
if not row or len(row) < 2:
|
||||
continue
|
||||
try:
|
||||
tid = int(row[0])
|
||||
except Exception:
|
||||
continue
|
||||
name = (row[1] or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
_ID2NAME[tid] = name
|
||||
key = name.lower()
|
||||
# Merke auch die Blaupause separat (Name endet exakt auf " Blueprint")
|
||||
if key.endswith(" blueprint"):
|
||||
_BP_NAME2ID[key] = tid
|
||||
except Exception:
|
||||
# Map bleibt ggf. leer; Aufrufer bekommen dann None zurück
|
||||
pass
|
||||
|
||||
|
||||
def product_name_by_id(type_id: int) -> Optional[str]:
|
||||
"""Gibt den Produktnamen zur TypeID zurück (aus typeIDs.csv)."""
|
||||
_ensure_loaded()
|
||||
return _ID2NAME.get(type_id)
|
||||
|
||||
|
||||
def resolve_blueprint_id(product_name: str) -> Optional[int]:
|
||||
"""
|
||||
Ermittelt die Blueprint-TypeID zu einem *Produktnamen*.
|
||||
Regeln:
|
||||
1) Benutzer-Override (blueprints.json)
|
||||
2) Exakter Treffer "<Produktname> Blueprint" in typeIDs.csv
|
||||
3) Weicher Treffer (enthält Produktname & 'blueprint')
|
||||
"""
|
||||
if not product_name:
|
||||
return None
|
||||
|
||||
_ensure_loaded()
|
||||
|
||||
# 1) User-Override
|
||||
if product_name in _USER_MAP:
|
||||
return int(_USER_MAP[product_name])
|
||||
|
||||
wanted_exact = f"{product_name} Blueprint".lower()
|
||||
|
||||
# 2) Exakter Treffer
|
||||
if wanted_exact in _BP_NAME2ID:
|
||||
return _BP_NAME2ID[wanted_exact]
|
||||
|
||||
# 3) Weiche Suche
|
||||
prod_l = product_name.lower()
|
||||
for k, tid in _BP_NAME2ID.items():
|
||||
if prod_l in k and "blueprint" in k:
|
||||
return tid
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resolve_blueprint_id_by_product_id(product_type_id: int) -> Optional[int]:
|
||||
"""Ermittelt die Blueprint-TypeID aus der *Produkt*-TypeID."""
|
||||
name = product_name_by_id(int(product_type_id))
|
||||
if not name:
|
||||
return None
|
||||
return resolve_blueprint_id(name)
|
||||
@@ -0,0 +1,145 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
from urllib.parse import urljoin
|
||||
import requests
|
||||
|
||||
__all__ = ["call_cookbook_build_cost", "call_eve_ref_materials"]
|
||||
|
||||
# ---------- HTTP Session mit Retries (für evecookbook + everef) ---------------
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
_SESSION = requests.Session()
|
||||
_RETRY = Retry(
|
||||
total=3, connect=3, read=3,
|
||||
backoff_factor=0.3,
|
||||
status_forcelist=(429, 500, 502, 503, 504),
|
||||
allowed_methods=frozenset(["GET", "POST"]),
|
||||
)
|
||||
_ADAPTER = HTTPAdapter(max_retries=_RETRY)
|
||||
_SESSION.mount("http://", _ADAPTER)
|
||||
_SESSION.mount("https://", _ADAPTER)
|
||||
|
||||
# ---------- EVE Cookbook: Build Cost ------------------------------------------
|
||||
|
||||
def call_cookbook_build_cost(
|
||||
blueprint_type_ids: Union[List[str], List[int]],
|
||||
*,
|
||||
quantity: int = 1,
|
||||
price_mode: str = "buy",
|
||||
additional_costs: int = 0,
|
||||
base_me: int = 0,
|
||||
components_me: int = 0,
|
||||
system: str = "Jita",
|
||||
facility_tax: int = 0,
|
||||
industry_structure_type: str = "Station",
|
||||
industry_rig: str = "None",
|
||||
reaction_structure_type: str = "Athanor",
|
||||
reaction_rig: str = "None",
|
||||
reaction_flag: str = "",
|
||||
blueprint_version: str = "tq",
|
||||
timeout: Tuple[float, float] = (5.0, 25.0),
|
||||
) -> Dict[str, Any]:
|
||||
base = "https://evecookbook.com/api/"
|
||||
url = urljoin(base, "buildCost")
|
||||
|
||||
params: List[Tuple[str, Any]] = []
|
||||
for bid in blueprint_type_ids:
|
||||
params.append(("blueprintTypeId", str(bid)))
|
||||
params.extend([
|
||||
("quantity", str(int(quantity))),
|
||||
("priceMode", price_mode),
|
||||
("additionalCosts", str(int(additional_costs))),
|
||||
("baseMe", str(int(base_me))),
|
||||
("componentsMe", str(int(components_me))),
|
||||
("system", system),
|
||||
("facilityTax", str(int(facility_tax))),
|
||||
("industryStructureType", industry_structure_type),
|
||||
("industryRig", industry_rig),
|
||||
("reactionStructureType", reaction_structure_type),
|
||||
("reactionRig", reaction_rig),
|
||||
("reactionFlag", reaction_flag),
|
||||
("blueprintVersion", blueprint_version),
|
||||
])
|
||||
|
||||
headers = {"Accept": "application/json"}
|
||||
resp = _SESSION.get(url, params=params, headers=headers, timeout=timeout)
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
except requests.HTTPError as e:
|
||||
try:
|
||||
detail = resp.json()
|
||||
except Exception:
|
||||
detail = resp.text[:600]
|
||||
raise RuntimeError(f"Cookbook API {resp.status_code}: {detail}") from e
|
||||
|
||||
try:
|
||||
return resp.json() if resp.content else {}
|
||||
except Exception:
|
||||
return {"raw": resp.text}
|
||||
|
||||
# ---------- EVE Ref: Materials (BOM) ------------------------------------------
|
||||
|
||||
EVEREF_COST_URL = "https://api.everef.net/v1/industry/cost"
|
||||
REFDATA_TYPE_URL = "https://ref-data.everef.net/types/{type_id}"
|
||||
|
||||
_TYPE_NAME_CACHE: Dict[int, str] = {}
|
||||
|
||||
def _type_name(type_id: int) -> str:
|
||||
if type_id in _TYPE_NAME_CACHE:
|
||||
return _TYPE_NAME_CACHE[type_id]
|
||||
try:
|
||||
r = _SESSION.get(REFDATA_TYPE_URL.format(type_id=type_id), timeout=10)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
name = (data.get("name", {}) or {}).get("en") or data.get("name") or str(type_id)
|
||||
except Exception:
|
||||
name = str(type_id)
|
||||
_TYPE_NAME_CACHE[type_id] = name
|
||||
return name
|
||||
|
||||
def call_eve_ref_materials(params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Holt Materialliste (Manufacturing) zu einer Blueprint-ID.
|
||||
Erwartet mindestens: blueprintTypeId; optional: quantity, baseMe, industryStructureType.
|
||||
"""
|
||||
bp_id = int(params.get("blueprintTypeId"))
|
||||
runs = int(params.get("quantity", 1))
|
||||
me = int(params.get("baseMe", 0))
|
||||
te = int(params.get("baseTe", 0)) if params.get("baseTe") is not None else 0
|
||||
|
||||
structure_map = {"Station": None, "Raitaru": 35825, "Azbel": 35826, "Sotiyo": 35827}
|
||||
st_name = params.get("industryStructureType", "Station")
|
||||
st_id = structure_map.get(st_name)
|
||||
|
||||
q: Dict[str, Any] = {"blueprint_id": bp_id, "runs": runs, "me": me, "te": te}
|
||||
if st_id:
|
||||
q["structure_type_id"] = st_id
|
||||
|
||||
r = _SESSION.get(EVEREF_COST_URL, params=q, timeout=20)
|
||||
r.raise_for_status()
|
||||
payload = r.json()
|
||||
|
||||
manuf = (payload.get("manufacturing") or {})
|
||||
if not manuf:
|
||||
return {"materials": []}
|
||||
|
||||
first_key = next(iter(manuf.keys()))
|
||||
mats = manuf[first_key].get("materials", {}) or {}
|
||||
|
||||
out: List[Dict[str, Any]] = []
|
||||
for _, row in mats.items():
|
||||
try:
|
||||
tid = int(row.get("type_id"))
|
||||
except Exception:
|
||||
continue
|
||||
out.append({
|
||||
"type_id": tid,
|
||||
"name": _type_name(tid),
|
||||
"quantity": row.get("quantity"),
|
||||
"cost_per_unit": row.get("cost_per_unit"),
|
||||
"cost": row.get("cost"),
|
||||
})
|
||||
|
||||
out.sort(key=lambda x: (x.get("cost") or 0), reverse=True)
|
||||
return {"materials": out}
|
||||
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
from urllib.parse import urljoin
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
__all__ = ["call_cookbook_build_cost", "call_eve_ref_materials"]
|
||||
|
||||
_SESSION = requests.Session()
|
||||
_RETRY = Retry(total=3, connect=3, read=3, backoff_factor=0.3,
|
||||
status_forcelist=(429,500,502,503,504),
|
||||
allowed_methods=frozenset(["GET","POST"]))
|
||||
_ADAPTER = HTTPAdapter(max_retries=_RETRY)
|
||||
_SESSION.mount("http://", _ADAPTER)
|
||||
_SESSION.mount("https://", _ADAPTER)
|
||||
|
||||
def call_cookbook_build_cost(
|
||||
blueprint_type_ids: Union[List[str], List[int]],
|
||||
*,
|
||||
quantity: int = 1,
|
||||
price_mode: str = "buy",
|
||||
additional_costs: int = 0,
|
||||
base_me: int = 0,
|
||||
components_me: int = 0,
|
||||
system: str = "Jita",
|
||||
facility_tax: int = 0,
|
||||
industry_structure_type: str = "Station",
|
||||
industry_rig: str = "None",
|
||||
reaction_structure_type: str = "Athanor",
|
||||
reaction_rig: str = "None",
|
||||
reaction_flag: str = "",
|
||||
blueprint_version: str = "tq",
|
||||
timeout: Tuple[float, float] = (5.0, 25.0),
|
||||
) -> Dict[str, Any]:
|
||||
base = "https://evecookbook.com/api/"
|
||||
url = urljoin(base, "buildCost")
|
||||
|
||||
params: List[Tuple[str, Any]] = []
|
||||
for bid in blueprint_type_ids:
|
||||
params.append(("blueprintTypeId", str(bid)))
|
||||
params.extend([
|
||||
("quantity", str(int(quantity))),
|
||||
("priceMode", price_mode),
|
||||
("additionalCosts", str(int(additional_costs))),
|
||||
("baseMe", str(int(base_me))),
|
||||
("componentsMe", str(int(components_me))),
|
||||
("system", system),
|
||||
("facilityTax", str(int(facility_tax))),
|
||||
("industryStructureType", industry_structure_type),
|
||||
("industryRig", industry_rig),
|
||||
("reactionStructureType", reaction_structure_type),
|
||||
("reactionRig", reaction_rig),
|
||||
("reactionFlag", reaction_flag),
|
||||
("blueprintVersion", blueprint_version),
|
||||
])
|
||||
|
||||
headers = {"Accept": "application/json"}
|
||||
resp = _SESSION.get(url, params=params, headers=headers, timeout=timeout)
|
||||
resp.raise_for_status()
|
||||
try:
|
||||
return resp.json() if resp.content else {}
|
||||
except Exception:
|
||||
return {"raw": resp.text}
|
||||
|
||||
EVEREF_COST_URL = "https://api.everef.net/v1/industry/cost"
|
||||
REFDATA_TYPE_URL = "https://ref-data.everef.net/types/{type_id}"
|
||||
_TYPE_NAME_CACHE: Dict[int, str] = {}
|
||||
|
||||
def _type_name(type_id: int) -> str:
|
||||
if type_id in _TYPE_NAME_CACHE:
|
||||
return _TYPE_NAME_CACHE[type_id]
|
||||
try:
|
||||
r = _SESSION.get(REFDATA_TYPE_URL.format(type_id=type_id), timeout=10)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
name = (data.get("name", {}) or {}).get("en") or data.get("name") or str(type_id)
|
||||
except Exception:
|
||||
name = str(type_id)
|
||||
_TYPE_NAME_CACHE[type_id] = name
|
||||
return name
|
||||
|
||||
def call_eve_ref_materials(params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
bp_id = int(params.get("blueprintTypeId"))
|
||||
runs = int(params.get("quantity", 1))
|
||||
me = int(params.get("baseMe", 0))
|
||||
te = int(params.get("baseTe", 0)) if params.get("baseTe") is not None else 0
|
||||
|
||||
structure_map = {"Station": None, "Raitaru": 35825, "Azbel": 35826, "Sotiyo": 35827}
|
||||
st_name = params.get("industryStructureType", "Station")
|
||||
st_id = structure_map.get(st_name)
|
||||
|
||||
q: Dict[str, Any] = {"blueprint_id": bp_id, "runs": runs, "me": me, "te": te}
|
||||
if st_id:
|
||||
q["structure_type_id"] = st_id
|
||||
|
||||
r = _SESSION.get(EVEREF_COST_URL, params=q, timeout=20)
|
||||
r.raise_for_status()
|
||||
payload = r.json()
|
||||
|
||||
manuf = (payload.get("manufacturing") or {})
|
||||
if not manuf:
|
||||
return {"materials": []}
|
||||
|
||||
first_key = next(iter(manuf.keys()))
|
||||
mats = manuf[first_key].get("materials", {}) or {}
|
||||
|
||||
out: List[Dict[str, Any]] = []
|
||||
for _, row in mats.items():
|
||||
try:
|
||||
tid = int(row.get("type_id"))
|
||||
except Exception:
|
||||
continue
|
||||
out.append({
|
||||
"type_id": tid,
|
||||
"name": _type_name(tid),
|
||||
"quantity": row.get("quantity"),
|
||||
"cost_per_unit": row.get("cost_per_unit"),
|
||||
"cost": row.get("cost"),
|
||||
})
|
||||
out.sort(key=lambda x: (x.get("cost") or 0), reverse=True)
|
||||
return {"materials": out}
|
||||
@@ -0,0 +1,2 @@
|
||||
from .orders import load_orders, add_order, mark_done
|
||||
__all__ = ["load_orders", "add_order", "mark_done"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
import os, json, time
|
||||
from typing import List, Dict, Any
|
||||
from flask import current_app
|
||||
|
||||
FILE_NAME = "orders.json"
|
||||
|
||||
def _file_path() -> str:
|
||||
return os.path.join(current_app.root_path, "data", FILE_NAME)
|
||||
|
||||
def load_orders() -> List[Dict[str, Any]]:
|
||||
p = _file_path()
|
||||
if not os.path.isfile(p): return []
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8") as f:
|
||||
return json.load(f) or []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _save_orders(orders: List[Dict[str, Any]]) -> None:
|
||||
p = _file_path()
|
||||
os.makedirs(os.path.dirname(p), exist_ok=True)
|
||||
with open(p, "w", encoding="utf-8") as f:
|
||||
json.dump(orders, f, ensure_ascii=False, indent=2)
|
||||
|
||||
def add_order(order: Dict[str, Any]) -> None:
|
||||
orders = load_orders()
|
||||
orders.append(order)
|
||||
_save_orders(orders)
|
||||
|
||||
def mark_done(order_id: str) -> bool:
|
||||
orders = load_orders()
|
||||
changed = False
|
||||
now = time.strftime("%Y-%m-%dT%H:%M:%S%z", time.gmtime())
|
||||
for o in orders:
|
||||
if o.get("id") == order_id and o.get("status") != "done":
|
||||
o["status"] = "done"
|
||||
o["done_at"] = now
|
||||
changed = True
|
||||
if changed: _save_orders(orders)
|
||||
return changed
|
||||
|
||||
def update_order(order_id: str, patch: Dict[str, Any]) -> bool:
|
||||
orders = load_orders()
|
||||
ok = False
|
||||
for o in orders:
|
||||
if o.get("id") == order_id:
|
||||
o.update(patch)
|
||||
ok = True
|
||||
break
|
||||
if ok: _save_orders(orders)
|
||||
return ok
|
||||
|
||||
def cache_costs(order_id: str, costs: Dict[str, Any]) -> bool:
|
||||
now = time.strftime("%Y-%m-%dT%H:%M:%S%z", time.gmtime())
|
||||
return update_order(order_id, {"cookbook": costs, "last_updated": now})
|
||||
|
||||
def cache_materials(order_id: str, mats: Dict[str, Any]) -> bool:
|
||||
now = time.strftime("%Y-%m-%dT%H:%M:%S%z", time.gmtime())
|
||||
return update_order(order_id, {"materials": mats, "last_updated": now})
|
||||
@@ -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 %</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 %</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 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>Cookbook‑Rohdaten</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>Material‑Rohdaten</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>
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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 %</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 % (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 (0–10). 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 %</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 %</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 %</strong></div><div data-field="me">{{ o.me }}</div>
|
||||
<div><strong>Steuersatz %</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 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 & 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>
|
||||
Reference in New Issue
Block a user