398 lines
20 KiB
JavaScript
398 lines
20 KiB
JavaScript
// Production - The Idle Game: B2B Procurement Contracts Board
|
|
// Fine-grained DOM updates without innerHTML teardowns
|
|
|
|
import { getProduct } from '../data/products.js';
|
|
import { getClient } from '../data/contracts.js';
|
|
import { fmtCurrency, fmtNum, fmtTime } from '../core/formatters.js';
|
|
|
|
let currentMountedContractUids = '';
|
|
|
|
export function mountContractsBoard(container, state, onAction) {
|
|
const contracts = state.activeContracts || [];
|
|
currentMountedContractUids = contracts.map(c => c.uid).join(',');
|
|
|
|
const repPool = state.reputation || 0;
|
|
const allocs = state.repAllocations || { b2b: 0, clients: 0, brand: 0 };
|
|
const b2bRep = allocs.b2b || 0;
|
|
const clientsRep = allocs.clients || 0;
|
|
const brandRep = allocs.brand || 0;
|
|
const totalAllocated = b2bRep + clientsRep + brandRep;
|
|
const totalAccumulatedRep = repPool + totalAllocated;
|
|
|
|
// Computed bonus percentages
|
|
const b2bPayoutPct = (b2bRep * 0.5).toFixed(1);
|
|
const apexYieldPct = (clientsRep * 0.2).toFixed(1);
|
|
const titanDiscountPct = Math.min(50, clientsRep * 0.2).toFixed(1);
|
|
const omniSpeedPct = (clientsRep * 0.15).toFixed(1);
|
|
const vanguardCapPct = (clientsRep * 0.25).toFixed(1);
|
|
const brandPricePct = (brandRep * 0.25).toFixed(1);
|
|
|
|
let html = `
|
|
<div class="contracts-header-pane">
|
|
<div class="contracts-header-info">
|
|
<h2 class="view-title">
|
|
<iconify-icon icon="lucide:scroll-text" class="title-icon"></iconify-icon>
|
|
B2B Corporate Procurement Tenders
|
|
</h2>
|
|
<p class="view-subtitle">
|
|
Supply high-priority commodity shipments to corporate clients for massive cash injections and corporate prestige.
|
|
</p>
|
|
</div>
|
|
<div class="contracts-meta-badge">
|
|
<span>Ready to Deliver:</span>
|
|
<strong id="contract-stats-ready" class="font-mono text-profit">${state.readyContractsCount || 0}</strong>
|
|
<span style="margin-left: 8px; border-left: 1px solid var(--border-color); padding-left: 8px;">Completed:</span>
|
|
<strong id="contract-stats-completed" class="font-mono">${state.stats?.contractsCompleted || 0}</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Corporate Reputation Allocation Hub -->
|
|
<div class="rep-hub-container">
|
|
<div class="rep-hub-header">
|
|
<div class="rep-hub-title-group">
|
|
<div class="rep-hub-icon">
|
|
<iconify-icon icon="lucide:award"></iconify-icon>
|
|
</div>
|
|
<div>
|
|
<h3 class="rep-hub-title">Corporate Standing & Reputation Hub</h3>
|
|
<p class="rep-hub-desc">Distribute corporate reputation earned from fulfilled contracts across 3 strategic branches. Points can be reallocated at any time.</p>
|
|
</div>
|
|
</div>
|
|
<div class="rep-pool-card">
|
|
<div class="rep-pool-info">
|
|
<span class="rep-pool-label">Available Rep Pool</span>
|
|
<span class="rep-pool-val font-mono" id="rep-pool-val">${fmtNum(repPool, 0)}</span>
|
|
</div>
|
|
<div class="rep-pool-meta">
|
|
<span title="Total Reputation Earned This IPO Cycle">Accumulated: <strong>${fmtNum(totalAccumulatedRep, 0)}</strong></span>
|
|
<button class="btn-rep-reset" id="btn-rep-reset" ${totalAllocated > 0 ? '' : 'disabled'} title="Recall all allocated points back to available pool">
|
|
<iconify-icon icon="lucide:rotate-ccw"></iconify-icon> Reset Allocations
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rep-models-grid">
|
|
<!-- Model 1: B2B Standing -->
|
|
<div class="rep-model-card">
|
|
<div class="rep-model-head">
|
|
<div class="rep-model-icon rep-icon-b2b">
|
|
<iconify-icon icon="lucide:handshake"></iconify-icon>
|
|
</div>
|
|
<div class="rep-model-meta">
|
|
<h4>B2B Procurement Standing</h4>
|
|
<span class="rep-model-sub">Model 1: Tender Optimization</span>
|
|
</div>
|
|
<div class="rep-allocated-badge font-mono" id="rep-alloc-badge-b2b">${fmtNum(b2bRep, 0)} Rep</div>
|
|
</div>
|
|
<div class="rep-model-body">
|
|
<p class="rep-benefit-desc">Increases cash payouts on all delivered B2B tenders by establishing premier vendor standing.</p>
|
|
<div class="rep-bonus-stat">
|
|
<span>Contract Cash Bonus:</span>
|
|
<strong class="font-mono text-profit" id="rep-bonus-b2b">+${b2bPayoutPct}%</strong>
|
|
</div>
|
|
</div>
|
|
<div class="rep-model-actions">
|
|
<div class="rep-btn-group">
|
|
<button class="btn-rep-ctrl btn-rep-sub" data-dept="b2b" data-delta="-10" ${b2bRep >= 10 ? '' : 'disabled'}>-10</button>
|
|
<button class="btn-rep-ctrl btn-rep-sub" data-dept="b2b" data-delta="-1" ${b2bRep >= 1 ? '' : 'disabled'}>-1</button>
|
|
<button class="btn-rep-ctrl btn-rep-add" data-dept="b2b" data-delta="1" ${repPool >= 1 ? '' : 'disabled'}>+1</button>
|
|
<button class="btn-rep-ctrl btn-rep-add" data-dept="b2b" data-delta="10" ${repPool >= 10 ? '' : 'disabled'}>+10</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Model 2: Client Strategic Relations -->
|
|
<div class="rep-model-card">
|
|
<div class="rep-model-head">
|
|
<div class="rep-model-icon rep-icon-clients">
|
|
<iconify-icon icon="lucide:building-2"></iconify-icon>
|
|
</div>
|
|
<div class="rep-model-meta">
|
|
<h4>Client Strategic Relations</h4>
|
|
<span class="rep-model-sub">Model 2: MegaCorp Synergies</span>
|
|
</div>
|
|
<div class="rep-allocated-badge font-mono" id="rep-alloc-badge-clients">${fmtNum(clientsRep, 0)} Rep</div>
|
|
</div>
|
|
<div class="rep-model-body">
|
|
<p class="rep-benefit-desc">Deepens industrial partnerships with the 4 MegaCorps to unlock multifaceted operational synergies.</p>
|
|
<div class="rep-bonus-multi-grid">
|
|
<div class="rep-multi-item" title="Apex Resources Synergy">
|
|
<span class="client-name-tag text-amber">Apex Raw Yield</span>
|
|
<strong class="font-mono text-profit" id="rep-bonus-apex">+${apexYieldPct}%</strong>
|
|
</div>
|
|
<div class="rep-multi-item" title="Titan Heavy Industries Synergy">
|
|
<span class="client-name-tag text-blue">Titan Line Discount</span>
|
|
<strong class="font-mono text-profit" id="rep-bonus-titan">-${titanDiscountPct}%</strong>
|
|
</div>
|
|
<div class="rep-multi-item" title="Omni Dynamics Synergy">
|
|
<span class="client-name-tag text-purple">Omni Cycle Speed</span>
|
|
<strong class="font-mono text-profit" id="rep-bonus-omni">+${omniSpeedPct}%</strong>
|
|
</div>
|
|
<div class="rep-multi-item" title="Vanguard Shipping Synergy">
|
|
<span class="client-name-tag text-cyan">Vanguard Fleet Flow</span>
|
|
<strong class="font-mono text-profit" id="rep-bonus-vanguard">+${vanguardCapPct}%</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="rep-model-actions">
|
|
<div class="rep-btn-group">
|
|
<button class="btn-rep-ctrl btn-rep-sub" data-dept="clients" data-delta="-10" ${clientsRep >= 10 ? '' : 'disabled'}>-10</button>
|
|
<button class="btn-rep-ctrl btn-rep-sub" data-dept="clients" data-delta="-1" ${clientsRep >= 1 ? '' : 'disabled'}>-1</button>
|
|
<button class="btn-rep-ctrl btn-rep-add" data-dept="clients" data-delta="1" ${repPool >= 1 ? '' : 'disabled'}>+1</button>
|
|
<button class="btn-rep-ctrl btn-rep-add" data-dept="clients" data-delta="10" ${repPool >= 10 ? '' : 'disabled'}>+10</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Model 3: Wholesale Brand Premium -->
|
|
<div class="rep-model-card">
|
|
<div class="rep-model-head">
|
|
<div class="rep-model-icon rep-icon-brand">
|
|
<iconify-icon icon="lucide:sparkles"></iconify-icon>
|
|
</div>
|
|
<div class="rep-model-meta">
|
|
<h4>Wholesale Brand Premium</h4>
|
|
<span class="rep-model-sub">Model 3: Market Margin Power</span>
|
|
</div>
|
|
<div class="rep-allocated-badge font-mono" id="rep-alloc-badge-brand">${fmtNum(brandRep, 0)} Rep</div>
|
|
</div>
|
|
<div class="rep-model-body">
|
|
<p class="rep-benefit-desc">Builds prestigious global brand equity, boosting open-market wholesale prices across all logistics fleets.</p>
|
|
<div class="rep-bonus-stat">
|
|
<span>Wholesale Price Premium:</span>
|
|
<strong class="font-mono text-profit" id="rep-bonus-brand">+${brandPricePct}%</strong>
|
|
</div>
|
|
</div>
|
|
<div class="rep-model-actions">
|
|
<div class="rep-btn-group">
|
|
<button class="btn-rep-ctrl btn-rep-sub" data-dept="brand" data-delta="-10" ${brandRep >= 10 ? '' : 'disabled'}>-10</button>
|
|
<button class="btn-rep-ctrl btn-rep-sub" data-dept="brand" data-delta="-1" ${brandRep >= 1 ? '' : 'disabled'}>-1</button>
|
|
<button class="btn-rep-ctrl btn-rep-add" data-dept="brand" data-delta="1" ${repPool >= 1 ? '' : 'disabled'}>+1</button>
|
|
<button class="btn-rep-ctrl btn-rep-add" data-dept="brand" data-delta="10" ${repPool >= 10 ? '' : 'disabled'}>+10</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="contracts-grid">
|
|
`;
|
|
|
|
if (contracts.length === 0) {
|
|
html += `
|
|
<div class="empty-contracts-card">
|
|
<iconify-icon icon="lucide:clock" class="empty-icon"></iconify-icon>
|
|
<h3>Awaiting New Tenders</h3>
|
|
<p>Corporate procurement agents are preparing new commodity RFPs...</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
for (const c of contracts) {
|
|
const client = getClient(c.clientId);
|
|
const product = getProduct(c.productId);
|
|
const currentStock = state.products[c.productId]?.stock || 0;
|
|
const canFulfill = currentStock >= c.targetAmount;
|
|
const progressPct = Math.min(100, Math.floor((currentStock / c.targetAmount) * 100));
|
|
|
|
html += `
|
|
<div class="contract-card ${canFulfill ? 'ready-to-fulfill' : ''}" id="contract-${c.uid}">
|
|
<div class="contract-client-row">
|
|
<div class="client-brand">
|
|
<div class="client-avatar">
|
|
<iconify-icon icon="${client.icon}"></iconify-icon>
|
|
</div>
|
|
<div>
|
|
<h4 class="client-name">${client.name}</h4>
|
|
<span class="client-badge">${client.badge}</span>
|
|
</div>
|
|
</div>
|
|
<div class="contract-timer font-mono ${c.remainingSeconds < 60 ? 'timer-urgent' : ''}" id="timer-box-${c.uid}">
|
|
<iconify-icon icon="lucide:timer"></iconify-icon>
|
|
<span id="timer-val-${c.uid}">${fmtTime(c.remainingSeconds)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="contract-body">
|
|
<h3 class="contract-title">${c.title}</h3>
|
|
<div class="contract-req-row">
|
|
<div class="contract-req-item">
|
|
<span class="req-label">Required Commodity:</span>
|
|
<div class="req-val-wrap">
|
|
<iconify-icon icon="${product?.icon || 'lucide:box'}" style="color: ${product?.color || '#2563eb'};"></iconify-icon>
|
|
<strong>${product?.name}</strong>
|
|
</div>
|
|
</div>
|
|
<div class="contract-req-progress font-mono">
|
|
<span id="c-curr-stock-${c.uid}">${fmtNum(currentStock, 0)}</span> / <strong>${fmtNum(c.targetAmount, 0)}</strong> ${product?.unit || 'units'}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="contract-progress-track">
|
|
<div class="contract-progress-fill" id="c-fill-${c.uid}" style="width: ${progressPct}%;"></div>
|
|
</div>
|
|
|
|
<div class="contract-payout-box">
|
|
<div class="payout-col">
|
|
<span class="payout-label">Contract Payout:</span>
|
|
<span class="payout-val font-mono">${fmtCurrency(c.rewardCash)}</span>
|
|
</div>
|
|
<div class="payout-col">
|
|
<span class="payout-label">Corporate Rep:</span>
|
|
<span class="payout-val font-mono">+${c.rewardRep} Rep</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="contract-footer">
|
|
<button class="btn-primary-corp btn-fulfill-contract" id="c-btn-${c.uid}" data-uid="${c.uid}" ${canFulfill ? '' : 'disabled'}>
|
|
<iconify-icon icon="lucide:check-check"></iconify-icon>
|
|
<span id="c-btn-text-${c.uid}">${canFulfill ? 'Deliver Shipment & Collect Payout' : `Fulfill Tender (Need ${fmtNum(c.targetAmount - currentStock, 0)} more)`}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
html += `</div>`;
|
|
container.innerHTML = html;
|
|
|
|
container.querySelectorAll('.btn-fulfill-contract').forEach(btn => {
|
|
btn.onclick = () => onAction({ type: 'FULFILL_CONTRACT', uid: btn.dataset.uid });
|
|
});
|
|
|
|
// Rep Hub Allocation Control Listeners
|
|
container.querySelectorAll('.btn-rep-ctrl').forEach(btn => {
|
|
btn.onclick = () => onAction({
|
|
type: 'ALLOCATE_REP',
|
|
department: btn.dataset.dept,
|
|
delta: btn.dataset.delta
|
|
});
|
|
});
|
|
|
|
const btnReset = container.querySelector('#btn-rep-reset');
|
|
if (btnReset) {
|
|
btnReset.onclick = () => onAction({
|
|
type: 'ALLOCATE_REP',
|
|
delta: 'reset'
|
|
});
|
|
}
|
|
}
|
|
|
|
export function updateContractsBoard(container, state, onAction) {
|
|
const contracts = state.activeContracts || [];
|
|
const currentUids = contracts.map(c => c.uid).join(',');
|
|
|
|
// If active contracts changed (added, completed, expired), remount cleanly
|
|
if (currentUids !== currentMountedContractUids) {
|
|
mountContractsBoard(container, state, onAction);
|
|
return;
|
|
}
|
|
|
|
const elStatsReady = document.getElementById('contract-stats-ready');
|
|
if (elStatsReady) {
|
|
const val = String(state.readyContractsCount || 0);
|
|
if (elStatsReady.textContent !== val) elStatsReady.textContent = val;
|
|
}
|
|
|
|
const elStatsCompleted = document.getElementById('contract-stats-completed');
|
|
if (elStatsCompleted) {
|
|
const val = String(state.stats?.contractsCompleted || 0);
|
|
if (elStatsCompleted.textContent !== val) elStatsCompleted.textContent = val;
|
|
}
|
|
|
|
// Update Rep Pool & Allocations Live DOM
|
|
const repPool = state.reputation || 0;
|
|
const allocs = state.repAllocations || { b2b: 0, clients: 0, brand: 0 };
|
|
const b2bRep = allocs.b2b || 0;
|
|
const clientsRep = allocs.clients || 0;
|
|
const brandRep = allocs.brand || 0;
|
|
const totalAllocated = b2bRep + clientsRep + brandRep;
|
|
|
|
const elRepPool = document.getElementById('rep-pool-val');
|
|
if (elRepPool) {
|
|
const poolTxt = fmtNum(repPool, 0);
|
|
if (elRepPool.textContent !== poolTxt) elRepPool.textContent = poolTxt;
|
|
}
|
|
|
|
const elResetBtn = document.getElementById('btn-rep-reset');
|
|
if (elResetBtn) {
|
|
elResetBtn.disabled = totalAllocated <= 0;
|
|
}
|
|
|
|
// Model 1: B2B
|
|
const elBadgeB2B = document.getElementById('rep-alloc-badge-b2b');
|
|
if (elBadgeB2B) elBadgeB2B.textContent = `${fmtNum(b2bRep, 0)} Rep`;
|
|
const elBonusB2B = document.getElementById('rep-bonus-b2b');
|
|
if (elBonusB2B) elBonusB2B.textContent = `+${(b2bRep * 0.5).toFixed(1)}%`;
|
|
|
|
// Model 2: Clients
|
|
const elBadgeClients = document.getElementById('rep-alloc-badge-clients');
|
|
if (elBadgeClients) elBadgeClients.textContent = `${fmtNum(clientsRep, 0)} Rep`;
|
|
const elBonusApex = document.getElementById('rep-bonus-apex');
|
|
if (elBonusApex) elBonusApex.textContent = `+${(clientsRep * 0.2).toFixed(1)}%`;
|
|
const elBonusTitan = document.getElementById('rep-bonus-titan');
|
|
if (elBonusTitan) elBonusTitan.textContent = `-${Math.min(50, clientsRep * 0.2).toFixed(1)}%`;
|
|
const elBonusOmni = document.getElementById('rep-bonus-omni');
|
|
if (elBonusOmni) elBonusOmni.textContent = `+${(clientsRep * 0.15).toFixed(1)}%`;
|
|
const elBonusVanguard = document.getElementById('rep-bonus-vanguard');
|
|
if (elBonusVanguard) elBonusVanguard.textContent = `+${(clientsRep * 0.25).toFixed(1)}%`;
|
|
|
|
// Model 3: Brand
|
|
const elBadgeBrand = document.getElementById('rep-alloc-badge-brand');
|
|
if (elBadgeBrand) elBadgeBrand.textContent = `${fmtNum(brandRep, 0)} Rep`;
|
|
const elBonusBrand = document.getElementById('rep-bonus-brand');
|
|
if (elBonusBrand) elBonusBrand.textContent = `+${(brandRep * 0.25).toFixed(1)}%`;
|
|
|
|
// Rep Control Buttons Disabled State
|
|
container.querySelectorAll('.btn-rep-ctrl').forEach(btn => {
|
|
const dept = btn.dataset.dept;
|
|
const delta = parseInt(btn.dataset.delta, 10);
|
|
const currentDeptRep = allocs[dept] || 0;
|
|
if (delta > 0) {
|
|
btn.disabled = repPool < delta;
|
|
} else if (delta < 0) {
|
|
btn.disabled = currentDeptRep < Math.abs(delta);
|
|
}
|
|
});
|
|
|
|
for (const c of contracts) {
|
|
const currentStock = state.products[c.productId]?.stock || 0;
|
|
const canFulfill = currentStock >= c.targetAmount;
|
|
const progressPct = Math.min(100, Math.floor((currentStock / c.targetAmount) * 100));
|
|
|
|
// Timer
|
|
const elTimerVal = document.getElementById(`timer-val-${c.uid}`);
|
|
if (elTimerVal) elTimerVal.textContent = fmtTime(c.remainingSeconds);
|
|
|
|
const elTimerBox = document.getElementById(`timer-box-${c.uid}`);
|
|
if (elTimerBox) {
|
|
elTimerBox.classList.toggle('timer-urgent', c.remainingSeconds < 60);
|
|
}
|
|
|
|
// Current Stock Text
|
|
const elCurrStock = document.getElementById(`c-curr-stock-${c.uid}`);
|
|
if (elCurrStock) elCurrStock.textContent = fmtNum(currentStock, 0);
|
|
|
|
// Progress bar
|
|
const elFill = document.getElementById(`c-fill-${c.uid}`);
|
|
if (elFill) elFill.style.width = `${progressPct}%`;
|
|
|
|
// Card fulfill class
|
|
const elCard = document.getElementById(`contract-${c.uid}`);
|
|
if (elCard) elCard.classList.toggle('ready-to-fulfill', canFulfill);
|
|
|
|
// Button disabled & text
|
|
const elBtn = document.getElementById(`c-btn-${c.uid}`);
|
|
if (elBtn) {
|
|
elBtn.disabled = !canFulfill;
|
|
const elBtnText = document.getElementById(`c-btn-text-${c.uid}`);
|
|
if (elBtnText) {
|
|
elBtnText.textContent = canFulfill
|
|
? 'Deliver Shipment & Collect Payout'
|
|
: `Fulfill Tender (Need ${fmtNum(c.targetAmount - currentStock, 0)} more)`;
|
|
}
|
|
}
|
|
}
|
|
}
|