diff --git a/arma/client/addons/org/XEH_PREP.hpp b/arma/client/addons/org/XEH_PREP.hpp index d83118a..e5ecefa 100644 --- a/arma/client/addons/org/XEH_PREP.hpp +++ b/arma/client/addons/org/XEH_PREP.hpp @@ -1,3 +1,4 @@ PREP(handleUIEvents); PREP(initOrgClass); +PREP(initOrgUIBridge); PREP(openUI); diff --git a/arma/client/addons/org/XEH_postInitClient.sqf b/arma/client/addons/org/XEH_postInitClient.sqf index 9ad22af..13e220b 100644 --- a/arma/client/addons/org/XEH_postInitClient.sqf +++ b/arma/client/addons/org/XEH_postInitClient.sqf @@ -1,6 +1,7 @@ #include "script_component.hpp" if (isNil QGVAR(OrgClass)) then { call FUNC(initOrgClass); }; +if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initOrgUIBridge); }; [QGVAR(initOrg), { GVAR(OrgClass) call ["init", []]; @@ -21,27 +22,19 @@ if (isNil QGVAR(OrgClass)) then { call FUNC(initOrgClass); }; [QGVAR(responseCreateOrg), { params [["_payload", createHashMap, [createHashMap]]]; - private _control = uiNamespace getVariable [QGVAR(PendingBrowserControl), controlNull]; - uiNamespace setVariable [QGVAR(PendingBrowserControl), controlNull]; + GVAR(OrgUIBridge) call ["handleCreateResponse", [_payload]]; +}] call CFUNC(addEventHandler); - private _success = _payload getOrDefault ["success", false]; - if (!_success) exitWith { - if (_control isNotEqualTo controlNull) then { - private _json = toJSON (createHashMapFromArray [ - ["message", _payload getOrDefault ["message", "Organization registration failed."]] - ]); +[QGVAR(responseDisbandOrg), { + params [["_payload", createHashMap, [createHashMap]]]; - _control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.receiveCreateFailure(%1)", _json]]; - }; - }; + GVAR(OrgUIBridge) call ["handleDisbandResponse", [_payload]]; +}] call CFUNC(addEventHandler); - private _orgData = _payload getOrDefault ["org", createHashMap]; - GVAR(OrgClass) call ["sync", [_orgData, true]]; +[QGVAR(responseLeaveOrg), { + params [["_payload", createHashMap, [createHashMap]]]; - if (_control isNotEqualTo controlNull) then { - private _json = toJSON (GVAR(OrgClass) call ["buildPortalPayload", []]); - _control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.receiveCreateSuccess(%1)", _json]]; - }; + GVAR(OrgUIBridge) call ["handleLeaveResponse", [_payload]]; }] call CFUNC(addEventHandler); [{ diff --git a/arma/client/addons/org/functions/fnc_handleUIEvents.sqf b/arma/client/addons/org/functions/fnc_handleUIEvents.sqf index 7769bc5..c6d8ef2 100644 --- a/arma/client/addons/org/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/org/functions/fnc_handleUIEvents.sqf @@ -21,52 +21,25 @@ params ["_control", "_isConfirmDialog", "_message"]; private _alert = fromJSON _message; private _event = _alert get "event"; private _data = _alert get "data"; -// private _display = displayChild findDisplay 46; - -private _fnc_execBridge = { - params ["_control", "_fnName", "_payload"]; - - private _json = toJSON _payload; - _control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.%1(%2)", _fnName, _json]]; -}; diag_log format ["[FORGE:Client:Org] Handling UI event: %1 with data: %2", _event, _data]; switch (_event) do { case "org::close": { closeDialog 1; }; case "org::login::request": { - private _orgData = GVAR(OrgClass) get "org"; - private _orgId = _orgData getOrDefault ["id", ""]; - private _orgName = _orgData getOrDefault ["name", ""]; - - if (_orgId isEqualTo "" && {_orgName isEqualTo ""}) exitWith { - [_control, "receiveLoginFailure", createHashMapFromArray [ - ["message", "No organization data is available for this player."] - ]] call _fnc_execBridge; - }; - - private _payload = GVAR(OrgClass) call ["buildPortalPayload", []]; - [_control, "receiveLoginSuccess", _payload] call _fnc_execBridge; + GVAR(OrgUIBridge) call ["handleLoginRequest", [_control]]; }; case "org::create::request": { - private _orgName = _data getOrDefault ["orgName", ""]; - - if (_orgName isEqualTo "") exitWith { - [_control, "receiveCreateFailure", createHashMapFromArray [ - ["message", "Enter an organization name."] - ]] call _fnc_execBridge; - }; - - uiNamespace setVariable [QGVAR(PendingBrowserControl), _control]; - [SRPC(org,requestCreateOrg), [getPlayerUID player, _orgName]] call CFUNC(serverEvent); + GVAR(OrgUIBridge) call ["handleCreateRequest", [_control, _data]]; + }; + case "org::disband::request": { + GVAR(OrgUIBridge) call ["requestDisband", []]; + }; + case "org::leave::request": { + GVAR(OrgUIBridge) call ["requestLeave", []]; }; case "org::ready": { - [_control, "receive", createHashMapFromArray [ - ["event", "org::ready"], - ["data", createHashMapFromArray [ - ["loaded", true] - ]] - ]] call _fnc_execBridge; + GVAR(OrgUIBridge) call ["handleReady", [_control]]; }; default { hint format ["Unhandled UI event: %1", _event]; }; }; diff --git a/arma/client/addons/org/functions/fnc_initOrgClass.sqf b/arma/client/addons/org/functions/fnc_initOrgClass.sqf index 0e9fcf8..1a5f6c5 100644 --- a/arma/client/addons/org/functions/fnc_initOrgClass.sqf +++ b/arma/client/addons/org/functions/fnc_initOrgClass.sqf @@ -101,7 +101,10 @@ GVAR(OrgBaseClass) = compileFinal createHashMapFromArray [ if (_memberUid isEqualTo _ownerUid && {_ownerName isEqualTo ""}) then { _ownerName = _memberName; }; if (_memberUid isEqualTo _playerUid) then { _sessionRole = "Member"; }; - _membersList pushBack (createHashMapFromArray [["name", _memberName]]); + _membersList pushBack (createHashMapFromArray [ + ["uid", _memberUid], + ["name", _memberName] + ]); } forEach _membersRaw; if (_ownerName isEqualTo "" && { _ownerUid isEqualTo _playerUid }) then { _ownerName = _playerName; }; diff --git a/arma/client/addons/org/functions/fnc_initOrgUIBridge.sqf b/arma/client/addons/org/functions/fnc_initOrgUIBridge.sqf new file mode 100644 index 0000000..d0c4fc5 --- /dev/null +++ b/arma/client/addons/org/functions/fnc_initOrgUIBridge.sqf @@ -0,0 +1,173 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initOrgUIBridge.sqf + * Author: IDSolutions + * Date: 2026-03-10 + * Last Update: 2026-03-10 + * Public: No + * + * Description: + * Initializes the org UI bridge for browser control state and event routing. + * + * Arguments: + * None + * + * Return Value: + * Org UI bridge object [HASHMAP OBJECT] + * + * Examples: + * call forge_client_org_fnc_initOrgUIBridge + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "OrgUIBridgeBaseClass"], + ["#create", compileFinal { + _self set ["pendingBrowserControl", controlNull]; + }], + ["setPendingBrowserControl", compileFinal { + params [["_control", controlNull, [controlNull]]]; + + _self set ["pendingBrowserControl", _control]; + _control + }], + ["consumePendingBrowserControl", compileFinal { + private _control = _self getOrDefault ["pendingBrowserControl", controlNull]; + _self set ["pendingBrowserControl", controlNull]; + + _control + }], + ["getActiveBrowserControl", compileFinal { + private _display = uiNamespace getVariable ["RscOrg", displayNull]; + if (isNull _display) exitWith { controlNull }; + + _display displayCtrl 1003 + }], + ["execBridge", compileFinal { + params [ + ["_control", controlNull, [controlNull]], + ["_fnName", "", [""]], + ["_payload", createHashMap, [createHashMap]] + ]; + + if (isNull _control || { _fnName isEqualTo "" }) exitWith { false }; + + private _json = toJSON _payload; + _control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.%1(%2)", _fnName, _json]]; + + true + }], + ["sendBridgeEvent", compileFinal { + params [ + ["_event", "", [""]], + ["_data", createHashMap, [createHashMap]], + ["_control", controlNull, [controlNull]] + ]; + + if (_event isEqualTo "") exitWith { false }; + + private _targetControl = _control; + if (isNull _targetControl) then { + _targetControl = _self call ["getActiveBrowserControl", []]; + }; + + if (isNull _targetControl) exitWith { false }; + + _self call ["execBridge", [_targetControl, "receive", createHashMapFromArray [ + ["event", _event], + ["data", _data] + ]]] + }], + ["handleLoginRequest", compileFinal { + params [["_control", controlNull, [controlNull]]]; + + private _orgData = GVAR(OrgClass) get "org"; + private _orgId = _orgData getOrDefault ["id", ""]; + private _orgName = _orgData getOrDefault ["name", ""]; + + if (_orgId isEqualTo "" && { _orgName isEqualTo "" }) exitWith { + _self call ["execBridge", [_control, "receiveLoginFailure", createHashMapFromArray [ + ["message", "No organization data is available for this player."] + ]]]; + }; + + _self call ["execBridge", [_control, "receiveLoginSuccess", GVAR(OrgClass) call ["buildPortalPayload", []]]]; + }], + ["handleCreateRequest", compileFinal { + params [ + ["_control", controlNull, [controlNull]], + ["_data", createHashMap, [createHashMap]] + ]; + + private _orgName = _data getOrDefault ["orgName", ""]; + if (_orgName isEqualTo "") exitWith { + _self call ["execBridge", [_control, "receiveCreateFailure", createHashMapFromArray [ + ["message", "Enter an organization name."] + ]]]; + }; + + _self call ["setPendingBrowserControl", [_control]]; + [SRPC(org,requestCreateOrg), [getPlayerUID player, _orgName]] call CFUNC(serverEvent); + }], + ["handleCreateResponse", compileFinal { + params [["_payload", createHashMap, [createHashMap]]]; + + private _control = _self call ["consumePendingBrowserControl", []]; + private _success = _payload getOrDefault ["success", false]; + if (!_success) exitWith { + if (isNull _control) exitWith {}; + + _self call ["execBridge", [_control, "receiveCreateFailure", createHashMapFromArray [ + ["message", _payload getOrDefault ["message", "Organization registration failed."]] + ]]]; + }; + + private _orgData = _payload getOrDefault ["org", createHashMap]; + GVAR(OrgClass) call ["sync", [_orgData, true]]; + + if (isNull _control) exitWith {}; + _self call ["execBridge", [_control, "receiveCreateSuccess", GVAR(OrgClass) call ["buildPortalPayload", []]]]; + }], + ["handleDisbandResponse", compileFinal { + params [["_payload", createHashMap, [createHashMap]]]; + + private _eventName = if (_payload getOrDefault ["success", false]) then { + ["org::portal::revoked", "org::disband::success"] select (_payload getOrDefault ["requester", false]) + } else { + "org::disband::failure" + }; + + _self call ["sendBridgeEvent", [_eventName, _payload]]; + }], + ["handleLeaveResponse", compileFinal { + params [["_payload", createHashMap, [createHashMap]]]; + + private _eventName = [ + "org::leave::failure", + "org::leave::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); + }], + ["handleReady", compileFinal { + params [["_control", controlNull, [controlNull]]]; + + _self call ["sendBridgeEvent", [ + "org::ready", + createHashMapFromArray [ + ["loaded", true] + ], + _control + ]]; + }] +]; + +GVAR(OrgUIBridge) = createHashMapObject [GVAR(OrgUIBridgeBaseClass)]; +GVAR(OrgUIBridge) diff --git a/arma/client/addons/org/ui/_site/bridge.js b/arma/client/addons/org/ui/_site/bridge.js index b4afb78..ecc7062 100644 --- a/arma/client/addons/org/ui/_site/bridge.js +++ b/arma/client/addons/org/ui/_site/bridge.js @@ -41,6 +41,36 @@ store.failCreate("Arma registration bridge is unavailable."); } + function requestDisbandOrg() { + const sent = sendEvent("org::disband::request", {}); + if (sent) { + return; + } + + const OrgPortal = window.OrgPortal; + if (OrgPortal && OrgPortal.actions) { + OrgPortal.actions.showTreasuryNotice( + "error", + "Arma disband bridge is unavailable.", + ); + } + } + + function requestLeaveOrg() { + const sent = sendEvent("org::leave::request", {}); + if (sent) { + return; + } + + const OrgPortal = window.OrgPortal; + if (OrgPortal && OrgPortal.actions) { + OrgPortal.actions.showTreasuryNotice( + "error", + "Arma leave bridge is unavailable.", + ); + } + } + function receive(eventOrPayload, data = {}) { const event = typeof eventOrPayload === "object" && eventOrPayload !== null @@ -70,12 +100,76 @@ store.failCreate( payloadData.message || "Organization registration failed.", ); + return; + } + + const OrgPortal = window.OrgPortal; + if (event === "org::disband::success") { + if (OrgPortal && OrgPortal.store) { + OrgPortal.store.setModal(null); + OrgPortal.store.setOrgDisbanded(true); + } + return; + } + + if (event === "org::disband::failure") { + if (OrgPortal && OrgPortal.store) { + OrgPortal.store.setModal(null); + } + + if (OrgPortal && OrgPortal.actions) { + OrgPortal.actions.showTreasuryNotice( + "error", + payloadData.message || "Organization disbanding failed.", + ); + } + return; + } + + if (event === "org::leave::success") { + if (OrgPortal && OrgPortal.store) { + OrgPortal.store.setModal(null); + } + + store.failLogin( + payloadData.message || "You have left the organization.", + ); + store.setView("home"); + return; + } + + if (event === "org::leave::failure") { + if (OrgPortal && OrgPortal.store) { + OrgPortal.store.setModal(null); + } + + if (OrgPortal && OrgPortal.actions) { + OrgPortal.actions.showTreasuryNotice( + "error", + payloadData.message || "Unable to leave the organization.", + ); + } + return; + } + + if (event === "org::portal::revoked") { + if (OrgPortal && OrgPortal.store) { + OrgPortal.store.setModal(null); + } + + store.failLogin( + payloadData.message || + "Organization access is no longer available.", + ); + store.setView("home"); } } RegistryApp.bridge = { requestLogin, requestCreateOrg, + requestDisbandOrg, + requestLeaveOrg, receive, sendEvent, }; @@ -83,6 +177,8 @@ window.OrgUIBridge = { requestLogin, requestCreateOrg, + requestDisbandOrg, + requestLeaveOrg, receive, receiveLoginSuccess: (data) => receive("org::login::success", data), receiveLoginFailure: (data) => receive("org::login::failure", data), diff --git a/arma/client/addons/org/ui/_site/components/AppShell.js b/arma/client/addons/org/ui/_site/components/AppShell.js index 2d07e4e..6d98f32 100644 --- a/arma/client/addons/org/ui/_site/components/AppShell.js +++ b/arma/client/addons/org/ui/_site/components/AppShell.js @@ -71,6 +71,14 @@ : null; const view = store.getView(); + const portalPermissions = + window.OrgPortal && window.OrgPortal.permissions + ? window.OrgPortal.permissions + : null; + const portalActions = + window.OrgPortal && window.OrgPortal.actions + ? window.OrgPortal.actions + : null; const viewLabel = view === "create" ? "Organization Registration" @@ -116,6 +124,11 @@ } if (view === "portal" && PortalApp) { + const canLeaveOrg = + portalPermissions && + typeof portalPermissions.canLeaveOrg === "function" && + portalPermissions.canLeaveOrg(); + return h( "div", { className: "app-shell" }, @@ -126,6 +139,13 @@ Navbar({ title: "Global Organization Network", viewLabel, + actionLabel: canLeaveOrg ? "Leave Organization" : "", + onAction: + canLeaveOrg && + portalActions && + typeof portalActions.openModal === "function" + ? () => portalActions.openModal("leave") + : null, }), PortalApp(), ); diff --git a/arma/client/addons/org/ui/_site/components/portal/futureCard.js b/arma/client/addons/org/ui/_site/components/portal/futureCard.js index a922043..343835f 100644 --- a/arma/client/addons/org/ui/_site/components/portal/futureCard.js +++ b/arma/client/addons/org/ui/_site/components/portal/futureCard.js @@ -3,10 +3,26 @@ const { h, ensureScopedStyle } = OrgPortal.runtime; const scopeAttr = "data-ui-future-card"; const ROADMAP = [ - { name: "Contracts Board", status: "Planned", detail: "Track payouts, assignments, and claim approvals." }, - { name: "Diplomacy", status: "Future Review", detail: "Possible future module pending a full design and scope review." }, - { name: "Logistics Queue", status: "Future Review", detail: "Possible future module pending a full design and scope review." }, - { name: "Permissions", status: "Future Review", detail: "Possible future module pending a full design and scope review." }, + { + name: "Contracts Board", + status: "Planned", + detail: "Track payouts, assignments, and claim approvals.", + }, + { + name: "Diplomacy", + status: "Future Review", + detail: "Possible future module pending a full design and scope review.", + }, + { + name: "Logistics Queue", + status: "Future Review", + detail: "Possible future module pending a full design and scope review.", + }, + { + name: "Permissions", + status: "Future Review", + detail: "Possible future module pending a full design and scope review.", + }, ]; const scopeSelector = `[${scopeAttr}]`; const futureCardCss = ` diff --git a/arma/client/addons/org/ui/_site/components/portal/membersCard.js b/arma/client/addons/org/ui/_site/components/portal/membersCard.js index 3bc616e..83b2e8d 100644 --- a/arma/client/addons/org/ui/_site/components/portal/membersCard.js +++ b/arma/client/addons/org/ui/_site/components/portal/membersCard.js @@ -63,7 +63,7 @@ ${scopeSelector} .org-name-row button { className: "org-scroll-panel org-span-5", title: "Members", subtitle: - "Current roster listing. The organization owner cannot be removed.", + "Current roster listing. The organization owner and your own member entry cannot be removed.", rootProps: { [scopeAttr]: "" }, body: h( "div", @@ -71,7 +71,7 @@ ${scopeSelector} .org-name-row button { ...members.map((member) => { const canRemoveMember = allowMemberManagement && - !actions.isOwnerMember(member.name); + !actions.isProtectedMember(member); return h( "article", @@ -86,7 +86,7 @@ ${scopeSelector} .org-name-row button { title: `Remove ${member.name}`, "aria-label": `Remove ${member.name}`, onClick: () => - actions.removeMember(member.name), + actions.removeMember(member), }, h( "svg", diff --git a/arma/client/addons/org/ui/_site/components/portal/modalLayer.js b/arma/client/addons/org/ui/_site/components/portal/modalLayer.js index a0f0d3b..611ecfb 100644 --- a/arma/client/addons/org/ui/_site/components/portal/modalLayer.js +++ b/arma/client/addons/org/ui/_site/components/portal/modalLayer.js @@ -246,6 +246,41 @@ ), ), ); + } else if (modal.type === "leave") { + title = "Leave Organization"; + body = h( + "div", + { className: "app-modal-danger" }, + h( + "p", + null, + "Leave ", + portalData.org.name, + " and return to the default organization?", + ), + h( + "div", + { className: "app-modal-danger-actions" }, + h( + "button", + { + type: "button", + className: "org-secondary-btn", + onClick: () => actions.closeModal(), + }, + "Cancel", + ), + h( + "button", + { + type: "button", + className: "org-danger-btn", + onClick: () => actions.leaveOrganization(), + }, + "Confirm Leave", + ), + ), + ); } return Modal({ diff --git a/arma/client/addons/org/ui/_site/logic/portalActions.js b/arma/client/addons/org/ui/_site/logic/portalActions.js index bb9e9d1..b42d418 100644 --- a/arma/client/addons/org/ui/_site/logic/portalActions.js +++ b/arma/client/addons/org/ui/_site/logic/portalActions.js @@ -88,17 +88,58 @@ return el ? el.value : ""; } - isOwnerMember(memberName) { + getMemberName(member) { + if (member && typeof member === "object") { + return String(member.name || ""); + } + + return String(member || ""); + } + + getMemberUid(member) { + if (member && typeof member === "object") { + return String(member.uid || ""); + } + + return ""; + } + + isOwnerMember(member) { return ( - String(memberName || "") - .trim() - .toLowerCase() === + this.getMemberName(member).trim().toLowerCase() === String(portalData.org.owner || "") .trim() .toLowerCase() ); } + isCurrentMember(member) { + const session = window.OrgPortal?.data?.session || {}; + const memberUid = this.getMemberUid(member) + .trim() + .toLowerCase(); + const actorUid = String(session.actorUid || "") + .trim() + .toLowerCase(); + + if (memberUid && actorUid) { + return memberUid === actorUid; + } + + return ( + this.getMemberName(member).trim().toLowerCase() === + String(session.actorName || "") + .trim() + .toLowerCase() + ); + } + + isProtectedMember(member) { + return ( + this.isOwnerMember(member) || this.isCurrentMember(member) + ); + } + closePortal() { if ( typeof A3API !== "undefined" && @@ -136,6 +177,10 @@ return; } + if (type === "leave" && !permissions.canLeaveOrg()) { + return; + } + store.setModal({ type }); } @@ -143,18 +188,23 @@ store.setModal(null); } - removeMember(memberName) { + removeMember(member) { if (!permissions.canManageMembers()) { return false; } - if (this.isOwnerMember(memberName)) { + if (this.isProtectedMember(member)) { return false; } + const memberUid = this.getMemberUid(member); + const memberName = this.getMemberName(member); + store.setMembers((currentMembers) => - currentMembers.filter( - (member) => member.name !== memberName, + currentMembers.filter((entry) => + memberUid + ? entry.uid !== memberUid + : entry.name !== memberName, ), ); store.setCreditLines((currentLines) => @@ -168,8 +218,42 @@ return false; } - store.setOrgDisbanded(true); + const bridge = window.RegistryApp + ? window.RegistryApp.bridge + : null; + + if (!bridge || typeof bridge.requestDisbandOrg !== "function") { + this.showTreasuryNotice( + "error", + "Disband bridge is unavailable.", + ); + return false; + } + this.closeModal(); + bridge.requestDisbandOrg(); + return true; + } + + leaveOrganization() { + if (!permissions.canLeaveOrg()) { + return false; + } + + const bridge = window.RegistryApp + ? window.RegistryApp.bridge + : null; + + if (!bridge || typeof bridge.requestLeaveOrg !== "function") { + this.showTreasuryNotice( + "error", + "Leave bridge is unavailable.", + ); + return false; + } + + this.closeModal(); + bridge.requestLeaveOrg(); return true; } diff --git a/arma/client/addons/org/ui/_site/logic/portalPermissions.js b/arma/client/addons/org/ui/_site/logic/portalPermissions.js index b454363..f1ce3cd 100644 --- a/arma/client/addons/org/ui/_site/logic/portalPermissions.js +++ b/arma/client/addons/org/ui/_site/logic/portalPermissions.js @@ -66,7 +66,11 @@ } canDisbandOrg() { - return this.isOrgLeaderOrCeo(); + return this.isOrgOwner() && !this.isDefaultOrg(); + } + + canLeaveOrg() { + return !this.isDefaultOrg() && !this.isOrgOwner(); } } diff --git a/arma/server/addons/org/XEH_preInit.sqf b/arma/server/addons/org/XEH_preInit.sqf index 5a3cf79..1ebf302 100644 --- a/arma/server/addons/org/XEH_preInit.sqf +++ b/arma/server/addons/org/XEH_preInit.sqf @@ -25,11 +25,8 @@ PREP_RECOMPILE_END; private _result = GVAR(OrgStore) call ["register", [_uid, _orgName]]; if (_result getOrDefault ["success", false]) then { - private _org = _result getOrDefault ["org", createHashMap]; - private _orgID = _org getOrDefault ["id", ""]; - - if (_orgID isNotEqualTo "") then { - private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, true]]; + private _actorPatch = _result getOrDefault ["actorPatch", createHashMap]; + if (_actorPatch isNotEqualTo createHashMap) then { [CRPC(actor,responseSyncActor), [_actorPatch], _player] call CFUNC(targetEvent); }; }; @@ -89,5 +86,85 @@ PREP_RECOMPILE_END; private _index = GVAR(IndexRegistry) get _uid; private _key = _index get "orgID"; - GVAR(OrgStore) call ["remove", [GVAR(Registry), "org:update", _key]]; + GVAR(OrgStore) call ["delete", [_key]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestLeaveOrg), { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { + diag_log "[FORGE:Server:Org] Empty/Invalid UID for leave request!" + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith {}; + + private _result = GVAR(OrgStore) call ["leave", [_uid]]; + if (_result getOrDefault ["success", false]) then { + private _actorPatch = _result getOrDefault ["actorPatch", createHashMap]; + if (_actorPatch isNotEqualTo createHashMap) then { + [CRPC(actor,responseSyncActor), [_actorPatch], _player] call CFUNC(targetEvent); + }; + + GVAR(OrgStore) call ["init", [_uid]]; + + private _notificationParams = _result getOrDefault ["notification", []]; + if (_notificationParams isEqualType [] && { count _notificationParams > 0 }) then { + [CRPC(notifications,recieveNotification), _notificationParams, _player] call CFUNC(targetEvent); + }; + }; + + [CRPC(org,responseLeaveOrg), [createHashMapFromArray [ + ["success", _result getOrDefault ["success", false]], + ["message", _result getOrDefault ["message", "Unable to leave the organization."]] + ]], _player] call CFUNC(targetEvent); +}] call CFUNC(addEventHandler); + +[QGVAR(requestDisbandOrg), { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { + diag_log "[FORGE:Server:Org] Empty/Invalid UID for disband request!" + }; + + private _requester = [_uid] call EFUNC(common,getPlayer); + if (_requester isEqualTo objNull) exitWith {}; + + private _result = GVAR(OrgStore) call ["disband", [_uid]]; + if !(_result getOrDefault ["success", false]) exitWith { + [CRPC(org,responseDisbandOrg), [createHashMapFromArray [ + ["success", false], + ["message", _result getOrDefault ["message", "Failed to disband organization."]], + ["requester", true] + ]], _requester] call CFUNC(targetEvent); + }; + + { + [_x, _result] call { + params [["_member", createHashMap, [createHashMap]], ["_disbandResult", createHashMap, [createHashMap]]]; + + private _memberUid = _member getOrDefault ["uid", ""]; + if (_memberUid isEqualTo "") exitWith {}; + + private _memberPlayer = [_memberUid] call EFUNC(common,getPlayer); + if (_memberPlayer isEqualTo objNull) exitWith {}; + + private _actorPatch = _member getOrDefault ["actorPatch", createHashMap]; + if (_actorPatch isNotEqualTo createHashMap) then { + [CRPC(actor,responseSyncActor), [_actorPatch], _memberPlayer] call CFUNC(targetEvent); + }; + + GVAR(OrgStore) call ["init", [_memberUid]]; + [CRPC(org,responseDisbandOrg), [createHashMapFromArray [ + ["success", true], + ["message", _member getOrDefault ["message", _disbandResult getOrDefault ["message", "Organization disbanded."]]], + ["requester", _member getOrDefault ["requester", false]] + ]], _memberPlayer] call CFUNC(targetEvent); + + private _notificationParams = _member getOrDefault ["notification", []]; + if (_notificationParams isEqualType [] && { count _notificationParams > 0 }) then { + [CRPC(notifications,recieveNotification), _notificationParams, _memberPlayer] call CFUNC(targetEvent); + }; + }; + } forEach (_result getOrDefault ["members", []]); }] call CFUNC(addEventHandler); diff --git a/arma/server/addons/org/functions/fnc_initOrgStore.sqf b/arma/server/addons/org/functions/fnc_initOrgStore.sqf index e6bbb8a..83d3c83 100644 --- a/arma/server/addons/org/functions/fnc_initOrgStore.sqf +++ b/arma/server/addons/org/functions/fnc_initOrgStore.sqf @@ -140,13 +140,281 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _org }], + ["loadById", compileFinal { + params [["_orgID", "", [""]]]; + + if (_orgID isEqualTo "") exitWith { createHashMap }; + + private _cached = GVAR(Registry) getOrDefault [_orgID, createHashMap]; + if (_cached isNotEqualTo createHashMap) exitWith { _cached }; + + ["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_existsResult", "_existsSuccess"]; + if (!_existsSuccess || { _existsResult isNotEqualTo "true" }) exitWith { createHashMap }; + + private _org = _self call ["fetch", ["org:get", _orgID]]; + if (_org isEqualTo createHashMap) exitWith { _org }; + + private _memberRows = _self call ["fetch", ["org:members:get", _orgID]]; + if !(_memberRows isEqualType []) then { + _memberRows = []; + }; + + private _memberMap = createHashMap; + { + private _memberUid = _x getOrDefault ["uid", ""]; + if (_memberUid isNotEqualTo "") then { + _memberMap set [_memberUid, _x]; + }; + } forEach _memberRows; + + _org set ["members", _memberMap]; + GVAR(Registry) set [_orgID, _org, true]; + _org + }], + ["addMember", compileFinal { + params [ + ["_orgID", "", [""]], + ["_uid", "", [""]], + ["_player", objNull, [objNull]], + ["_actor", createHashMap, [createHashMap]] + ]; + + if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap }; + + private _org = _self call ["loadById", [_orgID]]; + if (_org isEqualTo createHashMap) exitWith { _org }; + + _org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]]; + GVAR(Registry) set [_orgID, _org, true]; + _org + }], + ["removeMember", compileFinal { + params [ + ["_orgID", "", [""]], + ["_uid", "", [""]] + ]; + + if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap }; + + private _org = _self call ["loadById", [_orgID]]; + if (_org isEqualTo createHashMap) exitWith { _org }; + + ["org:members:remove", [_orgID, _uid]] call EFUNC(extension,extCall) params ["_memberResult", "_memberSuccess"]; + if (!_memberSuccess) exitWith { + ["WARNING", format ["Failed to remove %1 from org %2 members: %3", _uid, _orgID, _memberResult]] call EFUNC(common,log); + createHashMap + }; + + private _members = +(_org getOrDefault ["members", createHashMap]); + _members deleteAt _uid; + _org set ["members", _members]; + GVAR(Registry) set [_orgID, _org, true]; + + _org + }], + ["delete", compileFinal { + params [["_orgID", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", ""] + ]; + + if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith { + _result set ["message", "Invalid organization ID."]; + _result + }; + + ["org:delete", [_orgID]] call EFUNC(extension,extCall) params ["_deleteResult", "_deleteSuccess"]; + if (!_deleteSuccess || { _deleteResult isNotEqualTo "OK" }) exitWith { + _result set ["message", format ["Failed to delete organization: %1", _deleteResult]]; + _result + }; + + GVAR(Registry) deleteAt _orgID; + _result set ["success", true]; + _result + }], + ["restoreDefaultMembership", compileFinal { + params [ + ["_uid", "", [""]], + ["_player", objNull, [objNull]], + ["_actor", createHashMap, [createHashMap]] + ]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", ""], + ["actorPatch", createHashMap] + ]; + + if (_uid isEqualTo "") exitWith { + _result set ["message", "A valid player UID is required."]; + _result + }; + + private _resolvedPlayer = _player; + if (_resolvedPlayer isEqualTo objNull) then { + _resolvedPlayer = [_uid] call EFUNC(common,getPlayer); + }; + + private _resolvedActor = EGVAR(actor,Registry) getOrDefault [_uid, _actor]; + private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", "default", true]]; + private _defaultOrg = _self call ["addMember", ["default", _uid, _resolvedPlayer, EGVAR(actor,Registry) getOrDefault [_uid, _resolvedActor]]]; + if (_defaultOrg isEqualTo createHashMap) exitWith { + _result set ["message", "Failed to restore default organization membership."]; + _result + }; + + GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", "default"]]]; + _result set ["success", true]; + _result set ["actorPatch", _actorPatch]; + _result + }], + ["leave", compileFinal { + params [["_uid", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", ""], + ["actorPatch", createHashMap], + ["notification", []] + ]; + + if (_uid isEqualTo "") exitWith { + _result set ["message", "A valid player UID is required."]; + _result + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; + private _orgID = _actor getOrDefault ["organization", ""]; + if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith { + _result set ["message", "You are already assigned to the default organization."]; + _result + }; + + private _org = _self call ["loadById", [_orgID]]; + if (_org isEqualTo createHashMap) exitWith { + _result set ["message", "Unable to load organization data for leave request."]; + _result + }; + + private _ownerUid = _org getOrDefault ["owner", ""]; + if (_ownerUid isEqualTo _uid) exitWith { + _result set ["message", "Organization owners must disband the organization instead of leaving it."]; + _result + }; + + private _orgName = _org getOrDefault ["name", "Organization"]; + private _updatedOrg = _self call ["removeMember", [_orgID, _uid]]; + if (_updatedOrg isEqualTo createHashMap) exitWith { + _result set ["message", "Failed to remove you from the organization roster."]; + _result + }; + + private _defaultResult = _self call ["restoreDefaultMembership", [_uid, _player, _actor]]; + if !(_defaultResult getOrDefault ["success", false]) exitWith { + _result set ["message", _defaultResult getOrDefault ["message", "Failed to restore default organization membership."]]; + _result + }; + + private _message = format ["You left %1 and returned to the default organization.", _orgName]; + _result set ["success", true]; + _result set ["message", _message]; + _result set ["actorPatch", _defaultResult getOrDefault ["actorPatch", createHashMap]]; + _result set ["notification", ["info", "Organization Left", _message, 6000]]; + _result + }], + ["disband", compileFinal { + params [["_uid", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", ""], + ["members", []] + ]; + + if (_uid isEqualTo "") exitWith { + _result set ["message", "A valid player UID is required."]; + _result + }; + + private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; + private _orgID = _actor getOrDefault ["organization", ""]; + if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith { + _result set ["message", "Only active player organizations can be disbanded."]; + _result + }; + + private _org = _self call ["loadById", [_orgID]]; + if (_org isEqualTo createHashMap) exitWith { + _result set ["message", "Unable to load organization data for disbanding."]; + _result + }; + + private _ownerUid = _org getOrDefault ["owner", ""]; + if (_ownerUid isEqualTo "" || { _ownerUid isNotEqualTo _uid }) exitWith { + _result set ["message", "Only the organization owner can disband this organization."]; + _result + }; + + private _orgName = _org getOrDefault ["name", "Organization"]; + private _memberMap = _org getOrDefault ["members", createHashMap]; + private _memberUids = keys _memberMap; + if !(_uid in _memberUids) then { + _memberUids pushBack _uid; + }; + + private _deleteResult = _self call ["delete", [_orgID]]; + if !(_deleteResult getOrDefault ["success", false]) exitWith { + _result set ["message", _deleteResult getOrDefault ["message", "Failed to disband organization."]]; + _result + }; + + private _memberResults = []; + { + private _memberUid = _x; + if (_memberUid isNotEqualTo "") then { + private _memberPlayer = [_memberUid] call EFUNC(common,getPlayer); + private _defaultResult = _self call ["restoreDefaultMembership", [_memberUid, _memberPlayer, EGVAR(actor,Registry) getOrDefault [_memberUid, createHashMap]]]; + if !(_defaultResult getOrDefault ["success", false]) then { + ["WARNING", format ["Failed to restore default org for %1 after disbanding %2: %3", _memberUid, _orgID, _defaultResult getOrDefault ["message", "Unknown error."]]] call EFUNC(common,log); + }; + + private _responseMessage = [ + format ["%1 has been disbanded.", _orgName], + format ["Your organization, %1, has been disbanded.", _orgName] + ] select (_memberUid isEqualTo _uid); + + private _notificationParams = [ + ["warning", "Organization Disbanded", _responseMessage, 6000], + ["success", "Organization Disbanded", _responseMessage, 6000] + ] select (_memberUid isEqualTo _uid); + + _memberResults pushBack (createHashMapFromArray [ + ["uid", _memberUid], + ["requester", _memberUid isEqualTo _uid], + ["message", _responseMessage], + ["notification", _notificationParams], + ["actorPatch", _defaultResult getOrDefault ["actorPatch", createHashMap]] + ]); + }; + } forEach _memberUids; + + _result set ["success", true]; + _result set ["message", format ["%1 has been disbanded.", _orgName]]; + _result set ["members", _memberResults]; + _result + }], ["register", compileFinal { params [["_uid", "", [""]], ["_orgName", "", [""]]]; private _result = createHashMapFromArray [ ["success", false], ["message", ""], - ["org", createHashMap] + ["org", createHashMap], + ["actorPatch", createHashMap] ]; if (_uid isEqualTo "" || { _orgName isEqualTo "" }) exitWith { @@ -158,7 +426,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; private _existingOrgID = _actor getOrDefault ["organization", ""]; - if (_existingOrgID isNotEqualTo "") exitWith { + if (_existingOrgID isNotEqualTo "" && { toLower _existingOrgID isNotEqualTo "default" }) exitWith { _result set ["message", "Player already belongs to an organization."]; _result }; @@ -205,21 +473,20 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _org set ["members", createHashMap]; _org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]]; - ["org:members:remove", ["default", _uid]] call EFUNC(extension,extCall); - - private _defaultOrg = GVAR(Registry) getOrDefault ["default", createHashMap]; - if (_defaultOrg isNotEqualTo createHashMap) then { - private _defaultMembers = _defaultOrg getOrDefault ["members", createHashMap]; - _defaultMembers deleteAt _uid; - _defaultOrg set ["members", _defaultMembers]; - GVAR(Registry) set ["default", _defaultOrg, true]; + if (toLower _existingOrgID isEqualTo "default") then { + private _defaultOrg = _self call ["removeMember", ["default", _uid]]; + if (_defaultOrg isEqualTo createHashMap) then { + ["WARNING", format ["Failed to remove %1 from default org members after creating org %2.", _uid, _orgID]] call EFUNC(common,log); + }; }; + private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, true]]; GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", _orgID]]]; GVAR(Registry) set [_orgID, _org, true]; _result set ["success", true]; _result set ["org", _org]; + _result set ["actorPatch", _actorPatch]; _result }], ["init", compileFinal { @@ -258,12 +525,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ }; private _finalOrg = createHashMap; - private _finalAssets = createHashMap; - private _finalFleet = createHashMap; - private _finalMembers = createHashMap; if (_result == "true") then { - _finalOrg = _self call ["fetch", ["org:get", _orgID]]; + _finalOrg = _self call ["loadById", [_orgID]]; ["INFO", format ["Found org for %1", _orgID]] call EFUNC(common,log); } else { ["WARNING", format ["No existing org found for %1, using default org.", _uid]] call EFUNC(common,log); @@ -274,26 +538,6 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ private _entry = createHashMapFromArray [["orgID", _orgID]]; GVAR(IndexRegistry) set [_uid, _entry]; - // private _assets = _self call ["fetch", ["org:assets:get", _orgID]]; - // private _fleet = _self call ["fetch", ["org:fleet:get", _orgID]]; - private _members = _self call ["fetch", ["org:members:get", _orgID]]; - - { - private _key = _x get "uid"; - private _value = _x; - _finalMembers set [_key, _value]; - } forEach _members; - - // { - // private _key = _x get "classname"; - // private _value = _x; - // _finalAssets set [_key, _value]; - // } forEach _assets; - - _finalOrg set ["assets", _finalAssets]; - _finalOrg set ["fleet", _finalFleet]; - _finalOrg set ["members", _finalMembers]; - private _finalOwner = _finalOrg getOrDefault ["owner", ""]; if (_orgID isEqualTo "default" || { _finalOwner isEqualTo _uid }) then { _finalOrg = _self call ["verifyMember", [_finalOrg, _orgID, _uid, _player, _actor]]; diff --git a/lib/repositories/src/org.rs b/lib/repositories/src/org.rs index 2997eea..068c837 100644 --- a/lib/repositories/src/org.rs +++ b/lib/repositories/src/org.rs @@ -147,15 +147,21 @@ impl OrgRepository for RedisOrgRepository { /// Permanently deletes an organization and all associated data from Redis. /// - /// Removes the organization hash and the associated members list. + /// Removes the organization hash and related subordinate keys. /// This operation is irreversible. fn delete(&self, id: &str) -> Result<(), String> { - // Generate Redis key using organization ID - let redis_key = format!("org:{}", id); + let redis_keys = [ + format!("org:{}", id), + format!("org:{}:members", id), + format!("org:{}:assets", id), + format!("org:{}:fleet", id), + ]; - // Delete the organization hash key from Redis - // Note: This does NOT delete member data stored separately - self.client.delete_key(redis_key) + for redis_key in redis_keys { + self.client.delete_key(redis_key)?; + } + + Ok(()) } /// Checks if an organization exists in Redis without retrieving the data. diff --git a/lib/services/src/org.rs b/lib/services/src/org.rs index 58d1257..bead927 100644 --- a/lib/services/src/org.rs +++ b/lib/services/src/org.rs @@ -163,16 +163,7 @@ impl OrgService { /// /// Irreversible operation. Delegates to repository. pub fn delete_org(&self, key: String) -> Result<(), String> { - let redis_key = format!("org:{}", key); - let assets_key = format!("org:{}:assets", key); - let members_key = format!("org:{}:members", key); - - // Delegate deletion to repository layer - self.repository.delete(&redis_key)?; - self.repository.delete(&assets_key)?; - self.repository.delete(&members_key)?; - - Ok(()) + self.repository.delete(&key) } /// Checks if an organization exists in the system.