- Replace separate bank/ATM pages with a unified `index.html` app bundle - Split bank init into `initClass`, `initSessionService`, and `initUIBridge` - Route UI events through `BankUIBridge` and refresh session payloads after sync
1651 lines
52 KiB
JavaScript
1651 lines
52 KiB
JavaScript
/* Generated by tools/build-webui.mjs for Bank UI app. Do not edit directly. */
|
|
(function () {
|
|
const runtime = window.ForgeWebUI;
|
|
const BankApp = (window.BankApp = window.BankApp || {});
|
|
BankApp.runtime = runtime;
|
|
window.AppRuntime = runtime;
|
|
})();
|
|
|
|
(function () {
|
|
const BankApp = (window.BankApp = window.BankApp || {});
|
|
|
|
const defaultSession = {
|
|
mode: "bank",
|
|
orgFunds: 0,
|
|
orgName: "",
|
|
playerName: "",
|
|
transferTargets: [],
|
|
uid: "",
|
|
};
|
|
|
|
const defaultAccount = {
|
|
bank: 0,
|
|
cash: 0,
|
|
earnings: 0,
|
|
pin: "1234",
|
|
transactions: [],
|
|
};
|
|
|
|
function cloneValue(value) {
|
|
return JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function replaceObject(target, source) {
|
|
Object.keys(target).forEach((key) => delete target[key]);
|
|
Object.assign(target, cloneValue(source));
|
|
}
|
|
|
|
BankApp.data = {
|
|
account: Object.assign({}, defaultAccount),
|
|
session: Object.assign({}, defaultSession),
|
|
applyHydratePayload(payload) {
|
|
replaceObject(
|
|
this.session,
|
|
Object.assign({}, defaultSession, payload?.session || {}),
|
|
);
|
|
replaceObject(
|
|
this.account,
|
|
Object.assign({}, defaultAccount, payload?.account || {}),
|
|
);
|
|
},
|
|
};
|
|
})();
|
|
|
|
(function () {
|
|
const BankApp = (window.BankApp = window.BankApp || {});
|
|
const { createSignal } = BankApp.runtime;
|
|
|
|
class BankStore {
|
|
constructor() {
|
|
[this.getMode, this.setMode] = createSignal("bank");
|
|
[this.getNotice, this.setNotice] = createSignal({
|
|
text: "",
|
|
type: "",
|
|
});
|
|
[this.getPendingAction, this.setPendingAction] = createSignal("");
|
|
[this.getAtmView, this.setAtmView] = createSignal("pin");
|
|
[this.getEnteredPin, this.setEnteredPin] = createSignal("");
|
|
[this.getCustomAmount, this.setCustomAmount] = createSignal("");
|
|
[this.getAccountVersion, this.setAccountVersion] = createSignal(0);
|
|
[this.getSessionVersion, this.setSessionVersion] = createSignal(0);
|
|
}
|
|
|
|
finishAction() {
|
|
this.setPendingAction("");
|
|
}
|
|
|
|
hydrateFromPayload(payload) {
|
|
const mode = String(payload?.session?.mode || "bank")
|
|
.trim()
|
|
.toLowerCase();
|
|
const currentMode = this.getMode();
|
|
const currentAtmView = this.getAtmView();
|
|
|
|
this.setMode(mode === "atm" ? "atm" : "bank");
|
|
this.setPendingAction("");
|
|
this.setNotice({ text: "", type: "" });
|
|
this.setEnteredPin("");
|
|
this.setCustomAmount("");
|
|
this.setAccountVersion(this.getAccountVersion() + 1);
|
|
this.setSessionVersion(this.getSessionVersion() + 1);
|
|
|
|
if (mode === "atm") {
|
|
this.setAtmView(currentMode === "atm" ? currentAtmView : "pin");
|
|
return;
|
|
}
|
|
|
|
this.setAtmView("dashboard");
|
|
}
|
|
|
|
resetAtm() {
|
|
this.setEnteredPin("");
|
|
this.setCustomAmount("");
|
|
this.setAtmView("pin");
|
|
}
|
|
|
|
startAction(action) {
|
|
this.setPendingAction(
|
|
String(action || "")
|
|
.trim()
|
|
.toLowerCase(),
|
|
);
|
|
}
|
|
}
|
|
|
|
BankApp.store = new BankStore();
|
|
})();
|
|
|
|
(function () {
|
|
const BankApp = (window.BankApp = window.BankApp || {});
|
|
const store = BankApp.store;
|
|
const bridge = window.ForgeWebUI.createBridge({
|
|
closeEvent: "bank::close",
|
|
globalName: "ForgeBridge",
|
|
readyEvent: "bank::ready",
|
|
});
|
|
|
|
function hydrate(payloadData) {
|
|
BankApp.data.applyHydratePayload(payloadData);
|
|
store.hydrateFromPayload(payloadData);
|
|
}
|
|
|
|
bridge.on("bank::hydrate", hydrate);
|
|
bridge.on("bank::sync", hydrate);
|
|
bridge.on("bank::notice", (payloadData) => {
|
|
if (BankApp.actions) {
|
|
BankApp.actions.showNotice(
|
|
payloadData.type || "error",
|
|
payloadData.message || "Bank notice received.",
|
|
);
|
|
}
|
|
});
|
|
|
|
BankApp.bridge = {
|
|
notifyReady() {
|
|
return bridge.ready({ loaded: true });
|
|
},
|
|
receive: bridge.receive,
|
|
requestClose() {
|
|
return bridge.close({});
|
|
},
|
|
requestDeposit(payload) {
|
|
return bridge.send("bank::deposit::request", payload);
|
|
},
|
|
requestDepositEarnings(payload) {
|
|
return bridge.send("bank::depositEarnings::request", payload);
|
|
},
|
|
requestRefresh() {
|
|
return bridge.send("bank::refresh", {});
|
|
},
|
|
requestTransfer(payload) {
|
|
return bridge.send("bank::transfer::request", payload);
|
|
},
|
|
requestWithdraw(payload) {
|
|
return bridge.send("bank::withdraw::request", payload);
|
|
},
|
|
sendEvent: bridge.send,
|
|
};
|
|
})();
|
|
|
|
(function () {
|
|
const BankApp = (window.BankApp = window.BankApp || {});
|
|
const store = BankApp.store;
|
|
|
|
let noticeTimer = null;
|
|
|
|
function getAccount() {
|
|
return BankApp.data?.account || {};
|
|
}
|
|
|
|
function getSession() {
|
|
return BankApp.data?.session || {};
|
|
}
|
|
|
|
function normalizeAmount(value) {
|
|
const amount = Math.floor(Number(value || 0));
|
|
return Number.isFinite(amount) ? amount : 0;
|
|
}
|
|
|
|
function showNotice(type, text) {
|
|
store.setNotice({ type, text });
|
|
|
|
if (noticeTimer) {
|
|
clearTimeout(noticeTimer);
|
|
}
|
|
|
|
noticeTimer = setTimeout(() => {
|
|
store.setNotice({ text: "", type: "" });
|
|
noticeTimer = null;
|
|
}, 3200);
|
|
}
|
|
|
|
function closeBank() {
|
|
const bridge = BankApp.bridge;
|
|
if (bridge && typeof bridge.requestClose === "function") {
|
|
const sent = bridge.requestClose();
|
|
if (sent) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
showNotice("error", "Bank bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
function refreshBank() {
|
|
const bridge = BankApp.bridge;
|
|
if (bridge && typeof bridge.requestRefresh === "function") {
|
|
const sent = bridge.requestRefresh();
|
|
if (sent) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
showNotice("error", "Bank refresh bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
function requestDeposit(amountValue) {
|
|
const amount = normalizeAmount(amountValue);
|
|
const account = getAccount();
|
|
|
|
if (amount <= 0) {
|
|
showNotice("error", "Enter a valid deposit amount.");
|
|
return false;
|
|
}
|
|
|
|
if (amount > Number(account.cash || 0)) {
|
|
showNotice("error", "Cash on hand cannot cover that deposit.");
|
|
return false;
|
|
}
|
|
|
|
const bridge = BankApp.bridge;
|
|
if (!bridge || typeof bridge.requestDeposit !== "function") {
|
|
showNotice("error", "Deposit bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
store.startAction("deposit");
|
|
const sent = bridge.requestDeposit({ amount });
|
|
if (!sent) {
|
|
store.finishAction();
|
|
showNotice("error", "Deposit bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function requestWithdraw(amountValue) {
|
|
const amount = normalizeAmount(amountValue);
|
|
const account = getAccount();
|
|
|
|
if (amount <= 0) {
|
|
showNotice("error", "Enter a valid withdrawal amount.");
|
|
return false;
|
|
}
|
|
|
|
if (amount > Number(account.bank || 0)) {
|
|
showNotice("error", "Bank balance cannot cover that withdrawal.");
|
|
return false;
|
|
}
|
|
|
|
const bridge = BankApp.bridge;
|
|
if (!bridge || typeof bridge.requestWithdraw !== "function") {
|
|
showNotice("error", "Withdraw bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
store.startAction("withdraw");
|
|
const sent = bridge.requestWithdraw({ amount });
|
|
if (!sent) {
|
|
store.finishAction();
|
|
showNotice("error", "Withdraw bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function requestTransfer(targetUid, amountValue) {
|
|
const amount = normalizeAmount(amountValue);
|
|
const session = getSession();
|
|
const account = getAccount();
|
|
const targetId = String(targetUid || "").trim();
|
|
|
|
if (!targetId) {
|
|
showNotice("error", "Select a transfer recipient.");
|
|
return false;
|
|
}
|
|
|
|
if (targetId === String(session.uid || "")) {
|
|
showNotice("error", "You cannot transfer funds to yourself.");
|
|
return false;
|
|
}
|
|
|
|
if (amount <= 0) {
|
|
showNotice("error", "Enter a valid transfer amount.");
|
|
return false;
|
|
}
|
|
|
|
if (amount > Number(account.bank || 0)) {
|
|
showNotice("error", "Bank balance cannot cover that transfer.");
|
|
return false;
|
|
}
|
|
|
|
const bridge = BankApp.bridge;
|
|
if (!bridge || typeof bridge.requestTransfer !== "function") {
|
|
showNotice("error", "Transfer bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
store.startAction("transfer");
|
|
const sent = bridge.requestTransfer({
|
|
amount,
|
|
from: "bank",
|
|
target: targetId,
|
|
});
|
|
if (!sent) {
|
|
store.finishAction();
|
|
showNotice("error", "Transfer bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function requestDepositEarnings(amountValue) {
|
|
const amount = normalizeAmount(amountValue);
|
|
const account = getAccount();
|
|
|
|
if (amount <= 0) {
|
|
showNotice("error", "No earnings are available to deposit.");
|
|
return false;
|
|
}
|
|
|
|
if (amount > Number(account.earnings || 0)) {
|
|
showNotice(
|
|
"error",
|
|
"Pending earnings cannot cover that deposit request.",
|
|
);
|
|
return false;
|
|
}
|
|
|
|
const bridge = BankApp.bridge;
|
|
if (!bridge || typeof bridge.requestDepositEarnings !== "function") {
|
|
showNotice("error", "Earnings bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
store.startAction("depositearnings");
|
|
const sent = bridge.requestDepositEarnings({ amount });
|
|
if (!sent) {
|
|
store.finishAction();
|
|
showNotice("error", "Earnings bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function appendPinDigit(digit) {
|
|
const nextDigit = String(digit || "").trim();
|
|
if (!nextDigit) {
|
|
return;
|
|
}
|
|
|
|
const currentPin = String(store.getEnteredPin() || "");
|
|
if (currentPin.length >= 4) {
|
|
return;
|
|
}
|
|
|
|
store.setEnteredPin(currentPin + nextDigit);
|
|
}
|
|
|
|
function backspacePin() {
|
|
const currentPin = String(store.getEnteredPin() || "");
|
|
store.setEnteredPin(currentPin.slice(0, -1));
|
|
}
|
|
|
|
function clearPin() {
|
|
store.setEnteredPin("");
|
|
}
|
|
|
|
function submitPin() {
|
|
const enteredPin = String(store.getEnteredPin() || "");
|
|
const actualPin = String(getAccount().pin || "1234");
|
|
|
|
if (enteredPin.length !== 4) {
|
|
showNotice("error", "Enter your four-digit access PIN.");
|
|
return false;
|
|
}
|
|
|
|
if (enteredPin !== actualPin) {
|
|
clearPin();
|
|
showNotice("error", "Incorrect PIN.");
|
|
return false;
|
|
}
|
|
|
|
clearPin();
|
|
store.setAtmView("menu");
|
|
return true;
|
|
}
|
|
|
|
function selectAtmView(view) {
|
|
const nextView = String(view || "").trim();
|
|
if (!nextView) {
|
|
return false;
|
|
}
|
|
|
|
if (nextView === "pin") {
|
|
store.resetAtm();
|
|
return true;
|
|
}
|
|
|
|
store.setCustomAmount("");
|
|
store.setAtmView(nextView);
|
|
return true;
|
|
}
|
|
|
|
function appendCustomAmountDigit(digit) {
|
|
const nextDigit = String(digit || "").trim();
|
|
if (!nextDigit) {
|
|
return;
|
|
}
|
|
|
|
const currentValue = String(store.getCustomAmount() || "");
|
|
if (currentValue.length >= 7) {
|
|
return;
|
|
}
|
|
|
|
store.setCustomAmount(currentValue + nextDigit);
|
|
}
|
|
|
|
function backspaceCustomAmount() {
|
|
const currentValue = String(store.getCustomAmount() || "");
|
|
store.setCustomAmount(currentValue.slice(0, -1));
|
|
}
|
|
|
|
function clearCustomAmount() {
|
|
store.setCustomAmount("");
|
|
}
|
|
|
|
function submitCustomAmount(kind) {
|
|
const amount = normalizeAmount(store.getCustomAmount());
|
|
const nextKind = String(kind || "")
|
|
.trim()
|
|
.toLowerCase();
|
|
|
|
if (amount <= 0) {
|
|
showNotice("error", "Enter a valid transaction amount.");
|
|
return false;
|
|
}
|
|
|
|
const success =
|
|
nextKind === "deposit"
|
|
? requestDeposit(amount)
|
|
: requestWithdraw(amount);
|
|
|
|
if (success) {
|
|
store.setCustomAmount("");
|
|
store.setAtmView("menu");
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
function requestAtmAmount(kind, amount) {
|
|
const nextKind = String(kind || "")
|
|
.trim()
|
|
.toLowerCase();
|
|
const success =
|
|
nextKind === "deposit"
|
|
? requestDeposit(amount)
|
|
: requestWithdraw(amount);
|
|
|
|
if (success) {
|
|
store.setAtmView("menu");
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
BankApp.actions = {
|
|
appendCustomAmountDigit,
|
|
appendPinDigit,
|
|
backspaceCustomAmount,
|
|
backspacePin,
|
|
clearCustomAmount,
|
|
clearPin,
|
|
closeBank,
|
|
refreshBank,
|
|
requestAtmAmount,
|
|
requestDeposit,
|
|
requestDepositEarnings,
|
|
requestTransfer,
|
|
requestWithdraw,
|
|
selectAtmView,
|
|
showNotice,
|
|
submitCustomAmount,
|
|
submitPin,
|
|
};
|
|
})();
|
|
|
|
(function () {
|
|
const BankApp = (window.BankApp = window.BankApp || {});
|
|
const { h } = BankApp.runtime;
|
|
const store = BankApp.store;
|
|
const { account } = BankApp.data;
|
|
|
|
function formatCurrency(value) {
|
|
return `$${Math.round(Number(value || 0)).toLocaleString()}`;
|
|
}
|
|
|
|
function pending(actionName) {
|
|
return store.getPendingAction() === actionName;
|
|
}
|
|
|
|
function statCard(label, value, tone = "") {
|
|
return h(
|
|
"div",
|
|
{
|
|
className: tone
|
|
? `bank-stat-card is-${tone}`
|
|
: "bank-stat-card",
|
|
},
|
|
h("span", { className: "bank-stat-label" }, label),
|
|
h("span", { className: "bank-stat-value" }, value),
|
|
);
|
|
}
|
|
|
|
function metricCard(label, value, copy, tone = "") {
|
|
return h(
|
|
"div",
|
|
{
|
|
className: tone
|
|
? `bank-metric-card is-${tone}`
|
|
: "bank-metric-card",
|
|
},
|
|
h("span", { className: "bank-eyebrow" }, label),
|
|
h("span", { className: "bank-metric-value" }, value),
|
|
h("span", { className: "bank-metric-copy" }, copy),
|
|
);
|
|
}
|
|
|
|
function pinIndicators(value) {
|
|
const pin = String(value || "");
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "bank-pin-indicators" },
|
|
[0, 1, 2, 3].map((index) =>
|
|
h("span", {
|
|
className:
|
|
index < pin.length
|
|
? "bank-pin-indicator is-filled"
|
|
: "bank-pin-indicator",
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
function readInputValue(id) {
|
|
return document.getElementById(id)?.value || "";
|
|
}
|
|
|
|
function clearInputValue(id) {
|
|
const input = document.getElementById(id);
|
|
if (input) {
|
|
input.value = "";
|
|
}
|
|
}
|
|
|
|
function keypad(onDigit, onBackspace, onClear, onEnter) {
|
|
const keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "bank-keypad" },
|
|
keys.map((digit) =>
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-key",
|
|
onClick: () => onDigit(digit),
|
|
},
|
|
digit,
|
|
),
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-key is-muted",
|
|
onClick: onClear,
|
|
},
|
|
"C",
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-key",
|
|
onClick: () => onDigit("0"),
|
|
},
|
|
"0",
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-key is-accent",
|
|
onClick: onEnter,
|
|
},
|
|
"Enter",
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-key is-wide",
|
|
onClick: onBackspace,
|
|
},
|
|
"Backspace",
|
|
),
|
|
);
|
|
}
|
|
|
|
function transactionRows() {
|
|
const transactions = Array.isArray(account.transactions)
|
|
? account.transactions
|
|
: [];
|
|
|
|
if (transactions.length === 0) {
|
|
return h(
|
|
"div",
|
|
{ className: "bank-empty-state" },
|
|
h("h3", { className: "bank-empty-title" }, "No transactions"),
|
|
h(
|
|
"p",
|
|
{ className: "bank-empty-copy" },
|
|
"Deposits, withdrawals, and transfers will appear here after the account begins moving funds.",
|
|
),
|
|
);
|
|
}
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "bank-history-list" },
|
|
transactions
|
|
.slice(0, 8)
|
|
.map((entry) =>
|
|
h(
|
|
"div",
|
|
{ className: "bank-history-row" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-history-copy" },
|
|
h(
|
|
"span",
|
|
{ className: "bank-history-title" },
|
|
entry.type || "Transaction",
|
|
),
|
|
h(
|
|
"span",
|
|
{ className: "bank-history-meta" },
|
|
entry.date || "Pending timestamp",
|
|
),
|
|
),
|
|
h(
|
|
"span",
|
|
{ className: "bank-history-value" },
|
|
formatCurrency(entry.amount || 0),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
BankApp.componentFns = BankApp.componentFns || {};
|
|
Object.assign(BankApp.componentFns, {
|
|
clearInputValue,
|
|
formatCurrency,
|
|
keypad,
|
|
metricCard,
|
|
pending,
|
|
pinIndicators,
|
|
readInputValue,
|
|
statCard,
|
|
transactionRows,
|
|
});
|
|
})();
|
|
|
|
(function () {
|
|
const BankApp = (window.BankApp = window.BankApp || {});
|
|
const { h } = BankApp.runtime;
|
|
const store = BankApp.store;
|
|
const actions = BankApp.actions;
|
|
const { account, session } = BankApp.data;
|
|
const { formatCurrency, statCard } = BankApp.componentFns;
|
|
|
|
BankApp.componentFns = BankApp.componentFns || {};
|
|
BankApp.componentFns.BankSidebar = function BankSidebar() {
|
|
store.getAccountVersion();
|
|
store.getSessionVersion();
|
|
|
|
return h(
|
|
"aside",
|
|
{ className: "bank-sidebar" },
|
|
h(
|
|
"section",
|
|
{ className: "bank-module" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-module-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h("span", { className: "bank-eyebrow" }, "Account"),
|
|
h(
|
|
"h2",
|
|
{ className: "bank-section-title" },
|
|
"Balances",
|
|
),
|
|
),
|
|
h("span", { className: "bank-pill" }, "Live"),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "bank-summary-grid" },
|
|
statCard("Bank", formatCurrency(account.bank), "accent"),
|
|
statCard("Cash", formatCurrency(account.cash)),
|
|
statCard(
|
|
"Earnings",
|
|
formatCurrency(account.earnings),
|
|
account.earnings > 0 ? "warning" : "",
|
|
),
|
|
statCard(
|
|
"Org Funds",
|
|
formatCurrency(session.orgFunds),
|
|
session.orgFunds > 0 ? "success" : "",
|
|
),
|
|
),
|
|
),
|
|
h(
|
|
"section",
|
|
{ className: "bank-module" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-module-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h("span", { className: "bank-eyebrow" }, "Profile"),
|
|
h(
|
|
"h2",
|
|
{ className: "bank-section-title" },
|
|
"Account Holder",
|
|
),
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-secondary",
|
|
onClick: () => actions.refreshBank(),
|
|
},
|
|
"Refresh",
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "bank-profile-stack" },
|
|
statCard("Name", session.playerName || "Unknown"),
|
|
statCard("UID", session.uid || "-"),
|
|
statCard(
|
|
"Organization",
|
|
session.orgName || "No active organization",
|
|
),
|
|
),
|
|
),
|
|
);
|
|
};
|
|
})();
|
|
|
|
(function () {
|
|
const BankApp = (window.BankApp = window.BankApp || {});
|
|
const { h } = BankApp.runtime;
|
|
const store = BankApp.store;
|
|
const { account, session } = BankApp.data;
|
|
const { formatCurrency } = BankApp.componentFns;
|
|
|
|
BankApp.componentFns = BankApp.componentFns || {};
|
|
BankApp.componentFns.BankFooter = function BankFooter() {
|
|
store.getAccountVersion();
|
|
store.getSessionVersion();
|
|
|
|
const sections = [
|
|
{
|
|
title: "Banking Resources",
|
|
items: [
|
|
"Account Access Policy",
|
|
"Transfer & Wire Guidelines",
|
|
"Cash Handling Schedule",
|
|
"Terminal Security Notice",
|
|
],
|
|
},
|
|
{
|
|
title: "Bank Support",
|
|
items: session.orgName
|
|
? [
|
|
`Organization: ${session.orgName}`,
|
|
`Treasury Reference: ${formatCurrency(session.orgFunds)}`,
|
|
`${session.transferTargets.length} transfer recipient(s) currently visible.`,
|
|
`Primary Ledger: ${formatCurrency(account.bank)}`,
|
|
]
|
|
: [
|
|
"Organization: No active treasury link",
|
|
`${session.transferTargets.length} transfer recipient(s) currently visible.`,
|
|
`Primary Ledger: ${formatCurrency(account.bank)}`,
|
|
`Cash On Hand: ${formatCurrency(account.cash)}`,
|
|
],
|
|
},
|
|
];
|
|
|
|
return h(
|
|
"footer",
|
|
{ className: "bank-footer-bar" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-footer" },
|
|
...sections.map((section) =>
|
|
h(
|
|
"div",
|
|
{ className: "bank-footer-block" },
|
|
h(
|
|
"h3",
|
|
{ className: "bank-footer-title" },
|
|
section.title,
|
|
),
|
|
h(
|
|
"ul",
|
|
{ className: "bank-footer-list" },
|
|
...(section.items || []).map((item) =>
|
|
h(
|
|
"li",
|
|
{ className: "bank-footer-copy" },
|
|
item,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
};
|
|
})();
|
|
|
|
(function () {
|
|
const BankApp = (window.BankApp = window.BankApp || {});
|
|
const { h } = BankApp.runtime;
|
|
const store = BankApp.store;
|
|
const actions = BankApp.actions;
|
|
const { account, session } = BankApp.data;
|
|
const {
|
|
clearInputValue,
|
|
formatCurrency,
|
|
metricCard,
|
|
pending,
|
|
readInputValue,
|
|
transactionRows,
|
|
} = BankApp.componentFns;
|
|
|
|
function trackAccount() {
|
|
store.getAccountVersion();
|
|
}
|
|
|
|
function trackSession() {
|
|
store.getSessionVersion();
|
|
}
|
|
|
|
function pageHeader() {
|
|
trackSession();
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "bank-page-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h("span", { className: "bank-eyebrow" }, "Treasury Desk"),
|
|
h("h1", { className: "bank-title" }, "Personal Banking"),
|
|
),
|
|
h(
|
|
"span",
|
|
{ className: "bank-pill" },
|
|
session.playerName || "Account Holder",
|
|
),
|
|
);
|
|
}
|
|
|
|
function summarySection() {
|
|
trackAccount();
|
|
trackSession();
|
|
|
|
return h(
|
|
"section",
|
|
{ className: "bank-page-section bank-summary-section" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-section-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h("span", { className: "bank-eyebrow" }, "Overview"),
|
|
h(
|
|
"h2",
|
|
{ className: "bank-section-title" },
|
|
"Financial Position",
|
|
),
|
|
),
|
|
h("span", { className: "bank-pill" }, "Banking Desk"),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "bank-summary-band" },
|
|
metricCard(
|
|
"Primary Balance",
|
|
formatCurrency(account.bank),
|
|
"Available for transfers and withdrawals.",
|
|
"accent",
|
|
),
|
|
metricCard(
|
|
"Cash On Hand",
|
|
formatCurrency(account.cash),
|
|
"Funds currently carried by the player.",
|
|
),
|
|
metricCard(
|
|
"Pending Earnings",
|
|
formatCurrency(account.earnings),
|
|
"Ready to sweep into the main account ledger.",
|
|
account.earnings > 0 ? "warning" : "",
|
|
),
|
|
metricCard(
|
|
"Org Snapshot",
|
|
formatCurrency(session.orgFunds),
|
|
"Reference value pulled from the organization treasury.",
|
|
session.orgFunds > 0 ? "success" : "",
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
function actionSections() {
|
|
trackSession();
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "bank-action-sections" },
|
|
h(
|
|
"section",
|
|
{ className: "bank-page-section" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-section-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h("span", { className: "bank-eyebrow" }, "Movement"),
|
|
h(
|
|
"h2",
|
|
{ className: "bank-section-title" },
|
|
"Deposit / Withdraw",
|
|
),
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "bank-form-stack" },
|
|
h("input", {
|
|
id: "bank-amount-input",
|
|
className: "bank-input",
|
|
type: "number",
|
|
min: "1",
|
|
placeholder: "Enter amount",
|
|
}),
|
|
h(
|
|
"div",
|
|
{ className: "bank-action-row" },
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-primary",
|
|
disabled: pending("deposit"),
|
|
onClick: () => {
|
|
const sent = actions.requestDeposit(
|
|
readInputValue("bank-amount-input"),
|
|
);
|
|
if (sent) {
|
|
clearInputValue("bank-amount-input");
|
|
}
|
|
},
|
|
},
|
|
pending("deposit") ? "Depositing..." : "Deposit",
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-secondary",
|
|
disabled: pending("withdraw"),
|
|
onClick: () => {
|
|
const sent = actions.requestWithdraw(
|
|
readInputValue("bank-amount-input"),
|
|
);
|
|
if (sent) {
|
|
clearInputValue("bank-amount-input");
|
|
}
|
|
},
|
|
},
|
|
pending("withdraw") ? "Withdrawing..." : "Withdraw",
|
|
),
|
|
),
|
|
),
|
|
),
|
|
h(
|
|
"section",
|
|
{ className: "bank-page-section" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-section-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h("span", { className: "bank-eyebrow" }, "Transfer"),
|
|
h(
|
|
"h2",
|
|
{ className: "bank-section-title" },
|
|
"Wire Funds",
|
|
),
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "bank-form-stack" },
|
|
h(
|
|
"select",
|
|
{
|
|
id: "bank-transfer-target",
|
|
className: "bank-select",
|
|
},
|
|
h(
|
|
"option",
|
|
{ value: "" },
|
|
session.transferTargets.length > 0
|
|
? "Select recipient"
|
|
: "No available recipients",
|
|
),
|
|
session.transferTargets.map((entry) =>
|
|
h(
|
|
"option",
|
|
{ value: entry.uid },
|
|
entry.name || entry.uid,
|
|
),
|
|
),
|
|
),
|
|
h("input", {
|
|
id: "bank-transfer-amount",
|
|
className: "bank-input",
|
|
type: "number",
|
|
min: "1",
|
|
placeholder: "Enter transfer amount",
|
|
}),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-primary",
|
|
disabled:
|
|
pending("transfer") ||
|
|
session.transferTargets.length === 0,
|
|
onClick: () => {
|
|
const sent = actions.requestTransfer(
|
|
readInputValue("bank-transfer-target"),
|
|
readInputValue("bank-transfer-amount"),
|
|
);
|
|
if (sent) {
|
|
clearInputValue("bank-transfer-amount");
|
|
}
|
|
},
|
|
},
|
|
pending("transfer")
|
|
? "Transferring..."
|
|
: "Transfer Funds",
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
function supportSection() {
|
|
trackAccount();
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "bank-support-sections" },
|
|
h(
|
|
"section",
|
|
{ className: "bank-page-section" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-section-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h("span", { className: "bank-eyebrow" }, "Sweep"),
|
|
h(
|
|
"h2",
|
|
{ className: "bank-section-title" },
|
|
"Deposit Earnings",
|
|
),
|
|
),
|
|
),
|
|
h(
|
|
"p",
|
|
{ className: "bank-card-copy" },
|
|
"Sweep pending earnings into the primary account when you want them reflected in the main balance.",
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-primary",
|
|
disabled:
|
|
pending("depositearnings") ||
|
|
Number(account.earnings || 0) <= 0,
|
|
onClick: () =>
|
|
actions.requestDepositEarnings(account.earnings),
|
|
},
|
|
pending("depositearnings")
|
|
? "Depositing..."
|
|
: "Deposit Earnings",
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
function historySection() {
|
|
trackAccount();
|
|
|
|
return h(
|
|
"section",
|
|
{ className: "bank-page-section bank-history-section" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-section-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h("span", { className: "bank-eyebrow" }, "History"),
|
|
h(
|
|
"h2",
|
|
{ className: "bank-section-title" },
|
|
"Recent Transactions",
|
|
),
|
|
),
|
|
),
|
|
transactionRows(),
|
|
);
|
|
}
|
|
|
|
BankApp.componentFns = BankApp.componentFns || {};
|
|
BankApp.componentFns.BankPageHeader = pageHeader;
|
|
BankApp.componentFns.BankSummarySection = summarySection;
|
|
BankApp.componentFns.BankActionSections = actionSections;
|
|
BankApp.componentFns.BankSupportSection = supportSection;
|
|
BankApp.componentFns.BankHistorySection = historySection;
|
|
})();
|
|
|
|
(function () {
|
|
const BankApp = (window.BankApp = window.BankApp || {});
|
|
const { h } = BankApp.runtime;
|
|
const store = BankApp.store;
|
|
const actions = BankApp.actions;
|
|
const { account } = BankApp.data;
|
|
const { formatCurrency, keypad, pinIndicators } = BankApp.componentFns;
|
|
|
|
function atmMenuCard() {
|
|
return h(
|
|
"div",
|
|
{ className: "bank-atm-action-grid" },
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-primary",
|
|
onClick: () => actions.selectAtmView("withdraw"),
|
|
},
|
|
"Withdraw Cash",
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-primary",
|
|
onClick: () => actions.selectAtmView("deposit"),
|
|
},
|
|
"Deposit Cash",
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-secondary",
|
|
onClick: () => actions.selectAtmView("balance"),
|
|
},
|
|
"Check Balance",
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-secondary",
|
|
onClick: () => actions.closeBank(),
|
|
},
|
|
"Exit Terminal",
|
|
),
|
|
);
|
|
}
|
|
|
|
function atmAmountMenu(kind) {
|
|
const label = kind === "deposit" ? "Deposit" : "Withdraw";
|
|
const amounts = [20, 50, 100, 500];
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "bank-atm-action-grid" },
|
|
amounts.map((amount) =>
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-primary",
|
|
onClick: () => actions.requestAtmAmount(kind, amount),
|
|
},
|
|
`${label} ${formatCurrency(amount)}`,
|
|
),
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-secondary",
|
|
onClick: () =>
|
|
actions.selectAtmView(
|
|
kind === "deposit"
|
|
? "customDeposit"
|
|
: "customWithdraw",
|
|
),
|
|
},
|
|
"Custom Amount",
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-secondary",
|
|
onClick: () => actions.selectAtmView("menu"),
|
|
},
|
|
"Back",
|
|
),
|
|
);
|
|
}
|
|
|
|
function atmCustomAmount(kind) {
|
|
const label = kind === "deposit" ? "Deposit" : "Withdraw";
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "bank-atm-stack" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-pin-display" },
|
|
store.getCustomAmount()
|
|
? formatCurrency(store.getCustomAmount())
|
|
: "$0",
|
|
),
|
|
keypad(
|
|
actions.appendCustomAmountDigit,
|
|
actions.backspaceCustomAmount,
|
|
actions.clearCustomAmount,
|
|
() => actions.submitCustomAmount(kind),
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-secondary",
|
|
onClick: () => actions.selectAtmView("menu"),
|
|
},
|
|
`Cancel ${label}`,
|
|
),
|
|
);
|
|
}
|
|
|
|
BankApp.componentFns = BankApp.componentFns || {};
|
|
BankApp.componentFns.ATMView = function ATMView() {
|
|
store.getAccountVersion();
|
|
const atmViewName = store.getAtmView();
|
|
const enteredPin = String(store.getEnteredPin() || "");
|
|
let title = "Terminal Access";
|
|
let copy =
|
|
"Authenticate with the four-digit account PIN before using the terminal.";
|
|
let content = null;
|
|
|
|
switch (atmViewName) {
|
|
case "menu":
|
|
title = "ATM Menu";
|
|
copy =
|
|
"Select a banking action. The ATM can deposit, withdraw, and show the live account balance.";
|
|
content = atmMenuCard();
|
|
break;
|
|
case "withdraw":
|
|
title = "Withdraw Cash";
|
|
copy =
|
|
"Choose a preset amount or enter a custom amount for withdrawal.";
|
|
content = atmAmountMenu("withdraw");
|
|
break;
|
|
case "deposit":
|
|
title = "Deposit Cash";
|
|
copy =
|
|
"Move cash on hand back into the main bank balance from the terminal.";
|
|
content = atmAmountMenu("deposit");
|
|
break;
|
|
case "customWithdraw":
|
|
title = "Custom Withdraw";
|
|
copy = "Enter the exact withdrawal amount.";
|
|
content = atmCustomAmount("withdraw");
|
|
break;
|
|
case "customDeposit":
|
|
title = "Custom Deposit";
|
|
copy = "Enter the exact deposit amount.";
|
|
content = atmCustomAmount("deposit");
|
|
break;
|
|
case "balance":
|
|
title = "Available Balance";
|
|
copy = "Current bank balance available at this terminal.";
|
|
content = h(
|
|
"div",
|
|
{ className: "bank-atm-stack" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-balance-display" },
|
|
formatCurrency(account.bank),
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-primary",
|
|
onClick: () => actions.selectAtmView("menu"),
|
|
},
|
|
"Return to Menu",
|
|
),
|
|
);
|
|
break;
|
|
default:
|
|
content = h(
|
|
"div",
|
|
{ className: "bank-atm-stack" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-pin-display" },
|
|
pinIndicators(enteredPin),
|
|
),
|
|
keypad(
|
|
actions.appendPinDigit,
|
|
actions.backspacePin,
|
|
actions.clearPin,
|
|
actions.submitPin,
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bank-btn bank-btn-secondary",
|
|
onClick: () => actions.closeBank(),
|
|
},
|
|
"Exit Terminal",
|
|
),
|
|
);
|
|
break;
|
|
}
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "bank-atm-shell" },
|
|
h(
|
|
"section",
|
|
{ className: "bank-atm-panel" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-panel-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h("span", { className: "bank-eyebrow" }, "ATM"),
|
|
h("h1", { className: "bank-title" }, title),
|
|
),
|
|
h("span", { className: "bank-pill" }, "Secure Terminal"),
|
|
),
|
|
h("p", { className: "bank-panel-copy" }, copy),
|
|
content,
|
|
),
|
|
);
|
|
};
|
|
})();
|
|
|
|
(function () {
|
|
const BankApp = (window.BankApp = window.BankApp || {});
|
|
const { h } = BankApp.runtime;
|
|
const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar;
|
|
const store = BankApp.store;
|
|
const actions = BankApp.actions;
|
|
|
|
BankApp.componentFns = BankApp.componentFns || {};
|
|
BankApp.componentFns.NoticeLayer = function NoticeLayer() {
|
|
const notice = store.getNotice();
|
|
|
|
if (!notice.text) {
|
|
return null;
|
|
}
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "bank-notice-stack" },
|
|
h(
|
|
"div",
|
|
{
|
|
className:
|
|
notice.type === "error"
|
|
? "bank-notice is-error"
|
|
: "bank-notice is-success",
|
|
},
|
|
notice.text,
|
|
),
|
|
);
|
|
};
|
|
|
|
BankApp.components = BankApp.components || {};
|
|
BankApp.components.App = function App() {
|
|
const mode = store.getMode();
|
|
|
|
return h(
|
|
"div",
|
|
{ className: mode === "atm" ? "bank-shell is-atm" : "bank-shell" },
|
|
mode === "atm"
|
|
? null
|
|
: WindowTitleBar({
|
|
kicker: "FORGE Finance",
|
|
title: "Global Banking Network",
|
|
onClose: () => actions.closeBank(),
|
|
closeLabel: "Close banking interface",
|
|
}),
|
|
h("div", { id: "bank-notice-root" }),
|
|
mode === "atm"
|
|
? h("div", { id: "bank-atm-root" })
|
|
: [
|
|
h(
|
|
"div",
|
|
{
|
|
className: "bank-scroll-shell",
|
|
"data-preserve-scroll-id": "bank-page-scroll",
|
|
},
|
|
[
|
|
h(
|
|
"div",
|
|
{ className: "bank-layout" },
|
|
h("div", { id: "bank-sidebar-root" }),
|
|
h(
|
|
"main",
|
|
{ className: "bank-main" },
|
|
h(
|
|
"div",
|
|
{ className: "bank-page" },
|
|
h("div", {
|
|
id: "bank-page-header-root",
|
|
}),
|
|
h(
|
|
"p",
|
|
{ className: "bank-page-copy" },
|
|
"Manage deposits, withdrawals, transfers, and earnings sweeps from the same shared financial console.",
|
|
),
|
|
h("div", {
|
|
className: "bank-page-divider",
|
|
}),
|
|
h(
|
|
"div",
|
|
{ className: "bank-page-body" },
|
|
h("div", {
|
|
id: "bank-summary-section-root",
|
|
}),
|
|
h("div", {
|
|
id: "bank-action-sections-root",
|
|
}),
|
|
h("div", {
|
|
id: "bank-support-section-root",
|
|
}),
|
|
h("div", {
|
|
id: "bank-history-section-root",
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
h("div", { id: "bank-footer-root" }),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
};
|
|
})();
|
|
|
|
(function () {
|
|
const ForgeWebUI = window.ForgeWebUI;
|
|
const BankApp = window.BankApp;
|
|
const islandDefinitions = [
|
|
{
|
|
id: "bank-notice-root",
|
|
preserveScroll: false,
|
|
render: () => BankApp.componentFns.NoticeLayer(),
|
|
},
|
|
{
|
|
id: "bank-sidebar-root",
|
|
preserveScroll: false,
|
|
render: () => BankApp.componentFns.BankSidebar(),
|
|
},
|
|
{
|
|
id: "bank-page-header-root",
|
|
preserveScroll: false,
|
|
render: () => BankApp.componentFns.BankPageHeader(),
|
|
},
|
|
{
|
|
id: "bank-summary-section-root",
|
|
preserveScroll: false,
|
|
render: () => BankApp.componentFns.BankSummarySection(),
|
|
},
|
|
{
|
|
id: "bank-action-sections-root",
|
|
preserveScroll: false,
|
|
render: () => BankApp.componentFns.BankActionSections(),
|
|
},
|
|
{
|
|
id: "bank-support-section-root",
|
|
preserveScroll: false,
|
|
render: () => BankApp.componentFns.BankSupportSection(),
|
|
},
|
|
{
|
|
id: "bank-history-section-root",
|
|
preserveScroll: false,
|
|
render: () => BankApp.componentFns.BankHistorySection(),
|
|
},
|
|
{
|
|
id: "bank-atm-root",
|
|
preserveScroll: false,
|
|
render: () => BankApp.componentFns.ATMView(),
|
|
},
|
|
{
|
|
id: "bank-footer-root",
|
|
preserveScroll: false,
|
|
render: () => BankApp.componentFns.BankFooter(),
|
|
},
|
|
];
|
|
|
|
function createIslandManager() {
|
|
const mounts = new Map();
|
|
|
|
function sync() {
|
|
islandDefinitions.forEach((definition) => {
|
|
const container = document.getElementById(definition.id);
|
|
const current = mounts.get(definition.id);
|
|
|
|
if (!container) {
|
|
if (current) {
|
|
current.handle.dispose();
|
|
mounts.delete(definition.id);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (current && current.container === container) {
|
|
return;
|
|
}
|
|
|
|
if (current) {
|
|
current.handle.dispose();
|
|
}
|
|
|
|
const handle = ForgeWebUI.mount(container, definition.render, {
|
|
preserveScroll: definition.preserveScroll,
|
|
});
|
|
mounts.set(definition.id, {
|
|
container,
|
|
handle,
|
|
});
|
|
});
|
|
}
|
|
|
|
return {
|
|
sync,
|
|
};
|
|
}
|
|
|
|
const app = ForgeWebUI.createApp({
|
|
name: "bank",
|
|
root: "#app",
|
|
setup({ root }) {
|
|
const islandManager = createIslandManager();
|
|
|
|
ForgeWebUI.mount(root, () => BankApp.components.App(), {
|
|
preserveScroll: false,
|
|
});
|
|
|
|
if (BankApp.bridge) {
|
|
BankApp.bridge.notifyReady();
|
|
}
|
|
|
|
ForgeWebUI.effect(() => {
|
|
BankApp.store.getMode();
|
|
|
|
requestAnimationFrame(() => {
|
|
islandManager.sync();
|
|
});
|
|
});
|
|
},
|
|
});
|
|
|
|
app.start();
|
|
})();
|