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