Implemented features: - High-performance Rust extension with Redis persistence - Actor/player management with loadout, position, and state tracking - Banking system with deposit, withdraw, and transfer operations - Physical and virtual garage/locker systems for vehicle and equipment storage - Organization management with member tracking and permissions - Client-side UI with React-like state management - Server-side event-driven architecture with CBA Events - Security: Self-transfer prevention at multiple layers - Logging system with per-module log files - ICOM module for inter-server communication Co-Authored-By: Warp <agent@warp.dev>
282 lines
9.9 KiB
JavaScript
282 lines
9.9 KiB
JavaScript
/**
|
|
* Banking Interface
|
|
* Handles transfers, deposits, withdrawals, and account management
|
|
*/
|
|
|
|
// ============================================================================
|
|
// INITIALIZATION
|
|
// ============================================================================
|
|
|
|
function initBank() {
|
|
setupEventHandlers();
|
|
|
|
// Subscribe to store updates
|
|
if (typeof store !== 'undefined') {
|
|
store.subscribe(() => {
|
|
updateBalances();
|
|
renderTransactions();
|
|
});
|
|
}
|
|
|
|
// Initial render
|
|
updateBalances();
|
|
renderTransactions();
|
|
|
|
console.log('[Bank] Interface initialized');
|
|
}
|
|
|
|
// ============================================================================
|
|
// EVENT HANDLERS
|
|
// ============================================================================
|
|
|
|
function setupEventHandlers() {
|
|
// Close button
|
|
const closeBtn = document.querySelector('.close-btn');
|
|
if (closeBtn) {
|
|
closeBtn.addEventListener('click', () => {
|
|
sendEvent('bank::close', {});
|
|
});
|
|
}
|
|
|
|
// Transfer form
|
|
const transferBtn = document.getElementById('transferBtn');
|
|
const transferFrom = document.getElementById('transferFrom');
|
|
const amount = document.getElementById('amount');
|
|
const playerId = document.getElementById('playerId');
|
|
const playerIdGroup = document.getElementById('playerIdGroup');
|
|
|
|
// Always show player ID field since transfer is only to players
|
|
if (playerIdGroup) {
|
|
playerIdGroup.style.display = 'flex';
|
|
}
|
|
|
|
// Transfer button
|
|
if (transferBtn) {
|
|
transferBtn.addEventListener('click', () => {
|
|
const from = transferFrom.value;
|
|
const transferAmount = parseFloat(amount.value);
|
|
|
|
if (!transferAmount || transferAmount <= 0) {
|
|
console.log('Please enter a valid amount');
|
|
return;
|
|
}
|
|
|
|
if (!playerId.value) {
|
|
console.log('Please enter a player ID');
|
|
return;
|
|
}
|
|
|
|
const currentState = store.getState();
|
|
const fromAccountBalance = currentState.accounts[from];
|
|
|
|
if (transferAmount > fromAccountBalance) {
|
|
console.log('Insufficient funds');
|
|
return;
|
|
}
|
|
|
|
const transferData = {
|
|
from: from,
|
|
amount: transferAmount,
|
|
target: playerId.value
|
|
};
|
|
|
|
sendEvent('bank::transfer', transferData);
|
|
|
|
// Dispatch to store to update UI
|
|
store.dispatch(transfer(from, transferAmount, 'player'));
|
|
|
|
// Clear form
|
|
amount.value = '';
|
|
playerId.value = '';
|
|
});
|
|
}
|
|
|
|
// Quick action buttons
|
|
const quickActionBtns = document.querySelectorAll('.quick-action-btn');
|
|
quickActionBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const action = btn.dataset.action;
|
|
const currentState = store.getState();
|
|
|
|
switch (action) {
|
|
case 'deposit-amount':
|
|
const depositAmountStr = document.getElementById('amount').value;
|
|
if (depositAmountStr && parseFloat(depositAmountStr) > 0) {
|
|
const depositAmount = parseFloat(depositAmountStr);
|
|
if (depositAmount > currentState.accounts.cash) {
|
|
console.log('Insufficient cash');
|
|
return;
|
|
}
|
|
sendEvent('bank::deposit', { amount: depositAmount });
|
|
store.dispatch(deposit(depositAmount));
|
|
document.getElementById('amount').value = '';
|
|
} else {
|
|
console.log('Please enter a valid amount');
|
|
}
|
|
break;
|
|
case 'deposit':
|
|
const cashBalance = currentState.accounts.cash;
|
|
if (cashBalance <= 0) {
|
|
console.log('No cash to deposit');
|
|
return;
|
|
}
|
|
sendEvent('bank::deposit', { amount: cashBalance });
|
|
store.dispatch(deposit(cashBalance));
|
|
break;
|
|
case 'withdraw':
|
|
const amountStr = document.getElementById('amount').value;
|
|
if (amountStr && parseFloat(amountStr) > 0) {
|
|
const withdrawAmount = parseFloat(amountStr);
|
|
sendEvent('bank::withdraw', { amount: withdrawAmount });
|
|
store.dispatch(withdraw(withdrawAmount));
|
|
document.getElementById('amount').value = '';
|
|
} else {
|
|
console.log('Please enter a valid amount');
|
|
}
|
|
break;
|
|
default:
|
|
console.log('Invalid action');
|
|
break;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// UI UPDATES
|
|
// ============================================================================
|
|
|
|
function updateBalances() {
|
|
const currentState = store.getState();
|
|
const balanceElements = document.querySelectorAll('.balance-amount');
|
|
|
|
// The HTML structure has 3 account cards.
|
|
// 0: Cash, 1: Bank, 2: Org
|
|
if (balanceElements.length >= 3) {
|
|
balanceElements[0].textContent = `$${currentState.accounts.cash.toLocaleString()}`;
|
|
balanceElements[1].textContent = `$${currentState.accounts.bank.toLocaleString()}`;
|
|
balanceElements[2].textContent = `$${currentState.accounts.org.toLocaleString()}`;
|
|
}
|
|
|
|
// Update form options
|
|
const transferFrom = document.getElementById('transferFrom');
|
|
|
|
if (transferFrom) {
|
|
const currentSelection = transferFrom.value;
|
|
transferFrom.innerHTML = `
|
|
<option value="cash">Cash</option>
|
|
<option value="bank" selected>Bank Account</option>
|
|
`;
|
|
if (currentSelection && (currentSelection === 'cash' || currentSelection === 'bank')) {
|
|
transferFrom.value = currentSelection;
|
|
}
|
|
}
|
|
|
|
// Update player list
|
|
const playerSelect = document.getElementById('playerId');
|
|
if (playerSelect && currentState.accounts.players) {
|
|
const currentPlayerSelection = playerSelect.value;
|
|
const players = currentState.accounts.players;
|
|
const currentPlayerUid = currentState.uid;
|
|
|
|
// Clear existing options
|
|
playerSelect.innerHTML = '<option value="">Select Player...</option>';
|
|
|
|
// Handle hashmap structure from Arma (UID -> {name, uid})
|
|
if (players && typeof players === 'object') {
|
|
// Convert hashmap to array and iterate
|
|
Object.keys(players).forEach(uid => {
|
|
// Skip current player to prevent self-transfers
|
|
if (uid === currentPlayerUid) {
|
|
return;
|
|
}
|
|
|
|
const playerData = players[uid];
|
|
if (playerData && playerData.name) {
|
|
const option = document.createElement('option');
|
|
option.value = uid;
|
|
option.textContent = playerData.name;
|
|
playerSelect.appendChild(option);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (currentPlayerSelection) {
|
|
// Verify if the selected player is still in the list
|
|
const optionExists = Array.from(playerSelect.options).some(opt => opt.value === currentPlayerSelection);
|
|
if (optionExists) {
|
|
playerSelect.value = currentPlayerSelection;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function renderTransactions() {
|
|
const transactionList = document.querySelector('.transaction-list');
|
|
if (!transactionList) return;
|
|
|
|
transactionList.innerHTML = '';
|
|
|
|
const currentState = store.getState();
|
|
|
|
currentState.transactions.forEach((transaction, index) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'transaction-item';
|
|
|
|
// Deposits are gains (green), Withdrawals and Transfers are losses (red)
|
|
const isGain = transaction.type === 'Deposit';
|
|
const amountClass = isGain ? 'positive' : 'negative';
|
|
const displayAmount = isGain ? `+$${transaction.amount.toLocaleString()}` : `-$${Math.abs(transaction.amount).toLocaleString()}`;
|
|
|
|
// Map transaction types to CSS classes
|
|
const typeClassMap = {
|
|
'Deposit': 'deposit',
|
|
'Withdraw': 'withdrawal',
|
|
'Transfer': 'transfer'
|
|
};
|
|
const typeClass = typeClassMap[transaction.type] || transaction.type.toLowerCase();
|
|
|
|
item.innerHTML = `
|
|
<div class="transaction-header">
|
|
<span class="transaction-type ${typeClass}">${transaction.type}</span>
|
|
<span class="transaction-amount ${amountClass}">${displayAmount}</span>
|
|
</div>
|
|
<div class="transaction-details">
|
|
<span class="transaction-time">${transaction.date}</span>
|
|
</div>
|
|
`;
|
|
|
|
transactionList.appendChild(item);
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// ARMA 3 INTEGRATION
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Sends an event to Arma 3
|
|
* @param {string} event - Event name
|
|
* @param {Object} data - Event data
|
|
*/
|
|
function sendEvent(event, data) {
|
|
if (typeof A3API !== 'undefined') {
|
|
A3API.SendAlert(JSON.stringify({
|
|
event: event,
|
|
data: data
|
|
}));
|
|
} else {
|
|
console.log('Event:', event, 'Data:', data);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// AUTO-INITIALIZE
|
|
// ============================================================================
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initBank);
|
|
} else {
|
|
initBank();
|
|
}
|