first commit
This commit is contained in:
532
data/orders.json
Normal file
532
data/orders.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
8
main.py
Normal file
8
main.py
Normal file
@@ -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)
|
||||
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Flask==3.1.2
|
||||
Requests==2.32.5
|
||||
44
webapp/__init__.py
Normal file
44
webapp/__init__.py
Normal file
@@ -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
|
||||
BIN
webapp/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
webapp/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
1761
webapp/data/orders.json
Normal file
1761
webapp/data/orders.json
Normal file
File diff suppressed because it is too large
Load Diff
48130
webapp/data/typeIDs.csv
Normal file
48130
webapp/data/typeIDs.csv
Normal file
File diff suppressed because it is too large
Load Diff
0
webapp/routes/__init__.py
Normal file
0
webapp/routes/__init__.py
Normal file
BIN
webapp/routes/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
webapp/routes/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
webapp/routes/__pycache__/archive.cpython-313.pyc
Normal file
BIN
webapp/routes/__pycache__/archive.cpython-313.pyc
Normal file
Binary file not shown.
BIN
webapp/routes/__pycache__/blueprints.cpython-313.pyc
Normal file
BIN
webapp/routes/__pycache__/blueprints.cpython-313.pyc
Normal file
Binary file not shown.
BIN
webapp/routes/__pycache__/cookbook.cpython-313.pyc
Normal file
BIN
webapp/routes/__pycache__/cookbook.cpython-313.pyc
Normal file
Binary file not shown.
BIN
webapp/routes/__pycache__/structures.cpython-313.pyc
Normal file
BIN
webapp/routes/__pycache__/structures.cpython-313.pyc
Normal file
Binary file not shown.
27
webapp/routes/archive.py
Normal file
27
webapp/routes/archive.py
Normal file
@@ -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
|
||||
34
webapp/routes/blueprints.py
Normal file
34
webapp/routes/blueprints.py
Normal file
@@ -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)})
|
||||
293
webapp/routes/cookbook.py
Normal file
293
webapp/routes/cookbook.py
Normal file
@@ -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()})
|
||||
86
webapp/routes/structures.py
Normal file
86
webapp/routes/structures.py
Normal file
@@ -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"))
|
||||
0
webapp/services/__init__.py
Normal file
0
webapp/services/__init__.py
Normal file
BIN
webapp/services/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
webapp/services/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
webapp/services/__pycache__/blueprints.cpython-313.pyc
Normal file
BIN
webapp/services/__pycache__/blueprints.cpython-313.pyc
Normal file
Binary file not shown.
BIN
webapp/services/__pycache__/ccookbook.cpython-313.pyc
Normal file
BIN
webapp/services/__pycache__/ccookbook.cpython-313.pyc
Normal file
Binary file not shown.
108
webapp/services/blueprints.py
Normal file
108
webapp/services/blueprints.py
Normal file
@@ -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)
|
||||
145
webapp/services/ccookbook.py
Normal file
145
webapp/services/ccookbook.py
Normal file
@@ -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}
|
||||
121
webapp/services/cookbook.py
Normal file
121
webapp/services/cookbook.py
Normal file
@@ -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}
|
||||
2
webapp/storage/__init__.py
Normal file
2
webapp/storage/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .orders import load_orders, add_order, mark_done
|
||||
__all__ = ["load_orders", "add_order", "mark_done"]
|
||||
BIN
webapp/storage/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
webapp/storage/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
webapp/storage/__pycache__/orders.cpython-313.pyc
Normal file
BIN
webapp/storage/__pycache__/orders.cpython-313.pyc
Normal file
Binary file not shown.
BIN
webapp/storage/__pycache__/sql_store.cpython-313.pyc
Normal file
BIN
webapp/storage/__pycache__/sql_store.cpython-313.pyc
Normal file
Binary file not shown.
60
webapp/storage/orders.py
Normal file
60
webapp/storage/orders.py
Normal file
@@ -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})
|
||||
175
webapp/templates/archive.html
Normal file
175
webapp/templates/archive.html
Normal file
@@ -0,0 +1,175 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>EVE Web Helper – Archiv</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:2rem}
|
||||
.container{max-width:1440px;margin:0 auto}
|
||||
|
||||
.nav{display:flex;gap:.6rem;margin-bottom:1rem}
|
||||
.tab{padding:.5rem .8rem;border:1px solid #e5e7eb;border-radius:.6rem;text-decoration:none;color:#111827}
|
||||
.tab.active{background:#111827;color:#fff;border-color:#111827}
|
||||
|
||||
.card{border:1px solid #e5e7eb;border-radius:.8rem;padding:1rem 1.2rem;margin-bottom:1rem;background:#fff}
|
||||
.full{width:100%}
|
||||
.section-title{font-weight:700;margin-bottom:.6rem}
|
||||
.table-wrap{overflow:auto;border:1px solid #e5e7eb;border-radius:.6rem}
|
||||
.table-wrap table{width:100%;border-collapse:collapse;margin:0;table-layout:fixed}
|
||||
th,td{padding:.5rem .45rem;border-bottom:1px solid #e5e7eb;text-align:left;vertical-align:top;word-break:break-word;overflow-wrap:anywhere}
|
||||
th{font-weight:700}
|
||||
.id{font-family:ui-monospace,SFMono-Regular,Consolas,Menlo,monospace;font-size:.85em;max-width:90px}
|
||||
.num{text-align:right}
|
||||
.empty{padding:.65rem .8rem;border:1px dashed #d1d5db;border-radius:.6rem;color:#6b7280;background:transparent}
|
||||
|
||||
/* Modal (nur lesen) */
|
||||
.modal{position:fixed;inset:0;display:none;align-items:flex-start;justify-content:center;padding:4vh 1rem;background:rgba(0,0,0,.45);z-index:999}
|
||||
.modal:target{display:flex}
|
||||
.dialog{max-width:1100px;width:min(92vw,1100px);background:#fff;border-radius:14px;padding:1rem 1.2rem;box-shadow:0 20px 45px rgba(0,0,0,.35);max-height:92vh;overflow:auto;position:relative}
|
||||
.close{position:sticky;top:0;float:right;font-size:1.4rem;text-decoration:none;color:#111827;padding:.2rem .5rem;border-radius:.4rem}
|
||||
.kv{display:grid;grid-template-columns:180px 1fr;gap:.35rem .8rem}
|
||||
.pill{display:inline-block;background:#111827;color:#fff;border-radius:999px;padding:.2rem .55rem;font-size:.85rem;margin-bottom:.5rem}
|
||||
.result{border:1px solid #e5e7eb;border-radius:.6rem;padding:.7rem .8rem;background:#fafafa;margin-top:.6rem}
|
||||
|
||||
@media (prefers-color-scheme:dark){
|
||||
body{background:#0b0f19;color:#e5e7eb}
|
||||
.tab{border-color:#374151;color:#e5e7eb}
|
||||
.tab.active{background:#1f2937;border-color:#374151}
|
||||
.card,.dialog{background:#0f1423;border-color:#374151}
|
||||
th,td{border-bottom-color:#374151}
|
||||
.table-wrap{border-color:#374151}
|
||||
.result{background:#111318;border-color:#374151}
|
||||
.close{color:#e5e7eb}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<nav class="nav">
|
||||
<a class="tab" href="/">Home</a>
|
||||
<a class="tab" href="/strukturen">Strukturen</a>
|
||||
<a class="tab active" href="/archiv">Archiv</a>
|
||||
</nav>
|
||||
|
||||
<section class="card full">
|
||||
<div class="section-title">Archivierte / fertige Aufträge</div>
|
||||
|
||||
{% if archived_orders %}
|
||||
<div class="table-wrap">
|
||||
<table aria-label="Archivierte Aufträge">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Struktur</th>
|
||||
<th>System</th>
|
||||
<th>Ind.-Struktur</th>
|
||||
<th>Ind.-Rig</th>
|
||||
<th>Reakt.-Struktur</th>
|
||||
<th>Reakt.-Rig</th>
|
||||
<th class="num">Menge</th>
|
||||
<th class="num">ME %</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>
|
||||
78
webapp/templates/base.html
Normal file
78
webapp/templates/base.html
Normal file
@@ -0,0 +1,78 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>EVE Web Helper – {% block title %}{% endblock %}</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:2rem}
|
||||
.container{max-width:1440px;margin:0 auto}
|
||||
|
||||
/* Einheitliche Tabs */
|
||||
.nav{display:flex;gap:.6rem;margin-bottom:1.2rem}
|
||||
.tab{padding:.5rem .8rem;border:1px solid #e5e7eb;border-radius:.6rem;text-decoration:none;color:#111827}
|
||||
.tab.active{background:#111827;color:#fff;border-color:#111827}
|
||||
|
||||
/* Grund-UI */
|
||||
.card{border:1px solid #e5e7eb;border-radius:.8rem;padding:1rem 1.2rem;margin-bottom:1rem;background:#fff}
|
||||
.wide{max-width:1200px}
|
||||
.full{width:100%}
|
||||
h1{font-size:1.35rem;margin:.2rem 0 1rem}
|
||||
label{font-weight:600;display:block;margin-bottom:.35rem}
|
||||
select,input,textarea,button,a.button{padding:.5rem .7rem;border:1px solid #d1d5db;border-radius:.45rem}
|
||||
textarea{width:100%;min-height:90px;resize:vertical}
|
||||
button,a.button{background:#111827;color:#fff;cursor:pointer;text-decoration:none;display:inline-block}
|
||||
.muted{color:#6b7280;font-size:.9rem}
|
||||
|
||||
table{width:100%;border-collapse:collapse;table-layout:fixed}
|
||||
th,td{padding:.5rem .45rem;border-bottom:1px solid #e5e7eb;text-align:left;vertical-align:top;word-break:break-word;overflow-wrap:anywhere}
|
||||
th{font-weight:700}
|
||||
.id{font-family:ui-monospace,SFMono-Regular,Consolas,Menlo,monospace;font-size:.85em;max-width:90px}
|
||||
.num{text-align:right}
|
||||
.actions form{display:inline}
|
||||
.empty{padding:.65rem .8rem;border:1px dashed #d1d5db;border-radius:.6rem;color:#6b7280;background:transparent}
|
||||
.table-wrap{overflow:auto;border:1px solid #e5e7eb;border-radius:.6rem}
|
||||
.table-wrap thead th{position:sticky;top:0;background:#fff;z-index:1}
|
||||
|
||||
/* Modal (global) */
|
||||
.modal{position:fixed;inset:0;display:none;align-items:flex-start;justify-content:center;padding:4vh 1rem;background:rgba(0,0,0,.45);z-index:999}
|
||||
.modal:target{display:flex}
|
||||
.dialog{max-width:1100px;width:min(92vw,1100px);background:#fff;border-radius:14px;padding:1rem 1.2rem;box-shadow:0 20px 45px rgba(0,0,0,.35);max-height:92vh;overflow:auto;position:relative;z-index:1000;pointer-events:auto}
|
||||
.modal *{pointer-events:auto}
|
||||
.modal h2{margin:.2rem 0 1rem;font-size:1.2rem}
|
||||
.close{position:sticky;top:0;float:right;font-size:1.4rem;text-decoration:none;color:#111827;padding:.2rem .5rem;border-radius:.4rem;z-index:2}
|
||||
.kv{display:grid;grid-template-columns:180px 1fr;gap:.35rem .8rem}
|
||||
.pill{display:inline-block;background:#111827;color:#fff;border-radius:999px;padding:.2rem .55rem;font-size:.85rem}
|
||||
.result{border:1px solid #e5e7eb;border-radius:.6rem;padding:.7rem .8rem;background:#fafafa;margin-top:.6rem}
|
||||
.err{color:#b91c1c}
|
||||
|
||||
@media (prefers-color-scheme:dark){
|
||||
body{background:#0b0f19;color:#e5e7eb}
|
||||
.tab{border-color:#374151;color:#e5e7eb}
|
||||
.tab.active{background:#1f2937;border-color:#374151}
|
||||
.card,.dialog{background:#0f1423;border-color:#374151}
|
||||
th,td{border-bottom-color:#374151}
|
||||
.table-wrap{border-color:#374151}
|
||||
.table-wrap thead th{background:#0f1423}
|
||||
.result{background:#111318;border-color:#374151}
|
||||
.close{color:#e5e7eb}
|
||||
}
|
||||
</style>
|
||||
{% block head_extra %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<nav class="nav">
|
||||
{% set p = request.path %}
|
||||
<a class="tab {{ 'active' if p == '/' else '' }}" href="/">Home</a>
|
||||
<a class="tab {{ 'active' if p.startswith('/strukturen') else '' }}" href="/strukturen">Strukturen</a>
|
||||
<a class="tab {{ 'active' if p.startswith('/archiv') else '' }}" href="/archiv">Archiv</a>
|
||||
</nav>
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
7
webapp/templates/index.html
Normal file
7
webapp/templates/index.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>EVE Web Helper</h1>
|
||||
<p>Willkommen! Nutze die Tabs oben, um zu den Strukturen oder zum Archiv zu wechseln.</p>
|
||||
{% endblock %}
|
||||
440
webapp/templates/minerals.html
Normal file
440
webapp/templates/minerals.html
Normal file
@@ -0,0 +1,440 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>EVE Web Helper – Materialien</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:2rem}
|
||||
.container{max-width:1440px;margin:0 auto}
|
||||
.nav{display:flex;gap:.6rem;margin-bottom:1rem}
|
||||
.tab{padding:.5rem .8rem;border:1px solid #e5e7eb;border-radius:.6rem;text-decoration:none;color:#111827}
|
||||
.tab.struct{background:#111827;color:#fff;border-color:#111827}
|
||||
|
||||
/* Unterreiter links */
|
||||
.subtabs-left{position:fixed;left:.8rem;top:96px;display:flex;flex-direction:column;gap:.5rem;
|
||||
width:190px;padding:.6rem;border:1px solid #e5e7eb;border-radius:.6rem;background:#fff;
|
||||
box-shadow:0 2px 8px rgba(0,0,0,.05);z-index:50}
|
||||
.subtabs-left a{display:block;text-decoration:none;padding:.5rem .6rem;border:1px solid #e5e7eb;border-radius:.6rem;color:#111827;background:#fff}
|
||||
.subtabs-left a.active{background:#111827;color:#fff;border-color:#111827}
|
||||
@media (max-width:1200px){.subtabs-left{display:none}}
|
||||
|
||||
.row{display:flex;gap:.6rem;flex-wrap:wrap;align-items:center}
|
||||
.select, .button, input[type="number"]{padding:.5rem .7rem;border:1px solid #d1d5db;border-radius:.45rem}
|
||||
.button{background:#111827;color:#fff;cursor:pointer}
|
||||
.muted{color:#6b7280;font-size:.9rem}
|
||||
.bad{color:#b91c1c}
|
||||
|
||||
.card{border:1px solid #e5e7eb;border-radius:.8rem;padding:1rem 1.2rem;margin-bottom:1rem;background:#fff}
|
||||
.table-wrap{overflow:auto;border:1px solid #e5e7eb;border-radius:.6rem}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th,td{padding:.5rem .45rem;border-bottom:1px solid #e5e7eb;text-align:left;vertical-align:top;word-break:break-word}
|
||||
th{font-weight:700}
|
||||
.num{text-align:right}
|
||||
|
||||
details{border:1px solid #e5e7eb;border-radius:.6rem;margin:.6rem 0;background:#fff}
|
||||
details>summary{cursor:pointer;padding:.5rem .6rem;font-weight:600}
|
||||
details .inner{padding:.5rem .6rem;border-top:1px solid #e5e7eb}
|
||||
|
||||
@media (prefers-color-scheme:dark){
|
||||
body{background:#0b0f19;color:#e5e7eb}
|
||||
.tab{border-color:#374151;color:#e5e7eb}
|
||||
.card,details{background:#0f1423;border-color:#374151}
|
||||
th,td{border-bottom-color:#374151}
|
||||
.table-wrap{border-color:#374151}
|
||||
.button{background:#1f2937;border-color:#374151}
|
||||
.subtabs-left{background:#0f1423;border-color:#374151}
|
||||
.subtabs-left a{color:#e5e7eb;border-color:#374151;background:#0f1423}
|
||||
.subtabs-left a.active{background:#1f2937}
|
||||
details .inner{border-top-color:#374151}
|
||||
.select, input, .button{border-color:#374151}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="subtabs-left" aria-label="Unterreiter">
|
||||
<a href="{{ url_for('structures.structures') }}">Aufträge</a>
|
||||
<a class="active" href="{{ url_for('structures.mineralien') }}">Mineralien</a>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<nav class="nav">
|
||||
<a class="tab" href="/">Home</a>
|
||||
<a class="tab struct" href="{{ url_for('structures.structures') }}">Strukturen</a>
|
||||
<a class="tab" href="/archiv">Archiv</a>
|
||||
</nav>
|
||||
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<h1 style="margin:0">Material-Übersicht (aus offenen Aufträgen)</h1>
|
||||
<div class="row">
|
||||
<label class="muted">Ansicht:
|
||||
<select id="view-mode" class="select">
|
||||
<option value="all" selected>Alle Materialien</option>
|
||||
<option value="minerals">Nur Mineralien</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="muted" id="morphite-wrap" style="display:none">
|
||||
<input type="checkbox" id="include-morphite" checked> Morphite einschließen
|
||||
</label>
|
||||
<button id="btn-refresh" class="button" type="button" onclick="refreshAll()">Aktualisieren</button>
|
||||
<button id="btn-csv" class="button" type="button" onclick="exportCSV()">CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mini-Einstellungen NUR für Mineralien-Abfragen (persistiert via localStorage) -->
|
||||
<details class="card" style="margin-top:.8rem">
|
||||
<summary>Einstellungen für Berechnung (werden gespeichert)</summary>
|
||||
<div class="row" style="margin-top:.6rem">
|
||||
<label class="muted">System
|
||||
<select id="set-system" class="select" style="min-width:12rem">
|
||||
<option>Jita</option>
|
||||
<option>Perimeter</option>
|
||||
<option>Amarr</option>
|
||||
<option>Dodixie</option>
|
||||
<option>Rens</option>
|
||||
<option>Hek</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="muted">ME % (Blueprint)
|
||||
<input id="set-baseMe" type="number" min="0" max="10" step="1" class="select" style="width:6rem" />
|
||||
</label>
|
||||
<label class="muted">Steuersatz % (facilityTax)
|
||||
<input id="set-facilityTax" type="number" min="0" max="100" step="1" class="select" style="width:7rem" />
|
||||
</label>
|
||||
<button class="button" type="button" onclick="saveSettingsFromForm()">Speichern</button>
|
||||
</div>
|
||||
<p class="muted" style="margin:.6rem 0 0">Eine ME-Einstellung reicht – sie wird auch für Komponenten verwendet. Werte werden im Browser gespeichert.</p>
|
||||
</details>
|
||||
|
||||
<p class="muted">Wir lösen die Zwischenprodukte in <em>Blatt-Materialien</em> auf (nur echte Eingänge, keine Doppelzählung).</p>
|
||||
|
||||
<div id="box" class="card" style="padding:.6rem 1rem;margin:.8rem 0">Lade Materialien …</div>
|
||||
|
||||
<!-- 1) Pro-Item-Aufschlüsselung -->
|
||||
<div id="wrap-breakdown" style="display:none">
|
||||
<h3>Aufschlüsselung pro Zwischenprodukt</h3>
|
||||
<div id="breakdown"></div>
|
||||
</div>
|
||||
|
||||
<!-- 2) Gesamtübersicht -->
|
||||
<div id="wrap-total" style="display:none;margin-top:1rem">
|
||||
<h3>Gesamtübersicht (aggregiert)</h3>
|
||||
<div class="table-wrap">
|
||||
<table aria-label="Materialien – Gesamt">
|
||||
<thead>
|
||||
<tr><th>Item</th><th class="num">Gesamtmenge</th><th class="num">Ø Preis / Einh.</th><th class="num">Gesamtkosten</th></tr>
|
||||
</thead>
|
||||
<tbody id="tbody-total"></tbody>
|
||||
<tfoot>
|
||||
<tr><th colspan="3" class="num">Summe</th><th class="num"><strong id="sum-total">0 ISK</strong></th></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div class="muted" id="ts" style="margin-top:.4rem"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ===== Persistente Einstellungen NUR für Mineralien ===== */
|
||||
const SETTINGS_KEY = "sp_global_mats_settings";
|
||||
// nur 1 ME, System & facilityTax
|
||||
let SETTINGS = { system:"Jita", baseMe:0, facilityTax:0 };
|
||||
|
||||
function loadLocalSettings(){
|
||||
try{
|
||||
const raw = localStorage.getItem(SETTINGS_KEY);
|
||||
if(raw){
|
||||
const s = JSON.parse(raw);
|
||||
if(s && typeof s === 'object'){
|
||||
// migriert alte Objekte (componentsMe ggf. vorhanden -> ignoriert)
|
||||
SETTINGS.system = s.system ?? SETTINGS.system;
|
||||
SETTINGS.baseMe = (s.baseMe ?? s.componentsMe ?? SETTINGS.baseMe);
|
||||
SETTINGS.facilityTax = s.facilityTax ?? SETTINGS.facilityTax;
|
||||
}
|
||||
}
|
||||
}catch(_){}
|
||||
// UI füllen
|
||||
document.getElementById('set-system').value = SETTINGS.system;
|
||||
document.getElementById('set-baseMe').value = SETTINGS.baseMe;
|
||||
document.getElementById('set-facilityTax').value = SETTINGS.facilityTax;
|
||||
}
|
||||
function saveLocalSettings(){
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(SETTINGS));
|
||||
}
|
||||
function saveSettingsFromForm(){
|
||||
SETTINGS.system = document.getElementById('set-system').value || SETTINGS.system;
|
||||
SETTINGS.baseMe = parseInt(document.getElementById('set-baseMe').value || SETTINGS.baseMe, 10);
|
||||
SETTINGS.facilityTax = parseInt(document.getElementById('set-facilityTax').value || SETTINGS.facilityTax, 10);
|
||||
saveLocalSettings();
|
||||
loadMaterialsDeep(); // neu berechnen
|
||||
}
|
||||
|
||||
/* ===== Konstanten / Helfer ===== */
|
||||
const MINERAL_TYPES={34:"Tritanium",35:"Pyerite",36:"Mexallon",37:"Isogen",38:"Nocxium",39:"Zydrine",40:"Megacyte",11399:"Morphite"};
|
||||
const MINERAL_ORDER=[34,35,36,37,38,39,40,11399];
|
||||
const sleep=(ms)=>new Promise(r=>setTimeout(r,ms));
|
||||
function formatISK(x){const n=Number(x);if(!isFinite(n))return String(x);return n.toLocaleString('de-DE')+" ISK";}
|
||||
function safeNum(v){const n=Number(v);return isFinite(n)?n:0;}
|
||||
function formatTS(s){if(!s)return"";const d=new Date(s);if(isNaN(d.getTime())){const t=String(s);return t.slice(0,16);}const y=d.getFullYear(),m=String(d.getMonth()+1).padStart(2,'0'),da=String(d.getDate()).padStart(2,'0'),hh=String(d.getHours()).padStart(2,'0'),mm=String(d.getMinutes()).padStart(2,'0');return `${y}-${m}-${da} ${hh}:${mm}`;}
|
||||
|
||||
function looksLikeMatRow(o){
|
||||
if(!o||typeof o!=='object')return false;
|
||||
const hasId=('type_id'in o)||('typeId'in o);
|
||||
const hasQty=('quantity'in o)||('qty'in o)||('amount'in o)||('count'in o);
|
||||
return hasId && hasQty;
|
||||
}
|
||||
function hasChildren(o){
|
||||
for(const k of ['materials','components','inputs','children','parts','steps','subMaterials']){
|
||||
if(Array.isArray(o[k]) && o[k].length) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// sammelt NUR Blatt-Materialien
|
||||
function collectLeafMatRowsDeep(x,out){
|
||||
if(!x) return;
|
||||
if(Array.isArray(x)){ for(const v of x) collectLeafMatRowsDeep(v,out); return; }
|
||||
if(typeof x==='object'){
|
||||
if(looksLikeMatRow(x) && !hasChildren(x)) out.push(x);
|
||||
for(const v of Object.values(x)) collectLeafMatRowsDeep(v,out);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRows(rows){
|
||||
return rows.map(r=>{
|
||||
const id=Number(r.type_id??r.typeId??0);
|
||||
const name=r.name??r.item_name??r.typeName??("Type "+id);
|
||||
const qty=safeNum(r.quantity??r.qty??r.amount??r.count);
|
||||
const cost=safeNum(r.cost??r.total_cost);
|
||||
const unit=safeNum(r.unit_cost??r.cost_per_unit??(qty?cost/qty:0));
|
||||
return {type_id:id,name,quantity:qty,cost,unit};
|
||||
});
|
||||
}
|
||||
function isMineralId(id){return MINERAL_ORDER.includes(Number(id));}
|
||||
function filterByViewMode(normRows){
|
||||
const mode=document.getElementById('view-mode').value;
|
||||
if(mode==='minerals'){
|
||||
const includeMorphite=document.getElementById('include-morphite').checked;
|
||||
const allowed=new Set(includeMorphite?MINERAL_ORDER:MINERAL_ORDER.filter(x=>x!==11399));
|
||||
return normRows.filter(r=>allowed.has(r.type_id));
|
||||
}
|
||||
return normRows; // 'all'
|
||||
}
|
||||
function aggregateRows(rows){
|
||||
const map=new Map();
|
||||
for(const r of rows){
|
||||
const k=r.type_id;
|
||||
if(!map.has(k)) map.set(k,{type_id:k,name:r.name,quantity:0,cost:0,unitSum:0,unitWeight:0});
|
||||
const a=map.get(k);
|
||||
a.quantity+=safeNum(r.quantity);
|
||||
a.cost+=safeNum(r.cost);
|
||||
if(r.unit>0 && r.quantity>0){a.unitSum+=r.unit*r.quantity; a.unitWeight+=r.quantity;}
|
||||
}
|
||||
const all=[...map.values()];
|
||||
const minerals=all.filter(x=>isMineralId(x.type_id)).sort((a,b)=>MINERAL_ORDER.indexOf(a.type_id)-MINERAL_ORDER.indexOf(b.type_id));
|
||||
const others=all.filter(x=>!isMineralId(x.type_id)).sort((a,b)=>a.name.localeCompare(b.name,'de'));
|
||||
return [...minerals,...others].map(a=>{
|
||||
const avg=a.unitWeight>0 ? a.unitSum/a.unitWeight : (a.quantity ? a.cost/a.quantity : 0);
|
||||
return {type_id:a.type_id,name:a.name,quantity:a.quantity,unit:avg,cost:a.cost};
|
||||
});
|
||||
}
|
||||
|
||||
/* ===== API + Blueprint-Lookup ===== */
|
||||
async function fetchJSON(url,{retries=2,backoff=700}={}){let last=null;for(let i=0;i<=retries;i++){try{const r=await fetch(url);const retr=r.status>=500;if(!r.ok){const t=await r.text();last=new Error(`HTTP ${r.status} – ${t.slice(0,200)}`);if(!retr)throw last;}else{return await r.json();}}catch(e){last=e;}if(i<retries)await sleep(backoff*Math.pow(1.6,i));}throw last||new Error('Unbekannter Fehler');}
|
||||
function q(obj){const p=new URLSearchParams();for(const [k,v] of Object.entries(obj)){if(v!==undefined&&v!==null)p.append(k,String(v));}return p.toString();}
|
||||
const BP_CACHE=new Map();
|
||||
async function getBlueprintId(productName, productTypeId){
|
||||
const key=(productName||"")+":"+String(productTypeId||"");
|
||||
if(BP_CACHE.has(key)) return BP_CACHE.get(key);
|
||||
const url="/api/blueprint_id?"+q({name:productName,productTypeId:productTypeId});
|
||||
const j=await fetchJSON(url,{retries:1,backoff:400});
|
||||
const id=j && j.blueprint_type_id;
|
||||
if(!id) throw new Error("Blueprint-ID nicht gefunden");
|
||||
BP_CACHE.set(key,id);
|
||||
return id;
|
||||
}
|
||||
|
||||
// *** eine ME-Einstellung -> für baseMe UND componentsMe nutzen ***
|
||||
async function callMaterialsEndpoint(blueprintTypeId, qty){
|
||||
const params={
|
||||
blueprintTypeId,
|
||||
quantity:qty,
|
||||
priceMode:"buy",
|
||||
baseMe:SETTINGS.baseMe,
|
||||
componentsMe:SETTINGS.baseMe, // gleicher Wert
|
||||
system:SETTINGS.system,
|
||||
facilityTax:SETTINGS.facilityTax
|
||||
};
|
||||
return await fetchJSON("/api/cookbook/materials?"+q(params),{retries:2,backoff:800});
|
||||
}
|
||||
async function callBuildCostEndpoint(blueprintTypeId, qty){
|
||||
const params={
|
||||
blueprintTypeId,
|
||||
quantity:qty,
|
||||
priceMode:"buy",
|
||||
baseMe:SETTINGS.baseMe,
|
||||
componentsMe:SETTINGS.baseMe, // gleicher Wert
|
||||
system:SETTINGS.system,
|
||||
facilityTax:SETTINGS.facilityTax,
|
||||
detailed:"Yes",steps:"Yes",showDetailed:"Yes",showDetailedBuildSteps:"Yes"
|
||||
};
|
||||
return await fetchJSON("/api/cookbook/build_cost?"+q(params),{retries:2,backoff:900});
|
||||
}
|
||||
async function loadLeafMaterialsFor(productName, productTypeId, qty){
|
||||
try{
|
||||
const bpId=await getBlueprintId(productName, productTypeId);
|
||||
let rows=[], r=await callMaterialsEndpoint(bpId, qty);
|
||||
collectLeafMatRowsDeep(r, rows);
|
||||
if(!rows.length){ r=await callBuildCostEndpoint(bpId, qty); collectLeafMatRowsDeep(r, rows); }
|
||||
return rows;
|
||||
}catch(e){ console.warn("Materialermittlung fehlgeschlagen:", e); return []; }
|
||||
}
|
||||
|
||||
/* ===== Rendering ===== */
|
||||
function renderTotal(rows, metaTs){
|
||||
const tbody=document.getElementById('tbody-total');
|
||||
const wrap=document.getElementById('wrap-total');
|
||||
const box=document.getElementById('box');
|
||||
const sumEl=document.getElementById('sum-total');
|
||||
const ts=document.getElementById('ts');
|
||||
|
||||
if(!rows.length){ box.textContent='Keine Materialien ermittelt.'; wrap.style.display='none'; return; }
|
||||
|
||||
tbody.innerHTML=rows.map(r=>`
|
||||
<tr>
|
||||
<td>${r.name} <span class="muted">(${r.type_id})</span></td>
|
||||
<td class="num">${r.quantity.toLocaleString('de-DE')}</td>
|
||||
<td class="num">${r.unit?formatISK(r.unit):'—'}</td>
|
||||
<td class="num"><strong>${formatISK(r.cost)}</strong></td>
|
||||
</tr>`).join('');
|
||||
|
||||
const total=rows.reduce((s,r)=>s+safeNum(r.cost),0);
|
||||
sumEl.textContent=formatISK(total);
|
||||
ts.textContent = metaTs ? ('Stand: '+formatTS(metaTs)) : '';
|
||||
|
||||
box.style.display='none';
|
||||
wrap.style.display='block';
|
||||
}
|
||||
|
||||
function renderBreakdown(perItem){
|
||||
const host=document.getElementById('breakdown');
|
||||
const wrap=document.getElementById('wrap-breakdown');
|
||||
if(!perItem.length){ wrap.style.display='none'; return; }
|
||||
|
||||
host.innerHTML=perItem.map(it=>{
|
||||
const inner = it.rows.length
|
||||
? `<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Item</th><th class="num">Menge</th><th class="num">Ø Preis / Einh.</th><th class="num">Kosten</th></tr></thead>
|
||||
<tbody>
|
||||
${it.rows.map(r=>`<tr>
|
||||
<td>${r.name} <span class="muted">(${r.type_id})</span></td>
|
||||
<td class="num">${r.quantity.toLocaleString('de-DE')}</td>
|
||||
<td class="num">${r.unit?formatISK(r.unit):'—'}</td>
|
||||
<td class="num">${formatISK(r.cost)}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
: `<div class="muted inner">Keine (Blatt-)Materialien benötigt oder nicht ermittelbar.</div>`;
|
||||
|
||||
const err = it.error ? `<div class="bad inner">Fehler: ${it.error}</div>` : '';
|
||||
|
||||
return `<details>
|
||||
<summary>${it.label} <span class="muted">(${it.type_id})</span> – Menge: ${it.qty.toLocaleString('de-DE')}</summary>
|
||||
<div class="inner">${inner}${err}</div>
|
||||
</details>`;
|
||||
}).join('');
|
||||
|
||||
wrap.style.display='block';
|
||||
}
|
||||
|
||||
/* ===== Hauptlogik ===== */
|
||||
async function loadMaterialsDeep(){
|
||||
const box=document.getElementById('box');
|
||||
const totalWrap=document.getElementById('wrap-total');
|
||||
const breakdownWrap=document.getElementById('wrap-breakdown');
|
||||
box.textContent='Lade offene Materialien …';
|
||||
box.style.display='block';
|
||||
totalWrap.style.display='none';
|
||||
breakdownWrap.style.display='none';
|
||||
|
||||
try{
|
||||
const open=await fetchJSON("/api/cookbook/open_materials",{retries:1,backoff:500});
|
||||
const items=Array.isArray(open.items)?open.items:[];
|
||||
if(!items.length){ box.textContent='Keine offenen Zwischenprodukte vorhanden.'; return; }
|
||||
|
||||
let done=0; const progress=()=> box.textContent=`Ermittle Eingangs-Materialien … (${done}/${items.length})`;
|
||||
|
||||
const perItem=[]; // für Breakdown
|
||||
const allRows=[]; // für Gesamt
|
||||
|
||||
for(const it of items){
|
||||
progress();
|
||||
const typeId=it.type_id??it.typeId;
|
||||
const qty=safeNum(it.quantity??it.qty??it.amount??0);
|
||||
const label=(it.name??it.item_name??it.typeName??("Type "+typeId));
|
||||
if(!typeId || qty<=0){ done++; continue; }
|
||||
|
||||
try{
|
||||
const rowsRaw = await loadLeafMaterialsFor(label, typeId, qty); // echte Blätter
|
||||
const norm = normalizeRows(rowsRaw);
|
||||
const filtered= filterByViewMode(norm);
|
||||
const aggr = aggregateRows(filtered);
|
||||
|
||||
perItem.push({type_id:typeId,label,qty,rows:aggr});
|
||||
aggr.forEach(r=>allRows.push(r));
|
||||
}catch(err){
|
||||
perItem.push({type_id:typeId,label,qty,rows:[],error:String(err && err.message ? err.message : err)});
|
||||
}
|
||||
|
||||
done++; progress();
|
||||
await sleep(300); // Upstream schonen
|
||||
}
|
||||
|
||||
// 1) Breakdown pro Item
|
||||
renderBreakdown(perItem);
|
||||
// 2) Gesamt-Übersicht
|
||||
const total = aggregateRows(allRows);
|
||||
renderTotal(total, open.refreshed_at);
|
||||
}catch(err){
|
||||
box.innerHTML = `<span class="bad">Fehler: ${err && err.message ? err.message : err}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshAll(){
|
||||
const b=document.getElementById("btn-refresh"); if(b) b.disabled=true;
|
||||
fetch("/api/orders/refresh_all",{method:"POST"})
|
||||
.then(()=>loadMaterialsDeep())
|
||||
.finally(()=>{ if(b) b.disabled=false; });
|
||||
}
|
||||
|
||||
function exportCSV(){
|
||||
const rows=[["Item","Gesamtmenge","Ø Preis / Einh.","Gesamtkosten"]];
|
||||
document.querySelectorAll("#tbody-total tr").forEach(tr=>{
|
||||
const tds=[...tr.querySelectorAll("td")].map(td=>td.textContent);
|
||||
rows.push(tds);
|
||||
});
|
||||
const csv=rows.map(r=>r.map(v=>`"${String(v).replace(/"/g,'""')}"`).join(";")).join("\r\n");
|
||||
const blob=new Blob([csv],{type:"text/csv;charset=utf-8;"});
|
||||
const a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download="materialien_summe.csv"; a.click(); URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded",()=>{
|
||||
loadLocalSettings(); // Einstellungen laden
|
||||
const modeSel=document.getElementById('view-mode');
|
||||
const morphiteWrap=document.getElementById('morphite-wrap');
|
||||
modeSel.addEventListener('change',()=>{
|
||||
morphiteWrap.style.display = modeSel.value==='minerals' ? 'inline-flex' : 'none';
|
||||
loadMaterialsDeep();
|
||||
});
|
||||
document.getElementById('include-morphite')?.addEventListener('change', loadMaterialsDeep);
|
||||
morphiteWrap.style.display = modeSel.value==='minerals' ? 'inline-flex' : 'none';
|
||||
loadMaterialsDeep();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
537
webapp/templates/structures.html
Normal file
537
webapp/templates/structures.html
Normal file
@@ -0,0 +1,537 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>EVE Web Helper – Strukturen</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
html{scroll-behavior:smooth}
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:2rem}
|
||||
.container{max-width:1440px;margin:0 auto}
|
||||
|
||||
/* Haupt-Tabs oben */
|
||||
.nav{display:flex;gap:.6rem;margin-bottom:1rem}
|
||||
.tab{padding:.5rem .8rem;border:1px solid #e5e7eb;border-radius:.6rem;text-decoration:none;color:#111827}
|
||||
.tab.active{background:#111827;color:#fff;border-color:#111827}
|
||||
|
||||
/* Linke Unterreiter (untergeordnete Seite) */
|
||||
.subtabs-left{
|
||||
position:fixed; left:.8rem; top:96px; display:flex; flex-direction:column; gap:.5rem;
|
||||
width:170px; padding:.6rem; border:1px solid #e5e7eb; border-radius:.6rem; background:#fff;
|
||||
box-shadow:0 2px 8px rgba(0,0,0,.05); z-index:50
|
||||
}
|
||||
.subtabs-left a{display:block;text-decoration:none;padding:.5rem .6rem;border:1px solid #e5e7eb;border-radius:.6rem;color:#111827;background:#fff}
|
||||
.subtabs-left a.active{background:#111827;color:#fff;border-color:#111827}
|
||||
@media (max-width:1200px){.subtabs-left{display:none}}
|
||||
|
||||
.card{border:1px solid #e5e7eb;border-radius:.8rem;padding:1rem 1.2rem;margin-bottom:1rem;background:#fff}
|
||||
.wide{max-width:1200px}
|
||||
.full{width:100%}
|
||||
h1{font-size:1.35rem;margin:.2rem 0 1rem}
|
||||
label{font-weight:600;display:block;margin-bottom:.35rem}
|
||||
select,input,textarea,button,a.button{padding:.5rem .7rem;border:1px solid #d1d5db;border-radius:.45rem}
|
||||
textarea{width:100%;min-height:90px;resize:vertical}
|
||||
button,a.button{background:#111827;color:#fff;cursor:pointer;text-decoration:none;display:inline-block}
|
||||
.muted{color:#6b7280;font-size:.9rem}
|
||||
.row{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.8rem;margin-top:.8rem}
|
||||
@media (max-width:1000px){.row{grid-template-columns:1fr 1fr}}
|
||||
|
||||
table{width:100%;border-collapse:collapse;table-layout:fixed}
|
||||
th,td{padding:.5rem .45rem;border-bottom:1px solid #e5e7eb;text-align:left;vertical-align:top;word-break:break-word;overflow-wrap:anywhere}
|
||||
th{font-weight:700}
|
||||
.id{font-family:ui-monospace,SFMono-Regular,Consolas,Menlo,monospace;font-size:.85em;max-width:90px}
|
||||
.num{text-align:right}
|
||||
.actions form{display:inline}
|
||||
.section-title{font-weight:700;margin-bottom:.6rem}
|
||||
.empty{padding:.65rem .8rem;border:1px dashed #d1d5db;border-radius:.6rem;color:#6b7280;background:transparent}
|
||||
.table-wrap{overflow:auto;border:1px solid #e5e7eb;border-radius:.6rem}
|
||||
.table-wrap thead th{position:sticky;top:0;background:#fff;z-index:1}
|
||||
.scroll-5{max-height:260px}
|
||||
@media (max-width:1600px){ .col-notes{display:none} }
|
||||
@media (max-width:1450px){ .col-react-rig{display:none} }
|
||||
@media (max-width:1350px){ .col-ind-rig{display:none} }
|
||||
@media (max-width:1250px){ .col-ind-struct,.col-react-struct{display:none} }
|
||||
|
||||
/* Modal */
|
||||
.modal{position:fixed;inset:0;display:none;align-items:flex-start;justify-content:center;padding:4vh 1rem;background:rgba(0,0,0,.45);z-index:999}
|
||||
.modal:target{display:flex}
|
||||
.dialog{max-width:1100px;width:min(92vw,1100px);background:#fff;border-radius:14px;padding:1rem 1.2rem;box-shadow:0 20px 45px rgba(0,0,0,.35);max-height:92vh;overflow:auto;position:relative;z-index:1000;pointer-events:auto}
|
||||
.modal *{pointer-events:auto}
|
||||
.modal h2{margin:.2rem 0 1rem;font-size:1.2rem}
|
||||
.close{position:sticky;top:0;float:right;font-size:1.4rem;text-decoration:none;color:#111827;padding:.2rem .5rem;border-radius:.4rem;z-index:2}
|
||||
.kv{display:grid;grid-template-columns:180px 1fr;gap:.35rem .8rem}
|
||||
hr.sep{border:none;border-top:1px solid #e5e7eb;margin:.8rem 0}
|
||||
.pill{display:inline-block;background:#111827;color:#fff;border-radius:999px;padding:.2rem .55rem;font-size:.85rem}
|
||||
.result{border:1px solid #e5e7eb;border-radius:.6rem;padding:.7rem .8rem;background:#fafafa;margin-top:.6rem}
|
||||
.err{color:#b91c1c}
|
||||
|
||||
@media (prefers-color-scheme:dark){
|
||||
body{color:#e5e7eb;background:#0b0f19}
|
||||
.tab{border-color:#374151;color:#e5e7eb}
|
||||
.tab.active{background:#1f2937;border-color:#374151}
|
||||
.card,.dialog,.subtabs-left{background:#0f1423;border-color:#374151}
|
||||
th,td{border-bottom-color:#374151}
|
||||
.table-wrap{border-color:#374151}
|
||||
.table-wrap thead th{background:#0f1423}
|
||||
.result{background:#111318;border-color:#374151}
|
||||
.close{color:#e5e7eb}
|
||||
.subtabs-left a{color:#e5e7eb;border-color:#374151;background:#0f1423}
|
||||
.subtabs-left a.active{background:#1f2937}
|
||||
}
|
||||
|
||||
body{overflow-x:hidden}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Unterreiter links – AKTIV-Status serverseitig über request.path -->
|
||||
<nav class="subtabs-left" aria-label="Unterreiter">
|
||||
<a href="/strukturen"
|
||||
class="{{ 'active' if request.path == '/strukturen' else '' }}">Aufträge</a>
|
||||
<a href="/strukturen/mineralien"
|
||||
class="{{ 'active' if request.path.startswith('/strukturen/mineralien') else '' }}">Mineralien</a>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<nav class="nav">
|
||||
<a class="tab" href="/">Home</a>
|
||||
<a class="tab active" href="/strukturen">Strukturen</a>
|
||||
<a class="tab" href="/archiv">Archiv</a>
|
||||
</nav>
|
||||
|
||||
<!-- Formular -->
|
||||
<section class="card wide">
|
||||
<h1>Struktur-Auftrag anlegen</h1>
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="structure-select">Struktur</label>
|
||||
<select id="structure-select" name="structure" required>
|
||||
<option value="" disabled selected>— bitte wählen —</option>
|
||||
{% for s in structures %}<option value="{{ s }}">{{ s }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="quantity">Menge</label>
|
||||
<input id="quantity" name="quantity" type="number" min="1" step="1" value="{{ quantity or 1 }}" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="me">ME %</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