263 lines
13 KiB
JavaScript
263 lines
13 KiB
JavaScript
import { PRODUCTS, SECTORS, getProduct } from '../data/products.js';
|
|
import { fmtCurrency, fmtNum } from '../core/formatters.js';
|
|
import { computeProductNetFlow } from './production/ratesCalculator.js';
|
|
|
|
let currentMountedProductId = null;
|
|
|
|
export function mountInspector(container, state, onAction) {
|
|
const selectedId = state.selectedProductId || 'wheat';
|
|
const product = getProduct(selectedId) || PRODUCTS[0];
|
|
const pState = state.products[product.id] || { stock: 0, lines: 0, logistics: 0, unlocked: false };
|
|
const sector = SECTORS.find(s => s.id === product.sector);
|
|
currentMountedProductId = product.id;
|
|
|
|
// Downstream products that consume this product
|
|
const downstream = PRODUCTS.filter(p => p.inputs && p.inputs.some(inp => inp.id === product.id));
|
|
|
|
const totalLiquidationValue = (pState.stock || 0) * product.basePrice;
|
|
const halfLiquidationValue = totalLiquidationValue * 0.5;
|
|
const netFlow = computeProductNetFlow(product, state);
|
|
|
|
let html = `
|
|
<div class="inspector-card">
|
|
<div class="inspector-header">
|
|
<div class="inspector-badge-row">
|
|
<span class="sector-tag" style="background: ${product.color}15; color: ${product.color};">
|
|
<iconify-icon icon="${product.icon}"></iconify-icon> ${sector?.name}
|
|
</span>
|
|
<span class="tier-tag">Tier ${product.tier}</span>
|
|
</div>
|
|
<h3 class="inspector-title">${product.name}</h3>
|
|
<p class="inspector-desc">${product.description}</p>
|
|
</div>
|
|
|
|
<!-- Real-time metrics grid -->
|
|
<div class="inspector-metrics-grid">
|
|
<div class="metric-box">
|
|
<span class="metric-label">Warehouse Stock:</span>
|
|
<strong class="metric-val font-mono" id="insp-stock">${fmtNum(pState.stock, 0)} ${product.unit}</strong>
|
|
</div>
|
|
<div class="metric-box">
|
|
<span class="metric-label">Wholesale Price:</span>
|
|
<strong class="metric-val font-mono">${fmtCurrency(product.basePrice)}</strong>
|
|
</div>
|
|
<div class="metric-box">
|
|
<span class="metric-label">Net Supply Flow:</span>
|
|
<strong class="metric-val font-mono ${netFlow.netRate < -0.05 ? 'text-warning' : 'text-profit'}" id="insp-net-rate">
|
|
${netFlow.netRate > 0.01 ? '+' : ''}${fmtNum(netFlow.netRate, 1)}/s
|
|
</strong>
|
|
</div>
|
|
<div class="metric-box">
|
|
<span class="metric-label">Logistics Fleets:</span>
|
|
<strong class="metric-val font-mono" id="insp-logistics">${pState.logistics || 0} active (${fmtNum((pState.logistics || 0) * 0.75, 2)}/s)</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Distribution Fleet & Downstream Supply Policies -->
|
|
<div class="inspector-section">
|
|
<h4 class="section-heading">
|
|
<iconify-icon icon="lucide:sliders"></iconify-icon> Supply & Distribution Policies
|
|
</h4>
|
|
<div class="liquidation-buttons">
|
|
<button class="btn-corp-secondary" id="insp-btn-toggle-logistics" data-product="${product.id}" title="Toggle Wholesale Fleet on/off">
|
|
<span class="liquidation-btn-label">
|
|
<iconify-icon icon="${(pState.logisticsEnabled !== false) ? 'lucide:truck' : 'lucide:truck-x'}"></iconify-icon> Wholesale Fleet
|
|
</span>
|
|
<span class="liquidation-btn-val" id="insp-toggle-log-status" style="color: ${(pState.logisticsEnabled !== false) ? 'var(--emerald-dark)' : 'var(--amber-dark)'};">
|
|
${(pState.logisticsEnabled !== false) ? 'ACTIVE' : 'PAUSED'}
|
|
</span>
|
|
</button>
|
|
<button class="btn-corp-secondary" id="insp-btn-toggle-downstream" data-product="${product.id}" title="Toggle Raw Delivery to Downstream Consumers on/off">
|
|
<span class="liquidation-btn-label">
|
|
<iconify-icon icon="${(pState.supplyDownstreamEnabled !== false) ? 'lucide:git-pull-request' : 'lucide:git-pull-request-closed'}"></iconify-icon> Downstream
|
|
</span>
|
|
<span class="liquidation-btn-val" id="insp-toggle-down-status" style="color: ${(pState.supplyDownstreamEnabled !== false) ? 'var(--emerald-dark)' : 'var(--amber-dark)'};">
|
|
${(pState.supplyDownstreamEnabled !== false) ? 'ALLOWED' : 'BLOCKED'}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Instant Wholesale Market Liquidation -->
|
|
<div class="inspector-section">
|
|
<h4 class="section-heading">
|
|
<iconify-icon icon="lucide:dollar-sign"></iconify-icon> Instant Wholesale Liquidation
|
|
</h4>
|
|
<p class="section-subtext">Liquidate stockpiles immediately onto the wholesale spot market.</p>
|
|
<div class="liquidation-buttons">
|
|
<button class="btn-corp-secondary" id="insp-btn-half" data-product="${product.id}" ${pState.stock >= 2 ? '' : 'disabled'}>
|
|
<span class="liquidation-btn-label">
|
|
<iconify-icon icon="lucide:pie-chart"></iconify-icon> Sell 50%
|
|
</span>
|
|
<span class="liquidation-btn-val" id="insp-val-half">${fmtCurrency(halfLiquidationValue)}</span>
|
|
</button>
|
|
<button class="btn-primary-corp" id="insp-btn-all" data-product="${product.id}" ${pState.stock >= 1 ? '' : 'disabled'}>
|
|
<span class="liquidation-btn-label">
|
|
<iconify-icon icon="lucide:check-circle-2"></iconify-icon> Sell 100%
|
|
</span>
|
|
<span class="liquidation-btn-val" id="insp-val-all">${fmtCurrency(totalLiquidationValue)}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upstream & Downstream Supply Chain Flow -->
|
|
<div class="inspector-section">
|
|
<h4 class="section-heading">
|
|
<iconify-icon icon="lucide:git-fork"></iconify-icon> Supply Chain Dependencies
|
|
</h4>
|
|
|
|
<!-- Inputs (Upstream) -->
|
|
<div class="chain-box">
|
|
<span class="chain-title">Direct Inputs:</span>
|
|
${(!product.inputs || product.inputs.length === 0) ? `
|
|
<div class="chain-empty">Primary commodity • Direct extraction from natural deposits.</div>
|
|
` : `
|
|
<div class="chain-items">
|
|
${product.inputs.map(inp => {
|
|
const parent = getProduct(inp.id);
|
|
const parentStock = state.products[inp.id]?.stock || 0;
|
|
return `
|
|
<div class="chain-item-row chain-item-two-lines" data-product="${parent.id}">
|
|
<div class="chain-item-header-line">
|
|
<div class="chain-item-left">
|
|
<iconify-icon icon="${parent.icon}" style="color: ${parent.color};"></iconify-icon>
|
|
<span class="chain-item-name">${parent.name}</span>
|
|
</div>
|
|
</div>
|
|
<div class="chain-item-sub-line">
|
|
<span class="chain-item-ratio font-mono">${inp.amount} per unit (<strong id="insp-inp-val-${inp.id}">${fmtNum(parentStock, 0)}</strong> on hand)</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
`}
|
|
</div>
|
|
|
|
<!-- Downstream Consumers -->
|
|
<div class="chain-box">
|
|
<span class="chain-title">Downstream Consumers:</span>
|
|
${downstream.length === 0 ? `
|
|
<div class="chain-empty">End-tier consumer or industrial finished product.</div>
|
|
` : `
|
|
<div class="chain-items">
|
|
${downstream.map(child => {
|
|
const isUnlocked = state.products[child.id]?.unlocked;
|
|
return `
|
|
<div class="chain-item-row" data-product="${child.id}">
|
|
<div class="chain-item-left">
|
|
<iconify-icon icon="${child.icon}" style="color: ${child.color};"></iconify-icon>
|
|
<span>${child.name}</span>
|
|
</div>
|
|
<span class="chain-badge ${isUnlocked ? 'badge-open' : 'badge-locked'}" id="insp-child-${child.id}">
|
|
${isUnlocked ? 'Active' : 'Locked'}
|
|
</span>
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
`}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
container.innerHTML = html;
|
|
|
|
const halfBtn = container.querySelector('#insp-btn-half');
|
|
if (halfBtn) {
|
|
halfBtn.onclick = () => onAction({ type: 'LIQUIDATE_STOCK', productId: product.id, ratio: 0.5 });
|
|
}
|
|
|
|
const allBtn = container.querySelector('#insp-btn-all');
|
|
if (allBtn) {
|
|
allBtn.onclick = () => onAction({ type: 'LIQUIDATE_STOCK', productId: product.id, ratio: 1.0 });
|
|
}
|
|
|
|
const toggleLogBtn = container.querySelector('#insp-btn-toggle-logistics');
|
|
if (toggleLogBtn) {
|
|
toggleLogBtn.onclick = () => onAction({ type: 'TOGGLE_LOGISTICS', productId: product.id });
|
|
}
|
|
|
|
const toggleDownBtn = container.querySelector('#insp-btn-toggle-downstream');
|
|
if (toggleDownBtn) {
|
|
toggleDownBtn.onclick = () => onAction({ type: 'TOGGLE_DOWNSTREAM_SUPPLY', productId: product.id });
|
|
}
|
|
|
|
container.querySelectorAll('.chain-item-row').forEach(row => {
|
|
row.onclick = () => onAction({ type: 'SELECT_PRODUCT', productId: row.dataset.product });
|
|
});
|
|
}
|
|
|
|
export function updateInspector(container, state) {
|
|
const selectedId = state.selectedProductId || 'wheat';
|
|
if (selectedId !== currentMountedProductId) {
|
|
return; // Caller should remount if product changed
|
|
}
|
|
|
|
const product = getProduct(selectedId);
|
|
if (!product) return;
|
|
const pState = state.products[product.id] || { stock: 0, lines: 0, logistics: 0 };
|
|
|
|
// Update stock text
|
|
const elStock = document.getElementById('insp-stock');
|
|
if (elStock) {
|
|
elStock.textContent = `${fmtNum(pState.stock, 0)} ${product.unit}`;
|
|
}
|
|
|
|
// Update policy statuses
|
|
const isLogActive = pState.logisticsEnabled !== false;
|
|
const elLogStatus = document.getElementById('insp-toggle-log-status');
|
|
if (elLogStatus) {
|
|
elLogStatus.textContent = isLogActive ? 'ACTIVE' : 'PAUSED';
|
|
elLogStatus.style.color = isLogActive ? 'var(--emerald-dark)' : 'var(--amber-dark)';
|
|
}
|
|
|
|
const isDownActive = pState.supplyDownstreamEnabled !== false;
|
|
const elDownStatus = document.getElementById('insp-toggle-down-status');
|
|
if (elDownStatus) {
|
|
elDownStatus.textContent = isDownActive ? 'ALLOWED' : 'BLOCKED';
|
|
elDownStatus.style.color = isDownActive ? 'var(--emerald-dark)' : 'var(--amber-dark)';
|
|
}
|
|
|
|
// Update liquidation values & buttons
|
|
const totalLiquidationValue = (pState.stock || 0) * product.basePrice;
|
|
const halfLiquidationValue = totalLiquidationValue * 0.5;
|
|
|
|
const elHalfVal = document.getElementById('insp-val-half');
|
|
if (elHalfVal) elHalfVal.textContent = fmtCurrency(halfLiquidationValue);
|
|
|
|
const elAllVal = document.getElementById('insp-val-all');
|
|
if (elAllVal) elAllVal.textContent = fmtCurrency(totalLiquidationValue);
|
|
|
|
const btnHalf = document.getElementById('insp-btn-half');
|
|
if (btnHalf) btnHalf.disabled = pState.stock < 2;
|
|
|
|
const btnAll = document.getElementById('insp-btn-all');
|
|
if (btnAll) btnAll.disabled = pState.stock < 1;
|
|
|
|
// Update Net Flow rate
|
|
const netFlow = computeProductNetFlow(product, state);
|
|
const elNetRate = document.getElementById('insp-net-rate');
|
|
if (elNetRate) {
|
|
const sign = netFlow.netRate > 0.01 ? '+' : '';
|
|
const netText = `${sign}${fmtNum(netFlow.netRate, 1)}/s`;
|
|
if (elNetRate.textContent !== netText) {
|
|
elNetRate.textContent = netText;
|
|
}
|
|
const isDeficit = netFlow.netRate < -0.05;
|
|
const isSurplus = netFlow.netRate > 0.05;
|
|
elNetRate.className = `metric-val font-mono ${isDeficit ? 'text-warning' : (isSurplus ? 'text-profit' : '')}`;
|
|
}
|
|
|
|
// Update input values
|
|
if (product.inputs) {
|
|
for (const inp of product.inputs) {
|
|
const elInp = document.getElementById(`insp-inp-val-${inp.id}`);
|
|
if (elInp) {
|
|
elInp.textContent = fmtNum(state.products[inp.id]?.stock || 0, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|