|
@@ -0,0 +1,138 @@
|
|
|
|
|
+import {cachedFetchJSON} from './cache';
|
|
|
|
|
+
|
|
|
|
|
+const username = document.querySelector('#username') as HTMLInputElement;
|
|
|
|
|
+const apiKey = document.querySelector('#api-key') as HTMLInputElement;
|
|
|
|
|
+const cxSelect = document.querySelector('#cx') as HTMLSelectElement;
|
|
|
|
|
+{
|
|
|
|
|
+ const storedUsername = localStorage.getItem('fio-username');
|
|
|
|
|
+ if (storedUsername)
|
|
|
|
|
+ username.value = storedUsername;
|
|
|
|
|
+ const storedApiKey = localStorage.getItem('fio-api-key');
|
|
|
|
|
+ if (storedApiKey)
|
|
|
|
|
+ apiKey.value = storedApiKey;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+document.querySelector('#fetch')!.addEventListener('click', async () => {
|
|
|
|
|
+ const loader = document.querySelector('#loader') as HTMLElement;
|
|
|
|
|
+ loader.style.display = 'block';
|
|
|
|
|
+ try {
|
|
|
|
|
+ await calculate(username.value, apiKey.value, cxSelect.value);
|
|
|
|
|
+ localStorage.setItem('fio-username', username.value);
|
|
|
|
|
+ localStorage.setItem('fio-api-key', apiKey.value);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error(e);
|
|
|
|
|
+ (document.querySelector('#junk') as HTMLElement).innerText = e instanceof Error ? e.message : String(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ loader.style.display = 'none';
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const format = new Intl.NumberFormat(undefined, {maximumFractionDigits: 2}).format;
|
|
|
|
|
+
|
|
|
|
|
+async function calculate(username: string, apiKey: string, cx: string): Promise<void> {
|
|
|
|
|
+ const renderTarget = document.querySelector('#junk')!;
|
|
|
|
|
+ renderTarget.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ const fioBurns: FIOBurn[] = await cachedFetchJSON('https://rest.fnar.net/fioweb/burn/user/' + username,
|
|
|
|
|
+ {headers: {'Authorization': apiKey}});
|
|
|
|
|
+ const totalConsumption = new Map<string, number>();
|
|
|
|
|
+ for (const fioBurn of fioBurns) {
|
|
|
|
|
+ const netConsumption = new Map<string, number>();
|
|
|
|
|
+ for (const item of fioBurn.OrderProduction)
|
|
|
|
|
+ netConsumption.set(item.MaterialTicker, -item.DailyAmount);
|
|
|
|
|
+ for (const item of [...fioBurn.OrderConsumption, ...fioBurn.WorkforceConsumption])
|
|
|
|
|
+ netConsumption.set(item.MaterialTicker, (netConsumption.get(item.MaterialTicker) ?? 0) + item.DailyAmount);
|
|
|
|
|
+ for (const [ticker, consumption] of netConsumption)
|
|
|
|
|
+ if (consumption > 0)
|
|
|
|
|
+ totalConsumption.set(ticker, (totalConsumption.get(ticker) ?? 0) + consumption);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const warehouse = await warehouseInventory(username, apiKey, cx);
|
|
|
|
|
+ const supplies = [...warehouse.entries()].map(([ticker, amount]) => {
|
|
|
|
|
+ const consumption = totalConsumption.get(ticker);
|
|
|
|
|
+ return {
|
|
|
|
|
+ ticker,
|
|
|
|
|
+ amount,
|
|
|
|
|
+ days: consumption === undefined || consumption <= 0 ? Number.POSITIVE_INFINITY : amount / consumption,
|
|
|
|
|
+ consumption: consumption ?? 0,
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+ supplies.sort((a, b) => b.days - a.days);
|
|
|
|
|
+
|
|
|
|
|
+ const h2 = document.createElement('h2');
|
|
|
|
|
+ h2.textContent = 'warehouse supply';
|
|
|
|
|
+ renderTarget.appendChild(h2);
|
|
|
|
|
+
|
|
|
|
|
+ const table = document.createElement('table');
|
|
|
|
|
+ table.innerHTML = `
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th>mat</th>
|
|
|
|
|
+ <th>have</th>
|
|
|
|
|
+ <th>consumption/day</th>
|
|
|
|
|
+ <th>supply</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>`;
|
|
|
|
|
+ const tbody = document.createElement('tbody');
|
|
|
|
|
+ for (const supply of supplies) {
|
|
|
|
|
+ const tr = document.createElement('tr');
|
|
|
|
|
+ tr.innerHTML = `
|
|
|
|
|
+ <td>${supply.ticker}</td>
|
|
|
|
|
+ <td>${format(supply.amount)}</td>
|
|
|
|
|
+ <td>${format(supply.consumption)}</td>
|
|
|
|
|
+ <td>${Number.isFinite(supply.days) ? format(supply.days) + ' d' : '∞'}</td>
|
|
|
|
|
+ `;
|
|
|
|
|
+ tbody.appendChild(tr);
|
|
|
|
|
+ }
|
|
|
|
|
+ table.appendChild(tbody);
|
|
|
|
|
+ renderTarget.appendChild(table);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function warehouseInventory(username: string, apiKey: string, cx: string): Promise<Map<string, number>> {
|
|
|
|
|
+ const warehouses: Warehouse[] = await cachedFetchJSON('https://rest.fnar.net/sites/warehouses/' + username,
|
|
|
|
|
+ {headers: {'Authorization': apiKey}});
|
|
|
|
|
+
|
|
|
|
|
+ const inventory = new Map<string, number>();
|
|
|
|
|
+ for (const warehouse of warehouses)
|
|
|
|
|
+ if (warehouse.LocationNaturalId === cx) {
|
|
|
|
|
+ const storage: Storage = await cachedFetchJSON(`https://rest.fnar.net/storage/${username}/${warehouse.StoreId}`,
|
|
|
|
|
+ {headers: {'Authorization': apiKey}});
|
|
|
|
|
+ for (const item of storage.StorageItems)
|
|
|
|
|
+ inventory.set(item.MaterialTicker, item.MaterialAmount);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ return inventory;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface StorageItem {
|
|
|
|
|
+ MaterialTicker: string;
|
|
|
|
|
+ MaterialAmount: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface Storage {
|
|
|
|
|
+ Name: string;
|
|
|
|
|
+ StorageItems: StorageItem[];
|
|
|
|
|
+ WeightLoad: number;
|
|
|
|
|
+ VolumeLoad: number;
|
|
|
|
|
+ Type: 'STORE' | 'WAREHOUSE_STORE' | 'FTL_FUEL_STORE' | 'STL_FUEL_STORE' | 'SHIP_STORE';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface Warehouse {
|
|
|
|
|
+ StoreId: string;
|
|
|
|
|
+ LocationNaturalId: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface Amount {
|
|
|
|
|
+ MaterialTicker: string;
|
|
|
|
|
+ DailyAmount: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface FIOBurn {
|
|
|
|
|
+ PlanetId: string;
|
|
|
|
|
+ PlanetName: string;
|
|
|
|
|
+ PlanetNaturalId: string;
|
|
|
|
|
+ Error: any;
|
|
|
|
|
+ OrderConsumption: Amount[];
|
|
|
|
|
+ WorkforceConsumption: Amount[];
|
|
|
|
|
+ Inventory: StorageItem[];
|
|
|
|
|
+ OrderProduction: Amount[];
|
|
|
|
|
+}
|