## Summary This finishes the org credit line workflow so it behaves like reserved treasury-backed credit instead of a simple member allowance. ## What changed - reserve org funds immediately when a credit line is assigned - track credit lines with: - approved amount - available amount - outstanding principal - interest rate - amount due - consume reserved credit during store checkout without charging org funds a second time - add credit line repayment through the bank app - sync richer credit line state into org and bank payloads/UI - keep legacy `amount` compatibility mapped to available credit for older consumers ## User-facing behavior - assigning a credit line now reduces available org funds immediately - spending on `credit_line` reduces available credit and creates debt with interest - the bank app now shows outstanding credit debt and allows repayment from personal bank funds - the org treasury view now shows reserved credit and outstanding due totals ## Validation - `cargo fmt` - `npm run build:webui` - `cargo test -p forge-services --quiet` - `cargo test -p forge-server --quiet` ## Follow-up checks - validate in-game that assigning a credit line reduces org funds immediately - validate store checkout with `credit_line` updates available credit and debt correctly - validate bank repayment decreases player bank balance, increases org funds, and reduces amount due Co-authored-by: Jacob Schmidt <innovativestudios@outlook.com> Reviewed-on: #2
75 lines
2.2 KiB
JavaScript
75 lines
2.2 KiB
JavaScript
/**
|
|
* Shared JavaScript for Map UI
|
|
* Provides common utilities and state management across all UI components
|
|
*/
|
|
|
|
window.mapUIState = {
|
|
layersPanelVisible: true,
|
|
sidePanelElement: null,
|
|
};
|
|
|
|
window.mapUI = {
|
|
formatGridCoordinate(value) {
|
|
return Math.round(Number(value) || 0)
|
|
.toString()
|
|
.padStart(4, "0");
|
|
},
|
|
formatPosition(position) {
|
|
const safePosition = Array.isArray(position) ? position : [0, 0, 0];
|
|
return `X: ${this.formatGridCoordinate(safePosition[0])} Y: ${this.formatGridCoordinate(safePosition[1])}`;
|
|
},
|
|
sendEvent(event, data) {
|
|
A3API.SendAlert(JSON.stringify({ event: event, data: data }));
|
|
},
|
|
updateCoordinates(x, y) {
|
|
const coordDisplay = document.getElementById("coordsDisplay");
|
|
if (coordDisplay) {
|
|
coordDisplay.textContent = this.formatPosition([x, y, 0]);
|
|
}
|
|
},
|
|
updateScale(scale) {
|
|
const scaleDisplay = document.getElementById("scaleDisplay");
|
|
if (scaleDisplay) {
|
|
scaleDisplay.textContent = `Scale: 1:${Math.round(scale)}`;
|
|
}
|
|
},
|
|
updateStatus(text) {
|
|
const statusText = document.getElementById("statusText");
|
|
if (statusText) {
|
|
statusText.textContent = text;
|
|
}
|
|
},
|
|
};
|
|
|
|
window.updateCoordinates = window.mapUI.updateCoordinates;
|
|
window.updateScale = window.mapUI.updateScale;
|
|
window.updateStatus = window.mapUI.updateStatus;
|
|
|
|
window.ForgeBridge = window.ForgeBridge || {
|
|
_handlers: {},
|
|
on(event, handler) {
|
|
this._handlers[event] = this._handlers[event] || [];
|
|
this._handlers[event].push(handler);
|
|
},
|
|
ready(payload) {
|
|
window.mapUI.sendEvent("cad::ready", payload || {});
|
|
return true;
|
|
},
|
|
receive(payload) {
|
|
if (!payload || typeof payload !== "object") {
|
|
return;
|
|
}
|
|
|
|
const handlers = this._handlers[payload.event] || [];
|
|
handlers.forEach((handler) => handler(payload.data || {}));
|
|
},
|
|
send(event, data) {
|
|
window.mapUI.sendEvent(event, data || {});
|
|
return true;
|
|
},
|
|
close(data) {
|
|
window.mapUI.sendEvent("map::close", data || {});
|
|
return true;
|
|
},
|
|
};
|