333 lines
12 KiB
JavaScript

/**
* ATM App - Vanilla JS Kiosk Implementation
*/
//=============================================================================
// #region LIBRARY - DOM Helper
//=============================================================================
function h(tag, props = {}, ...children) {
const el = document.createElement(tag);
if (props) {
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith('on') && typeof value === 'function') {
el.addEventListener(key.substring(2).toLowerCase(), value);
} else if (key === 'className') {
el.className = value;
} else if (key === 'style' && typeof value === 'object') {
Object.assign(el.style, value);
} else {
el.setAttribute(key, value);
}
});
}
children.forEach(child => {
if (typeof child === 'string' || typeof child === 'number') {
el.appendChild(document.createTextNode(child));
} else if (child instanceof Node) {
el.appendChild(child);
} else if (Array.isArray(child)) {
child.forEach(c => {
if (c instanceof Node) el.appendChild(c);
});
}
});
return el;
}
let _rootContainer = null;
let _rootComponent = null;
function render(component, container) {
_rootContainer = container;
_rootComponent = component;
_render();
}
function _render() {
if (_rootContainer && _rootComponent) {
_rootContainer.innerHTML = '';
_rootContainer.appendChild(_rootComponent());
}
}
const createSignal = (initialValue) => {
let _val = initialValue;
const getValue = () => _val;
const setValue = (newValue) => {
_val = typeof newValue === 'function' ? newValue(_val) : newValue;
_render();
};
return [getValue, setValue];
};
//=============================================================================
// #region STATE
//=============================================================================
const [getView, setView] = createSignal('pin'); // 'pin', 'menu', 'withdraw', 'custom_withdraw', 'balance'
const [getPin, setPin] = createSignal('');
const [getCustomAmount, setCustomAmount] = createSignal('');
const [getMessage, setMessage] = createSignal('');
//=============================================================================
// #region UI COMPONENTS
//=============================================================================
function Header() {
return h('div', { className: 'header', style: { marginBottom: '2rem' } },
h('h1', null, 'ATM TERMINAL'),
h('p', null, 'Global Financial Network')
);
}
function PinView() {
const currentPin = getPin();
const handleNumClick = (num) => {
if (currentPin.length < 4) {
setPin(prev => prev + num);
}
};
const handleClear = () => setPin('');
const handleEnter = () => {
if (currentPin.length === 4) {
const state = typeof store !== 'undefined' ? store.getState() : { pin: '1234' };
if (currentPin === state.pin) {
setView('menu');
} else {
setMessage('Incorrect PIN');
setPin('');
setTimeout(() => setMessage(''), 2000);
}
} else {
setMessage('Invalid PIN Length');
setTimeout(() => setMessage(''), 2000);
}
};
return h('div', { className: 'card', style: { padding: '3rem 2rem' } },
h('h2', null, 'Enter Security PIN'),
h('div', { className: 'pin-display' },
currentPin.replace(/./g, String.fromCharCode(8226)) || '----'
),
h('p', { style: { color: '#ef4444', height: '1.5rem', textAlign: 'center' } }, getMessage()),
h('div', { className: 'numpad' },
['1', '2', '3', '4', '5', '6', '7', '8', '9'].map(num =>
h('button', { onClick: () => handleNumClick(num) }, num)
),
h('button', { style: { background: '#ef4444', color: 'white' }, onClick: handleClear }, 'C'),
h('button', { onClick: () => handleNumClick('0') }, '0'),
h('button', { style: { background: '#10b981', color: 'white' }, onClick: handleEnter }, String.fromCharCode(8629))
)
);
}
function MenuView() {
return h('div', { className: 'kiosk-content' },
h('h2', { style: { textAlign: 'center', marginBottom: '1rem' } }, 'Select Transaction'),
h('div', { className: 'kiosk-menu-stack' },
h('button', { className: 'kiosk-btn', onClick: () => setView('withdraw') },
'Withdraw Cash'
),
h('button', { className: 'kiosk-btn', onClick: () => setView('balance') },
'Check Balance'
),
h('button', {
className: 'kiosk-btn',
style: { background: 'var(--bg-surface)', color: 'var(--text-main)', border: '1px solid var(--border)' },
onClick: () => {
setPin('');
setView('pin');
sendEvent('atm::close', {});
}
}, 'Cancel Transaction')
)
);
}
function WithdrawView() {
const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
const bankBalance = state.accounts?.bank || 0;
const handleWithdraw = (amount) => {
if (bankBalance >= amount) {
if (typeof store !== 'undefined') {
store.dispatch(withdraw(amount));
}
sendEvent('atm::withdraw', { amount });
setMessage(`Please take your cash: $${amount.toLocaleString()}`);
setTimeout(() => {
setMessage('');
setView('menu');
}, 3000);
} else {
setMessage('Insufficient Funds');
setTimeout(() => setMessage(''), 2000);
}
};
if (getMessage()) {
return h('div', { className: 'card', style: { padding: '4rem', textAlign: 'center' } },
h('h2', { style: { color: 'var(--primary)' } }, getMessage())
);
}
return h('div', { className: 'kiosk-content' },
h('h2', { style: { textAlign: 'center', marginBottom: '1rem' } }, 'Select Amount'),
h('div', { className: 'kiosk-grid' },
h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(20) }, '$20'),
h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(50) }, '$50'),
h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(100) }, '$100'),
h('button', {
className: 'kiosk-btn',
onClick: () => {
setCustomAmount('');
setView('custom_withdraw');
}
}, 'Other Amount'),
h('button', { className: 'kiosk-btn', style: { gridColumn: 'span 2', background: 'var(--text-muted)' }, onClick: () => setView('menu') }, 'Cancel')
)
);
}
function CustomWithdrawView() {
const currentAmount = getCustomAmount();
const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
const bankBalance = state.accounts?.bank || 0;
const handleNumClick = (num) => {
if (currentAmount.length < 5) {
setCustomAmount(prev => prev + num);
}
};
const handleClear = () => setCustomAmount('');
const handleEnter = () => {
const amount = parseInt(currentAmount, 10);
if (amount > 0) {
if (bankBalance >= amount) {
if (typeof store !== 'undefined') {
store.dispatch(withdraw(amount));
}
sendEvent('atm::withdraw', { amount });
setMessage(`Please take your cash: $${amount.toLocaleString()}`);
setTimeout(() => {
setMessage('');
setView('menu');
}, 3000);
} else {
setMessage('Insufficient Funds');
setTimeout(() => setMessage(''), 2000);
}
} else {
setMessage('Invalid Amount');
setTimeout(() => setMessage(''), 2000);
}
};
if (getMessage()) {
return h('div', { className: 'card', style: { padding: '4rem', textAlign: 'center' } },
h('h2', { style: { color: 'var(--primary)' } }, getMessage())
);
}
return h('div', { className: 'card', style: { padding: '3rem 2rem' } },
h('h2', null, 'Enter Amount'),
h('div', { className: 'pin-display' },
currentAmount ? `$${currentAmount}` : '$0'
),
h('div', { className: 'numpad' },
['1', '2', '3', '4', '5', '6', '7', '8', '9'].map(num =>
h('button', { onClick: () => handleNumClick(num) }, num)
),
h('button', { style: { background: '#ef4444', color: 'white' }, onClick: handleClear }, 'C'),
h('button', { onClick: () => handleNumClick('0') }, '0'),
h('button', { style: { background: '#10b981', color: 'white' }, onClick: handleEnter }, String.fromCharCode(8629))
),
h('button', {
style: { width: '100%', marginTop: '2rem', padding: '1rem', background: 'var(--text-muted)' },
onClick: () => setView('withdraw')
}, 'Cancel')
);
}
function BalanceView() {
const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
const bankBalance = state.accounts?.bank || 0;
return h('div', { className: 'card', style: { textAlign: 'center', padding: '3rem' } },
h('h2', { style: { color: 'var(--text-muted)' } }, 'Available Balance'),
h('div', { style: { fontSize: '4rem', fontWeight: '800', margin: '2rem 0', color: 'var(--primary-hover)' } },
'$' + bankBalance.toLocaleString()
),
h('button', { className: 'kiosk-btn', style: { width: '100%', maxWidth: '300px', margin: '0 auto' }, onClick: () => setView('menu') }, 'Return to Menu')
);
}
function App() {
const view = getView();
let mainContent;
if (view === 'pin') {
mainContent = PinView();
} else if (view === 'menu') {
mainContent = MenuView();
} else if (view === 'withdraw') {
mainContent = WithdrawView();
} else if (view === 'custom_withdraw') {
mainContent = CustomWithdrawView();
} else if (view === 'balance') {
mainContent = BalanceView();
}
return h('main', null,
h('div', { className: 'container' },
Header(),
mainContent
)
);
}
//=============================================================================
// #region ARMA 3 INTEGRATION
//=============================================================================
function sendEvent(event, data) {
if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify({ event, data }));
} else {
console.log('Event:', event, 'Data:', data);
}
}
//=============================================================================
// #region INITIALIZATION
//=============================================================================
let initialized = false;
function initATM() {
if (initialized) return;
const root = document.getElementById('app');
if (root) {
if (typeof store !== 'undefined') {
store.subscribe(() => _render());
}
render(App, root);
initialized = true;
console.log('[ATM] Interface initialized');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initATM);
} else {
initATM();
}