Add org credit lines and multi-source store checkout state
- Wire org portal credit-line requests/responses through SQF bridge and UI events - Sync `creditLines` in org payloads and refresh portal state after org sync - Add store payment sources (cash, bank, org funds, credit line) and expose selection in cart UI - Scaffold server-side store addon initialization/config files
This commit is contained in:
parent
09ab290b5a
commit
9771e375b6
@ -11,12 +11,14 @@ if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initOrgUIBridge); };
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(OrgClass) call ["sync", [_data, true]];
|
||||
GVAR(OrgUIBridge) call ["refreshPortal", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncOrg), {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
|
||||
GVAR(OrgClass) call ["sync", [_data, _jip]];
|
||||
GVAR(OrgUIBridge) call ["refreshPortal", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseCreateOrg), {
|
||||
@ -37,6 +39,12 @@ if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initOrgUIBridge); };
|
||||
GVAR(OrgUIBridge) call ["handleLeaveResponse", [_payload]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseCreditLine), {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(OrgUIBridge) call ["handleCreditLineResponse", [_payload]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[{
|
||||
EGVAR(actor,ActorClass) get "isLoaded";
|
||||
}, {
|
||||
|
||||
@ -38,6 +38,9 @@ switch (_event) do {
|
||||
case "org::leave::request": {
|
||||
GVAR(OrgUIBridge) call ["requestLeave", []];
|
||||
};
|
||||
case "org::credit::request": {
|
||||
GVAR(OrgUIBridge) call ["requestCreditLine", [_data]];
|
||||
};
|
||||
case "org::ready": {
|
||||
GVAR(OrgUIBridge) call ["handleReady", [_control]];
|
||||
};
|
||||
|
||||
@ -35,6 +35,7 @@ GVAR(OrgBaseClass) = compileFinal createHashMapFromArray [
|
||||
_org set ["name", ""];
|
||||
_org set ["funds", 0];
|
||||
_org set ["reputation", 0];
|
||||
_org set ["credit_lines", createHashMap];
|
||||
_org set ["assets", createHashMap];
|
||||
_org set ["fleet", createHashMap];
|
||||
_org set ["members", createHashMap];
|
||||
@ -78,6 +79,7 @@ GVAR(OrgBaseClass) = compileFinal createHashMapFromArray [
|
||||
private _ownerUid = _orgData get "owner";
|
||||
private _funds = _orgData get "funds";
|
||||
private _reputation = _orgData get "reputation";
|
||||
private _creditLinesRaw = _orgData getOrDefault ["credit_lines", createHashMap];
|
||||
private _assetsRaw = _orgData get "assets";
|
||||
private _fleetRaw = _orgData get "fleet";
|
||||
private _membersRaw = _orgData get "members";
|
||||
@ -132,6 +134,16 @@ GVAR(OrgBaseClass) = compileFinal createHashMapFromArray [
|
||||
]);
|
||||
} forEach _fleetRaw;
|
||||
|
||||
private _creditLinesList = [];
|
||||
{
|
||||
private _creditLineData = _y;
|
||||
_creditLinesList pushBack (createHashMapFromArray [
|
||||
["uid", _creditLineData getOrDefault ["uid", _x]],
|
||||
["member", _creditLineData getOrDefault ["name", "Unknown Member"]],
|
||||
["amount", _creditLineData getOrDefault ["amount", 0]]
|
||||
]);
|
||||
} forEach _creditLinesRaw;
|
||||
|
||||
createHashMapFromArray [
|
||||
["session", createHashMapFromArray [
|
||||
["actorName", _playerName],
|
||||
@ -149,6 +161,7 @@ GVAR(OrgBaseClass) = compileFinal createHashMapFromArray [
|
||||
]],
|
||||
["funds", _funds],
|
||||
["reputation", _reputation],
|
||||
["creditLines", _creditLinesList],
|
||||
["members", _membersList],
|
||||
["fleet", _fleetList],
|
||||
["assets", _assetsList],
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* File: fnc_initOrgUIBridge.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-10
|
||||
* Last Update: 2026-03-10
|
||||
* Last Update: 2026-03-12
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
@ -150,12 +150,37 @@ GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
_self call ["sendBridgeEvent", [_eventName, _payload]];
|
||||
}],
|
||||
["handleCreditLineResponse", compileFinal {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
private _eventName = [
|
||||
"org::credit::failure",
|
||||
"org::credit::success"
|
||||
] select (_payload getOrDefault ["success", false]);
|
||||
|
||||
_self call ["sendBridgeEvent", [_eventName, _payload]];
|
||||
}],
|
||||
["requestDisband", compileFinal {
|
||||
[SRPC(org,requestDisbandOrg), [getPlayerUID player]] call CFUNC(serverEvent);
|
||||
}],
|
||||
["requestLeave", compileFinal {
|
||||
[SRPC(org,requestLeaveOrg), [getPlayerUID player]] call CFUNC(serverEvent);
|
||||
}],
|
||||
["requestCreditLine", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _memberUid = _data getOrDefault ["memberUid", ""];
|
||||
private _memberName = _data getOrDefault ["memberName", ""];
|
||||
private _amount = _data getOrDefault ["amount", 0];
|
||||
|
||||
[SRPC(org,requestAssignCreditLine), [getPlayerUID player, _memberUid, _memberName, _amount]] call CFUNC(serverEvent);
|
||||
}],
|
||||
["refreshPortal", compileFinal {
|
||||
private _control = _self call ["getActiveBrowserControl", []];
|
||||
if (isNull _control) exitWith { false };
|
||||
|
||||
_self call ["sendBridgeEvent", ["org::sync", GVAR(OrgClass) call ["buildPortalPayload", []], _control]]
|
||||
}],
|
||||
["handleReady", compileFinal {
|
||||
params [["_control", controlNull, [controlNull]]];
|
||||
|
||||
|
||||
@ -71,6 +71,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
function requestCreditLine(payload) {
|
||||
const sent = sendEvent("org::credit::request", payload);
|
||||
if (sent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const OrgPortal = window.OrgPortal;
|
||||
if (OrgPortal && OrgPortal.actions) {
|
||||
OrgPortal.actions.showTreasuryNotice(
|
||||
"error",
|
||||
"Arma credit line bridge is unavailable.",
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function receive(eventOrPayload, data = {}) {
|
||||
const event =
|
||||
typeof eventOrPayload === "object" && eventOrPayload !== null
|
||||
@ -103,7 +120,38 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === "org::sync") {
|
||||
if (store && typeof store.hydratePortal === "function") {
|
||||
store.hydratePortal(payloadData);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const OrgPortal = window.OrgPortal;
|
||||
if (event === "org::credit::success") {
|
||||
if (OrgPortal && OrgPortal.store) {
|
||||
OrgPortal.store.setModal(null);
|
||||
}
|
||||
|
||||
if (OrgPortal && OrgPortal.actions) {
|
||||
OrgPortal.actions.showTreasuryNotice(
|
||||
"success",
|
||||
payloadData.message || "Credit line assigned.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === "org::credit::failure") {
|
||||
if (OrgPortal && OrgPortal.actions) {
|
||||
OrgPortal.actions.showTreasuryNotice(
|
||||
"error",
|
||||
payloadData.message || "Unable to assign credit line.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === "org::disband::success") {
|
||||
if (OrgPortal && OrgPortal.store) {
|
||||
OrgPortal.store.setModal(null);
|
||||
@ -170,6 +218,7 @@
|
||||
requestCreateOrg,
|
||||
requestDisbandOrg,
|
||||
requestLeaveOrg,
|
||||
requestCreditLine,
|
||||
receive,
|
||||
sendEvent,
|
||||
};
|
||||
@ -179,6 +228,7 @@
|
||||
requestCreateOrg,
|
||||
requestDisbandOrg,
|
||||
requestLeaveOrg,
|
||||
requestCreditLine,
|
||||
receive,
|
||||
receiveLoginSuccess: (data) => receive("org::login::success", data),
|
||||
receiveLoginFailure: (data) => receive("org::login::failure", data),
|
||||
|
||||
@ -156,7 +156,7 @@
|
||||
"select",
|
||||
{ id: "treasury-credit-member", ...memberSelectProps },
|
||||
...members.map((member) =>
|
||||
h("option", { value: member.name }, member.name),
|
||||
h("option", { value: member.uid }, member.name),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -103,7 +103,11 @@
|
||||
),
|
||||
);
|
||||
store.setCreditLines((currentLines) =>
|
||||
currentLines.filter((line) => line.member !== memberName),
|
||||
currentLines.filter((line) =>
|
||||
memberUid
|
||||
? line.uid !== memberUid
|
||||
: line.member !== memberName,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@ -240,7 +244,7 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
grantCreditLine(memberName, amount) {
|
||||
grantCreditLine(memberUid, amount) {
|
||||
if (!getters.canManageTreasury()) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
@ -249,7 +253,7 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!memberName) {
|
||||
if (!memberUid) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Select a member for the credit line.",
|
||||
@ -265,30 +269,41 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setCreditLines((currentLines) => {
|
||||
const existingIndex = currentLines.findIndex(
|
||||
(line) => line.member === memberName,
|
||||
const member = store
|
||||
.getMembers()
|
||||
.find(
|
||||
(entry) =>
|
||||
getters.getMemberUid(entry) === memberUid,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
return [
|
||||
...currentLines,
|
||||
{ member: memberName, amount },
|
||||
];
|
||||
const memberName = member
|
||||
? getters.getMemberName(member)
|
||||
: "";
|
||||
|
||||
if (!memberName) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Selected member was not found in the organization roster.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const updatedLines = [...currentLines];
|
||||
updatedLines[existingIndex] = {
|
||||
member: memberName,
|
||||
amount,
|
||||
};
|
||||
return updatedLines;
|
||||
});
|
||||
const bridge = window.RegistryApp
|
||||
? window.RegistryApp.bridge
|
||||
: null;
|
||||
|
||||
if (!bridge || typeof bridge.requestCreditLine !== "function") {
|
||||
this.showTreasuryNotice(
|
||||
"success",
|
||||
`Credit line of ${getters.formatCurrency(amount)} assigned to ${memberName}.`,
|
||||
"error",
|
||||
"Credit line bridge is unavailable.",
|
||||
);
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return bridge.requestCreditLine({
|
||||
memberUid,
|
||||
memberName,
|
||||
amount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,9 @@
|
||||
[this.getMembers, this.setMembers] = createSignal([
|
||||
...portalData.members,
|
||||
]);
|
||||
[this.getCreditLines, this.setCreditLines] = createSignal([]);
|
||||
[this.getCreditLines, this.setCreditLines] = createSignal([
|
||||
...portalData.creditLines,
|
||||
]);
|
||||
[this.getTreasuryNotice, this.setTreasuryNotice] = createSignal(
|
||||
{
|
||||
type: "",
|
||||
@ -26,7 +28,7 @@
|
||||
hydrateFromPayload(payload) {
|
||||
this.setFunds(payload.portalData.funds || 0);
|
||||
this.setMembers([...(payload.portalData.members || [])]);
|
||||
this.setCreditLines([]);
|
||||
this.setCreditLines([...(payload.portalData.creditLines || [])]);
|
||||
this.setTreasuryNotice({ type: "", text: "" });
|
||||
this.setModal(null);
|
||||
this.setOrgDisbanded(false);
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
),
|
||||
funds: 0,
|
||||
reputation: 0,
|
||||
creditLines: [],
|
||||
members: [],
|
||||
fleet: [],
|
||||
assets: [],
|
||||
@ -77,6 +78,10 @@
|
||||
);
|
||||
this.portalData.funds = payload.portalData.funds || 0;
|
||||
this.portalData.reputation = payload.portalData.reputation || 0;
|
||||
replaceArray(
|
||||
this.portalData.creditLines,
|
||||
payload.portalData.creditLines || [],
|
||||
);
|
||||
|
||||
replaceArray(
|
||||
this.portalData.members,
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* File: fnc_initStoreClass.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-01-28
|
||||
* Last Update: 2026-03-11
|
||||
* Last Update: 2026-03-12
|
||||
* Public: Yes
|
||||
*
|
||||
* Description:
|
||||
@ -28,6 +28,7 @@ GVAR(StoreBaseClass) = compileFinal createHashMapFromArray [
|
||||
_self set ["store", createHashMap];
|
||||
_self set ["workspace", createHashMapFromArray [
|
||||
["budget", 48000],
|
||||
["creditLine", 0],
|
||||
["availability", "Open"],
|
||||
["approval", "Field Access"],
|
||||
["moduleState", "Preview"],
|
||||
@ -41,19 +42,106 @@ GVAR(StoreBaseClass) = compileFinal createHashMapFromArray [
|
||||
}],
|
||||
["buildUIPayload", compileFinal {
|
||||
private _workspace = _self getOrDefault ["workspace", createHashMap];
|
||||
private _budget = _workspace getOrDefault ["budget", 48000];
|
||||
private _creditLine = _workspace getOrDefault ["creditLine", 0];
|
||||
private _cashBalance = 0;
|
||||
private _bankBalance = 0;
|
||||
private _orgFunds = 0;
|
||||
private _orgId = "";
|
||||
private _orgName = "";
|
||||
private _orgOwnerUid = "";
|
||||
private _orgCreditLines = createHashMap;
|
||||
private _playerUid = getPlayerUID player;
|
||||
private _playerVar = toLowerANSI (vehicleVarName player);
|
||||
private _isOrgLeader = false;
|
||||
private _isDefaultOrg = false;
|
||||
private _isDefaultOrgCeo = false;
|
||||
|
||||
if !(isNil QEGVAR(bank,BankClass)) then {
|
||||
_cashBalance = EGVAR(bank,BankClass) call ["get", ["cash", 0]];
|
||||
_bankBalance = EGVAR(bank,BankClass) call ["get", ["bank", 0]];
|
||||
};
|
||||
|
||||
if !(isNil QEGVAR(org,OrgClass)) then {
|
||||
_orgId = EGVAR(org,OrgClass) call ["get", ["id", ""]];
|
||||
_orgName = EGVAR(org,OrgClass) call ["get", ["name", ""]];
|
||||
_orgOwnerUid = EGVAR(org,OrgClass) call ["get", ["owner", ""]];
|
||||
_orgFunds = EGVAR(org,OrgClass) call ["get", ["funds", 0]];
|
||||
_orgCreditLines = EGVAR(org,OrgClass) call ["get", ["credit_lines", createHashMap]];
|
||||
_isDefaultOrg = (_orgId isEqualTo "default") || { toLowerANSI _orgOwnerUid isEqualTo "server" };
|
||||
_isOrgLeader = _orgOwnerUid isEqualTo _playerUid;
|
||||
_isDefaultOrgCeo = _isDefaultOrg && { _playerVar isEqualTo "ceo" };
|
||||
};
|
||||
|
||||
if (_orgCreditLines isEqualType createHashMap) then {
|
||||
private _playerCreditLine = _orgCreditLines getOrDefault [_playerUid, createHashMap];
|
||||
if (_playerCreditLine isEqualType createHashMap) then {
|
||||
_creditLine = _playerCreditLine getOrDefault ["amount", _creditLine];
|
||||
};
|
||||
};
|
||||
|
||||
private _canUseOrgFunds = _isOrgLeader || _isDefaultOrgCeo;
|
||||
private _orgFundsEnabled = _canUseOrgFunds && {_orgFunds > 0};
|
||||
private _paymentSources = [
|
||||
createHashMapFromArray [
|
||||
["id", "cash"],
|
||||
["label", "Cash"],
|
||||
["balance", _cashBalance],
|
||||
["enabled", _cashBalance > 0],
|
||||
["detail", "Use on-hand cash carried by the player."]
|
||||
],
|
||||
createHashMapFromArray [
|
||||
["id", "bank"],
|
||||
["label", "Bank"],
|
||||
["balance", _bankBalance],
|
||||
["enabled", _bankBalance > 0],
|
||||
["detail", "Charge the player bank account."]
|
||||
],
|
||||
createHashMapFromArray [
|
||||
["id", "org_funds"],
|
||||
["label", "Org Funds"],
|
||||
["balance", _orgFunds],
|
||||
["enabled", _orgFundsEnabled],
|
||||
["detail", [
|
||||
"Only organization leaders or the default-org CEO can use treasury funds.",
|
||||
[
|
||||
"Charge organization treasury funds.",
|
||||
"No organization funds are currently available."
|
||||
] select _orgFundsEnabled
|
||||
] select _canUseOrgFunds]
|
||||
],
|
||||
createHashMapFromArray [
|
||||
["id", "credit_line"],
|
||||
["label", "Credit Line"],
|
||||
["balance", _creditLine],
|
||||
["enabled", _creditLine > 0],
|
||||
["detail", [
|
||||
"No approved credit line is assigned to this member.",
|
||||
"Use the approved procurement credit line."
|
||||
] select (_creditLine > 0)]
|
||||
]
|
||||
];
|
||||
|
||||
createHashMapFromArray [
|
||||
["session", createHashMapFromArray [
|
||||
["actorName", name player],
|
||||
["actorUid", getPlayerUID player],
|
||||
["approvalRole", _workspace getOrDefault ["approval", "Field Access"]]
|
||||
["approvalRole", _workspace getOrDefault ["approval", "Field Access"]],
|
||||
["orgId", _orgId],
|
||||
["orgName", _orgName],
|
||||
["orgLeader", _isOrgLeader],
|
||||
["defaultOrgCeo", _isDefaultOrgCeo],
|
||||
["canUseOrgFunds", _canUseOrgFunds]
|
||||
]],
|
||||
["workspace", createHashMapFromArray [
|
||||
["budget", _workspace getOrDefault ["budget", 48000]],
|
||||
["budget", _budget],
|
||||
["creditLine", _creditLine],
|
||||
["availability", _workspace getOrDefault ["availability", "Open"]],
|
||||
["approval", _workspace getOrDefault ["approval", "Field Access"]],
|
||||
["moduleState", _workspace getOrDefault ["moduleState", "Preview"]],
|
||||
["searchTags", _workspace getOrDefault ["searchTags", ["Field", "Logistics", "Issued", "Restricted"]]]
|
||||
["searchTags", _workspace getOrDefault ["searchTags", ["Field", "Logistics", "Issued", "Restricted"]]],
|
||||
["paymentSources", _paymentSources],
|
||||
["defaultPaymentSource", "cash"]
|
||||
]],
|
||||
["cartItems", []]
|
||||
]
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* File: fnc_initStoreUIBridge.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-10
|
||||
* Last Update: 2026-03-11
|
||||
* Last Update: 2026-03-12
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
@ -302,7 +302,8 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _items = _data getOrDefault ["items", []];
|
||||
private _message = format ["Checkout integration is not wired yet. Received %1 queued line(s).", count _items];
|
||||
private _paymentMethod = _data getOrDefault ["paymentMethod", "cash"];
|
||||
private _message = format ["Checkout integration is not wired yet. Received %1 queued line(s) using %2.", count _items, _paymentMethod];
|
||||
|
||||
diag_log format ["[FORGE:Client:Store] Checkout request received: %1", _data];
|
||||
_self call ["sendBridgeEvent", ["store::checkout::failure", createHashMapFromArray [["message", _message]]]];
|
||||
|
||||
@ -491,6 +491,11 @@ ${scopeSelector} .store-toast.is-error {
|
||||
getters.getSelectionKey(state) || "Catalog",
|
||||
)
|
||||
: actions.formatTitle(state.view);
|
||||
const selectedPaymentSource =
|
||||
getters.getPaymentSourceById(
|
||||
storeConfig,
|
||||
state.selectedPaymentSource,
|
||||
) || null;
|
||||
|
||||
ensureScopedStyle("storefront-app-shell", appShellCss);
|
||||
|
||||
@ -687,6 +692,27 @@ ${scopeSelector} .store-toast.is-error {
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "filter-group" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "filter-label" },
|
||||
"Payment",
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "filter-value" },
|
||||
h("span", null, "Checkout Source"),
|
||||
h(
|
||||
"span",
|
||||
{ className: "filter-placeholder" },
|
||||
selectedPaymentSource
|
||||
? selectedPaymentSource.label
|
||||
: "Cash",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -55,8 +55,39 @@ ${scopeSelector} .cart-header {
|
||||
}
|
||||
|
||||
${scopeSelector} .cart-close {
|
||||
min-width: 2rem;
|
||||
height: 2rem;
|
||||
min-width: 2.1rem;
|
||||
height: 2.1rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border-radius: 0.6rem;
|
||||
border: 1px solid rgb(173 48 48 / 0.9);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(214 92 92) 0%,
|
||||
rgb(175 52 52) 100%
|
||||
);
|
||||
color: #fff;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgb(255 255 255 / 0.26),
|
||||
0 8px 18px rgb(138 61 61 / 0.28);
|
||||
}
|
||||
|
||||
${scopeSelector} .cart-close:hover {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(226 107 107) 0%,
|
||||
rgb(187 61 61) 100%
|
||||
);
|
||||
border-color: rgb(173 48 48);
|
||||
}
|
||||
|
||||
${scopeSelector} .cart-close:focus-visible {
|
||||
outline: 2px solid rgb(191 80 80 / 0.35);
|
||||
}
|
||||
|
||||
${scopeSelector} .cart-status,
|
||||
@ -175,6 +206,60 @@ ${scopeSelector} .cart-summary {
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .payment-source-field {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .payment-source-select {
|
||||
width: 100%;
|
||||
min-height: 2.9rem;
|
||||
padding: 0 0.95rem;
|
||||
border-radius: 0.8rem;
|
||||
border: 1px solid var(--store-border);
|
||||
background: rgb(255 255 255 / 0.78);
|
||||
color: var(--store-text-main);
|
||||
}
|
||||
|
||||
${scopeSelector} .payment-source-meta,
|
||||
${scopeSelector} .payment-source-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .payment-source-meta {
|
||||
padding: 0.85rem 0.9rem;
|
||||
border-radius: 0.95rem;
|
||||
border: 1px solid var(--store-border);
|
||||
background: rgb(255 255 255 / 0.44);
|
||||
}
|
||||
|
||||
${scopeSelector} .payment-source-detail {
|
||||
margin: 0.2rem 0 0;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.4;
|
||||
color: var(--store-text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .payment-source-label {
|
||||
font-weight: 700;
|
||||
color: var(--store-text-main);
|
||||
}
|
||||
|
||||
${scopeSelector} .payment-source-balance {
|
||||
font-weight: 700;
|
||||
color: var(--store-success);
|
||||
}
|
||||
|
||||
${scopeSelector} .payment-source-state {
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--store-text-subtle);
|
||||
}
|
||||
|
||||
${scopeSelector} .summary-row.total {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
@ -217,9 +302,24 @@ ${scopeSelector} .cart-empty {
|
||||
StorefrontApp.componentFns.Cart = function Cart() {
|
||||
const state = getters.getStoreState(store);
|
||||
const summary = getters.summarizeCart(state.cartItems);
|
||||
const remainingBudget = Math.max(
|
||||
const paymentSources = getters.getPaymentSources(storeConfig);
|
||||
const selectedPaymentSource =
|
||||
getters.getPaymentSourceById(
|
||||
storeConfig,
|
||||
state.selectedPaymentSource,
|
||||
) || paymentSources[0] || null;
|
||||
const availablePaymentSourceCount = paymentSources.filter(
|
||||
(source) => source.enabled !== false,
|
||||
).length;
|
||||
const selectedPaymentLabel = selectedPaymentSource
|
||||
? selectedPaymentSource.label
|
||||
: "Unavailable";
|
||||
const selectedPaymentBalance = selectedPaymentSource
|
||||
? Number(selectedPaymentSource.balance || 0)
|
||||
: 0;
|
||||
const remainingSourceBalance = Math.max(
|
||||
0,
|
||||
Number(storeConfig.budget || 0) - summary.total,
|
||||
selectedPaymentBalance - summary.total,
|
||||
);
|
||||
|
||||
ensureScopedStyle("storefront-cart", cartCss);
|
||||
@ -254,8 +354,7 @@ ${scopeSelector} .cart-empty {
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className:
|
||||
"window-control-btn cart-close is-close",
|
||||
className: "cart-close",
|
||||
"aria-label": "Close cart",
|
||||
title: "Close cart",
|
||||
onClick: () => actions.closeCart(),
|
||||
@ -263,25 +362,13 @@ ${scopeSelector} .cart-empty {
|
||||
"X",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "cart-status" },
|
||||
h("span", { className: "eyebrow" }, "Status"),
|
||||
h(
|
||||
"p",
|
||||
{ className: "section-copy" },
|
||||
state.isCheckingOut
|
||||
? "Checkout request sent through the browser bridge."
|
||||
: "Local cart state is active. Checkout is routed through the same bridge contract used by the org UI.",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "cart-kpi" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "cart-kpi-card" },
|
||||
h("span", { className: "kpi-label" }, "Lines"),
|
||||
h("span", { className: "kpi-label" }, "Items"),
|
||||
h(
|
||||
"span",
|
||||
{ className: "kpi-value" },
|
||||
@ -291,14 +378,114 @@ ${scopeSelector} .cart-empty {
|
||||
h(
|
||||
"div",
|
||||
{ className: "cart-kpi-card" },
|
||||
h("span", { className: "kpi-label" }, "Budget"),
|
||||
h(
|
||||
"span",
|
||||
{ className: "kpi-label" },
|
||||
"Payment",
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "kpi-value" },
|
||||
getters.formatCurrency(storeConfig.budget),
|
||||
selectedPaymentLabel,
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "cart-status" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "eyebrow" },
|
||||
"Payment Source",
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "payment-source-field" },
|
||||
h(
|
||||
"select",
|
||||
{
|
||||
className: "payment-source-select",
|
||||
value: state.selectedPaymentSource,
|
||||
onChange: (event) =>
|
||||
actions.selectPaymentSource(
|
||||
event.target.value,
|
||||
),
|
||||
},
|
||||
paymentSources.map((source) =>
|
||||
h(
|
||||
"option",
|
||||
{
|
||||
value: source.id,
|
||||
disabled:
|
||||
source.enabled === false,
|
||||
},
|
||||
source.enabled === false
|
||||
? `${source.label} (Locked)`
|
||||
: source.label,
|
||||
),
|
||||
),
|
||||
),
|
||||
selectedPaymentSource
|
||||
? h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
"payment-source-meta",
|
||||
},
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
"payment-source-row",
|
||||
},
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
className:
|
||||
"payment-source-label",
|
||||
},
|
||||
selectedPaymentSource.label,
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
className:
|
||||
"payment-source-balance",
|
||||
},
|
||||
getters.formatCurrency(
|
||||
selectedPaymentSource.balance,
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"p",
|
||||
{
|
||||
className:
|
||||
"payment-source-detail",
|
||||
},
|
||||
selectedPaymentSource.detail,
|
||||
),
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
className:
|
||||
"payment-source-state",
|
||||
},
|
||||
availablePaymentSourceCount > 0
|
||||
? selectedPaymentSource.enabled ===
|
||||
false
|
||||
? "Locked"
|
||||
: "Available"
|
||||
: "Unavailable",
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
@ -436,12 +623,14 @@ ${scopeSelector} .cart-empty {
|
||||
h(
|
||||
"span",
|
||||
{ className: "summary-label" },
|
||||
"Remaining Budget",
|
||||
"Remaining Source",
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "summary-value" },
|
||||
getters.formatCurrency(remainingBudget),
|
||||
getters.formatCurrency(
|
||||
remainingSourceBalance,
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
|
||||
@ -5,14 +5,51 @@
|
||||
actorName: "",
|
||||
actorUid: "",
|
||||
approvalRole: "Field Access",
|
||||
orgId: "",
|
||||
orgName: "",
|
||||
orgLeader: false,
|
||||
defaultOrgCeo: false,
|
||||
canUseOrgFunds: false,
|
||||
};
|
||||
|
||||
const defaultStoreConfig = {
|
||||
budget: 48000,
|
||||
creditLine: 0,
|
||||
availability: "Open",
|
||||
approval: "Field Access",
|
||||
moduleState: "Preview",
|
||||
searchTags: ["Field", "Logistics", "Issued", "Restricted"],
|
||||
paymentSources: [
|
||||
{
|
||||
id: "cash",
|
||||
label: "Cash",
|
||||
balance: 0,
|
||||
enabled: false,
|
||||
detail: "Use on-hand cash carried by the player.",
|
||||
},
|
||||
{
|
||||
id: "bank",
|
||||
label: "Bank",
|
||||
balance: 0,
|
||||
enabled: false,
|
||||
detail: "Charge the player bank account.",
|
||||
},
|
||||
{
|
||||
id: "org_funds",
|
||||
label: "Org Funds",
|
||||
balance: 0,
|
||||
enabled: false,
|
||||
detail: "Only organization leaders or the default-org CEO can use treasury funds.",
|
||||
},
|
||||
{
|
||||
id: "credit_line",
|
||||
label: "Credit Line",
|
||||
balance: 0,
|
||||
enabled: false,
|
||||
detail: "No approved credit line is assigned to this member.",
|
||||
},
|
||||
],
|
||||
defaultPaymentSource: "cash",
|
||||
};
|
||||
|
||||
function cloneValue(value) {
|
||||
|
||||
@ -165,7 +165,6 @@
|
||||
return nextItems;
|
||||
});
|
||||
|
||||
store.setCartOpen(true);
|
||||
showNotice("success", `${item.name} added to the acquisition queue.`);
|
||||
}
|
||||
|
||||
@ -199,6 +198,31 @@
|
||||
);
|
||||
}
|
||||
|
||||
function selectPaymentSource(paymentSourceId) {
|
||||
const sourceId = String(paymentSourceId || "").trim();
|
||||
const paymentSources = getters.getPaymentSources(storeConfig);
|
||||
const selectedSource = paymentSources.find(
|
||||
(source) => source.id === sourceId,
|
||||
);
|
||||
|
||||
if (!selectedSource) {
|
||||
showNotice("error", "Selected payment source is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedSource.enabled === false) {
|
||||
showNotice(
|
||||
"error",
|
||||
selectedSource.detail ||
|
||||
"Selected payment source is not available.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setSelectedPaymentSource(sourceId);
|
||||
return true;
|
||||
}
|
||||
|
||||
function requestCheckout() {
|
||||
const cartItems = store.getCartItems();
|
||||
if (cartItems.length === 0) {
|
||||
@ -207,10 +231,29 @@
|
||||
}
|
||||
|
||||
const summary = getters.summarizeCart(cartItems);
|
||||
if (summary.total > Number(storeConfig.budget || 0)) {
|
||||
const selectedPaymentSource = getters.getPaymentSourceById(
|
||||
storeConfig,
|
||||
store.getSelectedPaymentSource(),
|
||||
);
|
||||
|
||||
if (!selectedPaymentSource) {
|
||||
showNotice("error", "Select a payment source before checkout.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedPaymentSource.enabled === false) {
|
||||
showNotice(
|
||||
"error",
|
||||
"Checkout total exceeds the current procurement budget.",
|
||||
selectedPaymentSource.detail ||
|
||||
"Selected payment source is unavailable.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (summary.total > Number(selectedPaymentSource.balance || 0)) {
|
||||
showNotice(
|
||||
"error",
|
||||
`${selectedPaymentSource.label} cannot cover this checkout total.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@ -226,6 +269,8 @@
|
||||
const sent = bridge.requestCheckout({
|
||||
actorUid: session.actorUid,
|
||||
actorName: session.actorName,
|
||||
paymentMethod: selectedPaymentSource.id,
|
||||
paymentLabel: selectedPaymentSource.label,
|
||||
items: cartItems,
|
||||
subtotal: summary.subtotal,
|
||||
total: summary.total,
|
||||
@ -257,6 +302,7 @@
|
||||
incrementCartItem,
|
||||
decrementCartItem,
|
||||
removeCartItem,
|
||||
selectPaymentSource,
|
||||
requestCheckout,
|
||||
formatTitle: getters.formatTitle,
|
||||
formatCurrency: getters.formatCurrency,
|
||||
|
||||
@ -29,6 +29,8 @@
|
||||
});
|
||||
[this.getIsCheckingOut, this.setIsCheckingOut] =
|
||||
createSignal(false);
|
||||
[this.getSelectedPaymentSource, this.setSelectedPaymentSource] =
|
||||
createSignal("cash");
|
||||
}
|
||||
|
||||
resetToCategories() {
|
||||
@ -163,6 +165,58 @@
|
||||
this.finishCategoryRequest(categoryKey);
|
||||
}
|
||||
|
||||
ensureSelectedPaymentSource(workspace) {
|
||||
const paymentSources = Array.isArray(workspace?.paymentSources)
|
||||
? workspace.paymentSources
|
||||
: [];
|
||||
const currentSource = String(
|
||||
this.getSelectedPaymentSource() || "",
|
||||
).trim();
|
||||
const defaultSource = String(
|
||||
workspace?.defaultPaymentSource || "",
|
||||
).trim();
|
||||
const sourceIds = paymentSources.map((source) =>
|
||||
String(source?.id || "").trim(),
|
||||
);
|
||||
const enabledSource = paymentSources.find(
|
||||
(source) => source && source.enabled !== false,
|
||||
);
|
||||
const defaultAvailable =
|
||||
defaultSource && sourceIds.includes(defaultSource)
|
||||
? paymentSources.find(
|
||||
(source) =>
|
||||
String(source?.id || "").trim() ===
|
||||
defaultSource,
|
||||
)
|
||||
: null;
|
||||
|
||||
if (
|
||||
currentSource &&
|
||||
sourceIds.includes(currentSource) &&
|
||||
paymentSources.some(
|
||||
(source) =>
|
||||
String(source?.id || "").trim() === currentSource &&
|
||||
source?.enabled !== false,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (defaultAvailable && defaultAvailable.enabled !== false) {
|
||||
this.setSelectedPaymentSource(defaultSource);
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabledSource) {
|
||||
this.setSelectedPaymentSource(
|
||||
String(enabledSource.id || "cash"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setSelectedPaymentSource(defaultSource || "cash");
|
||||
}
|
||||
|
||||
navigateToBreadcrumb(target) {
|
||||
switch (target) {
|
||||
case "categories":
|
||||
@ -198,6 +252,9 @@
|
||||
this.setCatalogRequestKey("");
|
||||
this.setIsCatalogLoading(false);
|
||||
this.setCatalogPage(1);
|
||||
this.ensureSelectedPaymentSource(
|
||||
payload?.workspace || payload?.storeConfig || {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
(function () {
|
||||
const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {});
|
||||
const CATALOG_PAGE_SIZE = 24;
|
||||
const CATALOG_PAGE_SIZE = 6;
|
||||
|
||||
function getSelectionKey(state) {
|
||||
return (
|
||||
@ -54,6 +54,7 @@
|
||||
selectedCategory: store.getSelectedCategory(),
|
||||
selectedWeaponSlot: store.getSelectedWeaponSlot(),
|
||||
selectedVehicleSlot: store.getSelectedVehicleSlot(),
|
||||
selectedPaymentSource: store.getSelectedPaymentSource(),
|
||||
cartOpen: store.getCartOpen(),
|
||||
searchQuery: store.getSearchQuery(),
|
||||
cartItems: store.getCartItems(),
|
||||
@ -241,6 +242,27 @@
|
||||
};
|
||||
}
|
||||
|
||||
function getPaymentSources(storeConfig) {
|
||||
const paymentSources = Array.isArray(storeConfig?.paymentSources)
|
||||
? storeConfig.paymentSources
|
||||
: [];
|
||||
|
||||
return paymentSources.map((source) => ({
|
||||
id: String(source?.id || "").trim(),
|
||||
label: String(source?.label || source?.id || "").trim(),
|
||||
balance: Number(source?.balance || 0),
|
||||
enabled: source?.enabled !== false,
|
||||
detail: String(source?.detail || "").trim(),
|
||||
}));
|
||||
}
|
||||
|
||||
function getPaymentSourceById(storeConfig, paymentSourceId) {
|
||||
const sourceId = String(paymentSourceId || "").trim();
|
||||
return getPaymentSources(storeConfig).find(
|
||||
(source) => source.id === sourceId,
|
||||
);
|
||||
}
|
||||
|
||||
StorefrontApp.getters = {
|
||||
formatTitle,
|
||||
formatCurrency,
|
||||
@ -255,5 +277,7 @@
|
||||
getVisibleItemsPage,
|
||||
getCatalogPagination,
|
||||
summarizeCart,
|
||||
getPaymentSources,
|
||||
getPaymentSourceById,
|
||||
};
|
||||
})();
|
||||
|
||||
@ -69,6 +69,39 @@ PREP_RECOMPILE_END;
|
||||
GVAR(OrgStore) call ["mset", [GVAR(Registry), "org:update", _key, _fieldValuePairs, _sync]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestAssignCreditLine), {
|
||||
params [
|
||||
["_uid", "", [""]],
|
||||
["_memberUid", "", [""]],
|
||||
["_memberName", "", [""]],
|
||||
["_amount", 0, [0]]
|
||||
];
|
||||
|
||||
if (_uid isEqualTo "" || { _memberUid isEqualTo "" } || { _amount <= 0 }) exitWith {
|
||||
diag_log "[FORGE:Server:Org] Invalid credit line request payload!"
|
||||
};
|
||||
|
||||
private _requester = [_uid] call EFUNC(common,getPlayer);
|
||||
if (_requester isEqualTo objNull) exitWith {};
|
||||
|
||||
private _result = GVAR(OrgStore) call ["assignCreditLine", [_uid, _memberUid, _memberName, _amount]];
|
||||
if (_result getOrDefault ["success", false]) then {
|
||||
private _patch = _result getOrDefault ["patch", createHashMap];
|
||||
|
||||
{
|
||||
private _memberPlayer = [_x] call EFUNC(common,getPlayer);
|
||||
if (_memberPlayer isNotEqualTo objNull && { _patch isNotEqualTo createHashMap }) then {
|
||||
[CRPC(org,responseSyncOrg), [_patch], _memberPlayer] call CFUNC(targetEvent);
|
||||
};
|
||||
} forEach (_result getOrDefault ["memberUids", []]);
|
||||
};
|
||||
|
||||
[CRPC(org,responseCreditLine), [createHashMapFromArray [
|
||||
["success", _result getOrDefault ["success", false]],
|
||||
["message", _result getOrDefault ["message", "Unable to assign credit line."]]
|
||||
]], _requester] call CFUNC(targetEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestSaveOrg), {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
|
||||
@ -32,6 +32,7 @@ GVAR(OrgModel) = compileFinal createHashMapObject [[
|
||||
_org set ["name", ""];
|
||||
_org set ["funds", 0];
|
||||
_org set ["reputation", 0];
|
||||
_org set ["credit_lines", createHashMap];
|
||||
_org set ["assets", createHashMap];
|
||||
_org set ["fleet", createHashMap];
|
||||
_org set ["members", createHashMap];
|
||||
@ -57,13 +58,15 @@ GVAR(OrgModel) = compileFinal createHashMapObject [[
|
||||
private _name = _org get "name";
|
||||
private _funds = _org get "funds";
|
||||
private _reputation = _org get "reputation";
|
||||
private _creditLines = _org getOrDefault ["credit_lines", createHashMap];
|
||||
|
||||
[_id, _owner, _name, _funds, _reputation] try {
|
||||
[_id, _owner, _name, _funds, _reputation, _creditLines] try {
|
||||
if (_id isEqualTo "" || !(_id isEqualType "")) then { throw "Invalid ID!"; };
|
||||
if (_owner isEqualTo "" || !(_owner isEqualType "")) then { throw "Invalid Owner!"; };
|
||||
if (_name isEqualTo "" || !(_name isEqualType "")) then { throw "Invalid Name!"; };
|
||||
if (_funds isEqualTo 0 || !(_funds isEqualType 0)) then { throw "Invalid Funds!"; };
|
||||
if (_reputation isEqualTo 0 || !(_reputation isEqualType 0)) then { throw "Invalid Reputation!"; };
|
||||
if !(_creditLines isEqualType createHashMap) then { throw "Invalid Credit Lines!"; };
|
||||
} catch {
|
||||
["ERROR", format ["Failed to validate org %1!", _exception]] call EFUNC(common,log);
|
||||
false
|
||||
@ -91,6 +94,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
["name", "Forge Dynamics"],
|
||||
["funds", 200000],
|
||||
["reputation", 0],
|
||||
["credit_lines", createHashMap],
|
||||
["assets", createHashMap],
|
||||
["fleet", createHashMap],
|
||||
["members", createHashMap]
|
||||
@ -110,6 +114,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
_finalOrg set ["name", "Forge Dynamics"];
|
||||
_finalOrg set ["funds", 200000];
|
||||
_finalOrg set ["reputation", 0];
|
||||
_finalOrg set ["credit_lines", createHashMap];
|
||||
|
||||
private _json = _self call ["toJSON", [_finalOrg]];
|
||||
["org:create", ["default", _json]] call EFUNC(extension,extCall);
|
||||
@ -153,18 +158,15 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
|
||||
private _org = _self call ["fetch", ["org:get", _orgID]];
|
||||
if (_org isEqualTo createHashMap) exitWith { _org };
|
||||
_org = GVAR(OrgModel) call ["migrate", [_org]];
|
||||
|
||||
private _memberRows = _self call ["fetch", ["org:members:get", _orgID]];
|
||||
if !(_memberRows isEqualType []) then {
|
||||
_memberRows = [];
|
||||
};
|
||||
if !(_memberRows isEqualType []) then { _memberRows = []; };
|
||||
|
||||
private _memberMap = createHashMap;
|
||||
{
|
||||
private _memberUid = _x getOrDefault ["uid", ""];
|
||||
if (_memberUid isNotEqualTo "") then {
|
||||
_memberMap set [_memberUid, _x];
|
||||
};
|
||||
if (_memberUid isNotEqualTo "") then { _memberMap set [_memberUid, _x]; };
|
||||
} forEach _memberRows;
|
||||
|
||||
_org set ["members", _memberMap];
|
||||
@ -407,6 +409,86 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
_result set ["members", _memberResults];
|
||||
_result
|
||||
}],
|
||||
["assignCreditLine", compileFinal {
|
||||
params [
|
||||
["_requesterUid", "", [""]],
|
||||
["_memberUid", "", [""]],
|
||||
["_memberName", "", [""]],
|
||||
["_amount", 0, [0]]
|
||||
];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", ""],
|
||||
["patch", createHashMap],
|
||||
["memberUids", []]
|
||||
];
|
||||
|
||||
if (
|
||||
_requesterUid isEqualTo ""
|
||||
|| { _memberUid isEqualTo "" }
|
||||
|| { _amount <= 0 }
|
||||
) exitWith {
|
||||
_result set ["message", "A valid requester, member, and credit amount are required."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
|
||||
private _orgID = _requesterActor getOrDefault ["organization", "default"];
|
||||
if (_orgID isEqualTo "") then {
|
||||
_orgID = "default";
|
||||
};
|
||||
|
||||
private _org = _self call ["loadById", [_orgID]];
|
||||
if (_org isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Unable to load organization data for credit line assignment."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _ownerUid = _org getOrDefault ["owner", ""];
|
||||
private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer);
|
||||
private _isDefaultOrg = (_orgID isEqualTo "default") || { toLower _ownerUid isEqualTo "server" };
|
||||
private _isDefaultOrgCeo = _isDefaultOrg
|
||||
&& { _requesterPlayer isNotEqualTo objNull }
|
||||
&& { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" };
|
||||
private _canManageTreasury = (_ownerUid isEqualTo _requesterUid) || _isDefaultOrgCeo;
|
||||
|
||||
if !_canManageTreasury exitWith {
|
||||
_result set ["message", "Only the organization leader or CEO can manage treasury actions."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _members = _org getOrDefault ["members", createHashMap];
|
||||
private _memberRecord = _members getOrDefault [_memberUid, createHashMap];
|
||||
if (_memberRecord isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Selected member was not found in the organization roster."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _resolvedMemberName = _memberRecord getOrDefault ["name", _memberName];
|
||||
if (_resolvedMemberName isEqualTo "") then {
|
||||
_resolvedMemberName = _memberName;
|
||||
};
|
||||
|
||||
private _creditLines = +(_org getOrDefault ["credit_lines", createHashMap]);
|
||||
_creditLines set [_memberUid, createHashMapFromArray [
|
||||
["uid", _memberUid],
|
||||
["name", _resolvedMemberName],
|
||||
["amount", _amount]
|
||||
]];
|
||||
|
||||
private _patch = _self call ["set", [GVAR(Registry), "org:update", _orgID, "credit_lines", _creditLines, true]];
|
||||
private _memberUids = keys _members;
|
||||
if !(_requesterUid in _memberUids) then {
|
||||
_memberUids pushBack _requesterUid;
|
||||
};
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", format ["Credit line of $%1 assigned to %2.", [_amount] call BIS_fnc_numberText, _resolvedMemberName]];
|
||||
_result set ["patch", _patch];
|
||||
_result set ["memberUids", _memberUids];
|
||||
_result
|
||||
}],
|
||||
["register", compileFinal {
|
||||
params [["_uid", "", [""]], ["_orgName", "", [""]]];
|
||||
|
||||
@ -454,6 +536,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
["name", _orgName],
|
||||
["funds", 0],
|
||||
["reputation", 0],
|
||||
["credit_lines", createHashMap],
|
||||
["assets", createHashMap],
|
||||
["fleet", createHashMap],
|
||||
["members", createHashMap]
|
||||
@ -466,9 +549,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
_result
|
||||
};
|
||||
|
||||
if (_createResult isNotEqualTo "") then {
|
||||
_org = _self call ["toHashMap", [_createResult]];
|
||||
};
|
||||
if (_createResult isNotEqualTo "") then { _org = _self call ["toHashMap", [_createResult]]; };
|
||||
|
||||
_org set ["members", createHashMap];
|
||||
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
|
||||
|
||||
1
arma/server/addons/store/$PBOPREFIX$
Normal file
1
arma/server/addons/store/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
forge\forge_server\addons\store
|
||||
17
arma/server/addons/store/CfgEventHandlers.hpp
Normal file
17
arma/server/addons/store/CfgEventHandlers.hpp
Normal file
@ -0,0 +1,17 @@
|
||||
class Extended_PreStart_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PreInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PostInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
|
||||
};
|
||||
};
|
||||
3
arma/server/addons/store/README.md
Normal file
3
arma/server/addons/store/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# forge_server_store
|
||||
|
||||
Description for this addon
|
||||
2
arma/server/addons/store/XEH_PREP.hpp
Normal file
2
arma/server/addons/store/XEH_PREP.hpp
Normal file
@ -0,0 +1,2 @@
|
||||
// PREP(initStore);
|
||||
// PREP(initStoreStore);
|
||||
3
arma/server/addons/store/XEH_postInit.sqf
Normal file
3
arma/server/addons/store/XEH_postInit.sqf
Normal file
@ -0,0 +1,3 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
// call FUNC(initStore);
|
||||
7
arma/server/addons/store/XEH_preInit.sqf
Normal file
7
arma/server/addons/store/XEH_preInit.sqf
Normal file
@ -0,0 +1,7 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
PREP_RECOMPILE_START;
|
||||
#include "XEH_PREP.hpp"
|
||||
PREP_RECOMPILE_END;
|
||||
|
||||
// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
|
||||
2
arma/server/addons/store/XEH_preStart.sqf
Normal file
2
arma/server/addons/store/XEH_preStart.sqf
Normal file
@ -0,0 +1,2 @@
|
||||
#include "script_component.hpp"
|
||||
#include "XEH_PREP.hpp"
|
||||
20
arma/server/addons/store/config.cpp
Normal file
20
arma/server/addons/store/config.cpp
Normal file
@ -0,0 +1,20 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
class CfgPatches {
|
||||
class ADDON {
|
||||
author = AUTHOR;
|
||||
authors[] = {"J.Schmidt"};
|
||||
url = ECSTRING(main,url);
|
||||
name = COMPONENT_NAME;
|
||||
requiredVersion = REQUIRED_VERSION;
|
||||
requiredAddons[] = {
|
||||
"forge_server_main",
|
||||
"forge_server_common"
|
||||
};
|
||||
units[] = {};
|
||||
weapons[] = {};
|
||||
VERSION_CONFIG;
|
||||
};
|
||||
};
|
||||
|
||||
#include "CfgEventHandlers.hpp"
|
||||
9
arma/server/addons/store/script_component.hpp
Normal file
9
arma/server/addons/store/script_component.hpp
Normal file
@ -0,0 +1,9 @@
|
||||
#define COMPONENT store
|
||||
#define COMPONENT_BEAUTIFIED Store
|
||||
#include "\forge\forge_server\addons\main\script_mod.hpp"
|
||||
|
||||
// #define DEBUG_MODE_FULL
|
||||
// #define DISABLE_COMPILE_CACHE
|
||||
// #define ENABLE_PERFORMANCE_COUNTERS
|
||||
|
||||
#include "\forge\forge_server\addons\main\script_macros.hpp"
|
||||
8
arma/server/addons/store/stringtable.xml
Normal file
8
arma/server/addons/store/stringtable.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project name="FFE">
|
||||
<Package name="Store">
|
||||
<Key ID="STR_forge_server_store_displayName">
|
||||
<English>Store</English>
|
||||
</Key>
|
||||
</Package>
|
||||
</Project>
|
||||
@ -10,6 +10,6 @@ pub use actor::Actor;
|
||||
pub use bank::Bank;
|
||||
pub use garage::{Garage, HitPoints, Vehicle};
|
||||
pub use locker::{Item, Locker};
|
||||
pub use org::{MemberSummary, Org};
|
||||
pub use org::{CreditLineSummary, MemberSummary, Org};
|
||||
pub use v_garage::{VGarage, VehicleCategory};
|
||||
pub use v_locker::{EquipmentCategory, VLocker};
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
use arma_rs::{FromArma, IntoArma};
|
||||
use forge_shared::OrgValidationError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreditLineSummary {
|
||||
pub uid: String,
|
||||
pub name: String,
|
||||
pub amount: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Org {
|
||||
@ -12,6 +20,8 @@ pub struct Org {
|
||||
pub funds: f64,
|
||||
#[serde(default)]
|
||||
pub reputation: i64,
|
||||
#[serde(default)]
|
||||
pub credit_lines: HashMap<String, CreditLineSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -28,6 +38,7 @@ impl Org {
|
||||
name: name.into(),
|
||||
funds: 0.0,
|
||||
reputation: 0,
|
||||
credit_lines: HashMap::new(),
|
||||
};
|
||||
|
||||
org.validate()?;
|
||||
@ -65,6 +76,26 @@ impl Org {
|
||||
return Err(OrgValidationError::InvalidName(self.name.clone()));
|
||||
}
|
||||
|
||||
for (uid, credit_line) in &self.credit_lines {
|
||||
let resolved_uid = if credit_line.uid.trim().is_empty() {
|
||||
uid
|
||||
} else {
|
||||
&credit_line.uid
|
||||
};
|
||||
|
||||
if !resolved_uid.chars().all(|c| c.is_numeric()) || resolved_uid.len() != 17 {
|
||||
return Err(OrgValidationError::InvalidCreditLineUid(
|
||||
resolved_uid.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if credit_line.amount < 0.0 {
|
||||
return Err(OrgValidationError::NegativeCreditLine(
|
||||
resolved_uid.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@ -5,8 +5,9 @@
|
||||
//!
|
||||
//! For full documentation, architecture, and examples, see the [crate README](../README.md).
|
||||
|
||||
use forge_models::{MemberSummary, Org};
|
||||
use forge_models::{CreditLineSummary, MemberSummary, Org};
|
||||
use forge_repositories::OrgRepository;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Service layer implementation for organization business logic and operations.
|
||||
///
|
||||
@ -138,6 +139,21 @@ impl<R: OrgRepository> OrgService<R> {
|
||||
return Err("Reputation must be an integer".to_string());
|
||||
}
|
||||
}
|
||||
"credit_lines" => {
|
||||
if value.is_null() {
|
||||
updated_org.credit_lines = HashMap::new();
|
||||
} else {
|
||||
updated_org.credit_lines = serde_json::from_value::<
|
||||
HashMap<String, CreditLineSummary>,
|
||||
>(value.clone())
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Credit lines must be an object of member credit entries: {}",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("Unknown field: {}", field));
|
||||
}
|
||||
|
||||
@ -80,9 +80,11 @@ pub enum OrgValidationError {
|
||||
EmptyOwner,
|
||||
EmptyName,
|
||||
NegativeFunds,
|
||||
NegativeCreditLine(String),
|
||||
InvalidId(String),
|
||||
InvalidOwner(String),
|
||||
InvalidName(String),
|
||||
InvalidCreditLineUid(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for OrgValidationError {
|
||||
@ -94,6 +96,9 @@ impl fmt::Display for OrgValidationError {
|
||||
OrgValidationError::NegativeFunds => {
|
||||
write!(f, "Organization funds cannot be negative")
|
||||
}
|
||||
OrgValidationError::NegativeCreditLine(uid) => {
|
||||
write!(f, "Credit line for '{}' cannot be negative", uid)
|
||||
}
|
||||
OrgValidationError::InvalidId(id) => write!(
|
||||
f,
|
||||
"Invalid organization ID '{}' - must contain only alphanumeric characters and underscores",
|
||||
@ -107,6 +112,11 @@ impl fmt::Display for OrgValidationError {
|
||||
"Invalid organization name '{}' - cannot exceed 100 characters or contain control characters",
|
||||
name
|
||||
),
|
||||
OrgValidationError::InvalidCreditLineUid(uid) => write!(
|
||||
f,
|
||||
"Invalid credit line UID '{}' - must be a 17-digit Steam ID",
|
||||
uid
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user