From 445a114c1cdd41520592fd3a4ed9954ba7704d9a Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Thu, 2 Apr 2026 13:56:49 -0500 Subject: [PATCH] Refactor org and store services around shared payloads - Add shared store payload handling in server and extension code - Remove obsolete org member and treasury service split - Update client repositories and portal UI/store hydration --- .../bank/functions/fnc_initRepository.sqf | 4 - .../org/functions/fnc_initRepository.sqf | 4 - arma/client/addons/org/ui/_site/org-ui.js | 2 +- arma/client/addons/org/ui/src/portal/data.js | 54 +- arma/client/addons/org/ui/src/portal/store.js | 51 +- arma/server/addons/bank/XEH_preInit.sqf | 9 - arma/server/addons/garage/XEH_preInit.sqf | 81 --- .../garage/functions/fnc_initGarageStore.sqf | 81 --- .../garage/functions/fnc_initVGStore.sqf | 148 ---- arma/server/addons/locker/XEH_preInit.sqf | 81 --- .../locker/functions/fnc_initLockerStore.sqf | 133 ---- .../locker/functions/fnc_initVAStore.sqf | 141 ---- .../addons/main/functions/fnc_initStores.sqf | 1 + arma/server/addons/org/XEH_PREP.hpp | 3 +- arma/server/addons/org/XEH_preInit.sqf | 50 -- .../addons/org/functions/fnc_initOrgStore.sqf | 670 ++++++++--------- .../org/functions/fnc_initPayloadBuilder.sqf | 213 ++++++ .../org/functions/fnc_memberService.sqf | 273 ------- .../org/functions/fnc_treasuryService.sqf | 166 ----- .../functions/fnc_initCatalogService.sqf | 41 +- .../store/functions/fnc_initStoreStore.sqf | 208 +++--- arma/server/extension/src/actor.rs | 8 + arma/server/extension/src/bank.rs | 7 + arma/server/extension/src/garage.rs | 8 + arma/server/extension/src/lib.rs | 2 + arma/server/extension/src/locker.rs | 7 + arma/server/extension/src/org.rs | 130 +++- arma/server/extension/src/store.rs | 37 + arma/server/extension/src/transport.rs | 42 ++ arma/server/extension/src/v_garage.rs | 7 + arma/server/extension/src/v_locker.rs | 7 + lib/models/src/lib.rs | 12 +- lib/models/src/org.rs | 111 +++ lib/models/src/store.rs | 72 ++ lib/services/src/lib.rs | 2 + lib/services/src/org.rs | 503 ++++++++++++- lib/services/src/store.rs | 685 ++++++++++++++++++ 37 files changed, 2396 insertions(+), 1658 deletions(-) create mode 100644 arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf delete mode 100644 arma/server/addons/org/functions/fnc_memberService.sqf delete mode 100644 arma/server/addons/org/functions/fnc_treasuryService.sqf create mode 100644 arma/server/extension/src/store.rs create mode 100644 lib/models/src/store.rs create mode 100644 lib/services/src/store.rs diff --git a/arma/client/addons/bank/functions/fnc_initRepository.sqf b/arma/client/addons/bank/functions/fnc_initRepository.sqf index c91ecf2..a3efc61 100644 --- a/arma/client/addons/bank/functions/fnc_initRepository.sqf +++ b/arma/client/addons/bank/functions/fnc_initRepository.sqf @@ -37,10 +37,6 @@ GVAR(BankRepositoryBaseClass) = compileFinal createHashMapFromArray [ ["markLoaded", compileFinal { if !(_self getOrDefault ["isLoaded", false]) then { _self set ["isLoaded", true]; }; true - }], - ["save", compileFinal { - [SRPC(bank,requestSaveBank), [getPlayerUID player]] call CFUNC(serverEvent); - _self set ["lastSave", time]; }] ]; diff --git a/arma/client/addons/org/functions/fnc_initRepository.sqf b/arma/client/addons/org/functions/fnc_initRepository.sqf index f8a6631..1a6ca54 100644 --- a/arma/client/addons/org/functions/fnc_initRepository.sqf +++ b/arma/client/addons/org/functions/fnc_initRepository.sqf @@ -37,10 +37,6 @@ GVAR(OrgRepositoryBaseClass) = compileFinal createHashMapFromArray [ ["markLoaded", compileFinal { if !(_self getOrDefault ["isLoaded", false]) then { _self set ["isLoaded", true]; }; true - }], - ["save", compileFinal { - [SRPC(org,requestSaveOrg), [getPlayerUID player]] call CFUNC(serverEvent); - _self set ["lastSave", time]; }] ]; diff --git a/arma/client/addons/org/ui/_site/org-ui.js b/arma/client/addons/org/ui/_site/org-ui.js index 375a7ff..58c7569 100644 --- a/arma/client/addons/org/ui/_site/org-ui.js +++ b/arma/client/addons/org/ui/_site/org-ui.js @@ -1 +1 @@ -!function(){const e=window.ForgeWebUI,n=window.RegistryApp=window.RegistryApp||{},r=window.OrgPortal=window.OrgPortal||{};n.runtime=e,r.runtime=e,window.AppRuntime=e}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{createSignal:n}=e.runtime;e.store=new class{constructor(){[this.getView,this.setView]=n("home"),[this.getIsAuthenticating,this.setIsAuthenticating]=n(!1),[this.getLoginError,this.setLoginError]=n(""),[this.getIsCreating,this.setIsCreating]=n(!1),[this.getCreateError,this.setCreateError]=n("")}startLogin(){this.setLoginError(""),this.setIsAuthenticating(!0)}startCreate(){this.setCreateError(""),this.setIsCreating(!0)}failLogin(e){this.setIsAuthenticating(!1),this.setLoginError(e||"Authentication failed.")}failCreate(e){this.setIsCreating(!1),this.setCreateError(e||"Organization registration failed.")}hydratePortal(e){const n=window.OrgPortal&&window.OrgPortal.data?window.OrgPortal.data:null,r=window.OrgPortal&&window.OrgPortal.store?window.OrgPortal.store:null,t=e&&e.portalData?e.portalData:null,a=e&&e.session?e.session:null;return!!(n&&"function"==typeof n.applyLoginPayload&&r&&"function"==typeof r.hydrateFromPayload&&t&&a)&&(n.applyLoginPayload(e),r.hydrateFromPayload(e),!0)}completeLogin(e){this.hydratePortal(e)?(this.setLoginError(""),this.setIsAuthenticating(!1),this.setView("portal")):this.failLogin("Login response was missing portal data.")}completeCreate(e){this.hydratePortal(e)?(this.setCreateError(""),this.setIsCreating(!1),this.setView("portal")):this.failCreate("Organization registration response was missing portal data.")}}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},n=e.store,r=window.ForgeWebUI.createBridge({closeEvent:"org::close",globalName:"ForgeBridge",readyEvent:"org::ready"});function t(e,n){return r.send(e,n)}r.on("org::login::success",e=>{n.completeLogin(e)}),r.on("org::login::failure",e=>{n.failLogin(e.message||"Authentication failed.")}),r.on("org::create::success",e=>{n.completeCreate(e)}),r.on("org::create::failure",e=>{n.failCreate(e.message||"Organization registration failed.")}),r.on("org::sync",e=>{n&&"function"==typeof n.hydratePortal&&n.hydratePortal(e)}),r.on("org::credit::success",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("success",e.message||"Credit line assigned.")}),r.on("org::credit::failure",e=>{const n=window.OrgPortal;n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to assign credit line.")}),r.on("org::member::creditUpdated",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setCreditLines(n=>{const r={amount:e.amount||0,member:e.memberName||"",uid:e.memberUid||""},t=n.findIndex(e=>e.uid===r.uid);return-1===t?[...n,r]:n.map((e,n)=>n===t?r:e)})}),r.on("org::disband::success",()=>{const e=window.OrgPortal;e&&e.store&&(e.store.setModal(null),e.store.setOrgDisbanded(!0))}),r.on("org::disband::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Organization disbanding failed.")}),r.on("org::leave::success",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"You have left the organization."),n.setView("home")}),r.on("org::leave::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to leave the organization.")}),r.on("org::portal::revoked",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"Organization access is no longer available."),n.setView("home")}),e.bridge={close:r.close,ready:r.ready,receive:r.receive,requestLogin:function(e){n.startLogin(),t("org::login::request",e)||n.failLogin("Arma login bridge is unavailable.")},requestCreateOrg:function(e){n.startCreate(),t("org::create::request",e)||n.failCreate("Arma registration bridge is unavailable.")},requestDisbandOrg:function(){if(t("org::disband::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma disband bridge is unavailable.")},requestLeaveOrg:function(){if(t("org::leave::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma leave bridge is unavailable.")},requestCreditLine:function(e){if(t("org::credit::request",e))return!0;const n=window.OrgPortal;return n&&n.actions&&n.actions.showTreasuryNotice("error","Arma credit line bridge is unavailable."),!1},sendEvent:t}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},n={type:"Organization",status:"Operational",headquarters:"ArmA Verse"};function r(e){return JSON.parse(JSON.stringify(e))}function t(e,n){Object.keys(e).forEach(n=>delete e[n]),Object.assign(e,r(n))}function a(e,n){e.splice(0,e.length,...r(n))}e.data={portalData:{org:Object.assign({name:"",tag:"",owner:"",ownerUid:"",isDefault:!1},n),funds:0,reputation:0,creditLines:[],members:[],fleet:[],assets:[],activity:[],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."}]},session:{actorName:"",actorUid:"",role:"",ceo:!1},applyLoginPayload(e){t(this.portalData.org,Object.assign({},e.portalData.org||{},n)),this.portalData.funds=e.portalData.funds||0,this.portalData.reputation=e.portalData.reputation||0,a(this.portalData.creditLines,e.portalData.creditLines||[]),a(this.portalData.members,e.portalData.members||[]),a(this.portalData.fleet,e.portalData.fleet||[]),a(this.portalData.assets,e.portalData.assets||[]),a(this.portalData.activity,e.portalData.activity||[]),a(this.portalData.roadmap,e.portalData.roadmap||[]),t(this.session,e.session||{})}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{createSignal:n}=window.RegistryApp.runtime,{portalData:r}=e.data;e.store=new class{constructor(){[this.getFunds,this.setFunds]=n(r.funds),[this.getReputation,this.setReputation]=n(r.reputation),[this.getMembers,this.setMembers]=n([...r.members]),[this.getCreditLines,this.setCreditLines]=n([...r.creditLines]),[this.getFleet,this.setFleet]=n([...r.fleet]),[this.getAssets,this.setAssets]=n([...r.assets]),[this.getActivity,this.setActivity]=n([...r.activity]),[this.getTreasuryNotice,this.setTreasuryNotice]=n({type:"",text:""}),[this.getModal,this.setModal]=n(null),[this.getOrgDisbanded,this.setOrgDisbanded]=n(!1)}hydrateFromPayload(e){const n=e.portalData||{};this.setFunds(n.funds||0),this.setReputation(n.reputation||0),this.setMembers([...n.members||[]]),this.setCreditLines([...n.creditLines||[]]),this.setFleet([...n.fleet||[]]),this.setAssets([...n.assets||[]]),this.setActivity([...n.activity||[]])}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n,session:r}=e.data;e.getters=new class{formatCurrency(e){return"$"+Number(e||0).toLocaleString()}formatVehicleType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatAssetType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatDisplayName(e){return e?String(e).trim().split(/\s+/).map(e=>e?e.charAt(0).toUpperCase()+e.slice(1).toLowerCase():"").join(" "):""}getAssetReadiness(){const r=e.store?e.store.getFleet():n.fleet;if(0===r.length)return null;const t=r.reduce((e,n)=>e+(100-parseInt(n.damage,10)),0);return Math.round(t/r.length)}getNormalizedRole(){return String(r.role||"").trim().toUpperCase()}isDefaultOrg(){return!0===n.org.isDefault||"DEFAULT"===String(n.org.tag||"").trim().toUpperCase()}isOrgOwner(){const e=String(n.org.ownerUid||n.org.owner||"").trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return e&&t?t===e:String(r.actorName||"").trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isSessionCeo(){return!0===r.ceo}isOrgLeaderOrCeo(){return this.isOrgOwner()||"LEADER"===this.getNormalizedRole()||this.isDefaultOrg()&&this.isSessionCeo()}canManageMembers(){return this.isOrgLeaderOrCeo()}canManageTreasury(){return this.isOrgLeaderOrCeo()}canDisbandOrg(){return this.isOrgOwner()&&!this.isDefaultOrg()}canLeaveOrg(){return!this.isDefaultOrg()&&!this.isOrgOwner()}getMemberName(e){return String(e&&"object"==typeof e?e.name||"":e||"")}getMemberUid(e){return e&&"object"==typeof e?String(e.uid||""):""}isOwnerMember(e){return this.getMemberName(e).trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isCurrentMember(e){const n=this.getMemberUid(e).trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return n&&t?n===t:this.getMemberName(e).trim().toLowerCase()===String(r.actorName||"").trim().toLowerCase()}isProtectedMember(e){return this.isOwnerMember(e)||this.isCurrentMember(e)}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n}=e.data,r=e.store,t=e.getters,a=window.RegistryApp.store;e.actions=new class{constructor(){this.treasuryNoticeTimer=null}showTreasuryNotice(e,n){r.setTreasuryNotice({type:e,text:n}),this.treasuryNoticeTimer&&clearTimeout(this.treasuryNoticeTimer),this.treasuryNoticeTimer=setTimeout(()=>{r.setTreasuryNotice({type:"",text:""}),this.treasuryNoticeTimer=null},3500)}parseAmount(e){const n=Number(e);return Number.isFinite(n)?Math.round(n):0}getInputValue(e){const n=document.getElementById(e);return n?n.value:""}closePortal(){const e=window.RegistryApp?window.RegistryApp.bridge:null;e&&"function"==typeof e.close?e.close({}):a&&a.setView("home")}openModal(e){"payroll"!==e&&"transfer"!==e&&"credit"!==e||t.canManageTreasury()?("disband"!==e||t.canDisbandOrg())&&("leave"!==e||t.canLeaveOrg())&&r.setModal({type:e}):this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions.")}closeModal(){r.setModal(null)}removeMember(e){if(!t.canManageMembers())return!1;if(t.isProtectedMember(e))return!1;const n=t.getMemberUid(e),a=t.getMemberName(e);return r.setMembers(e=>e.filter(e=>n?e.uid!==n:e.name!==a)),r.setCreditLines(e=>e.filter(e=>n?e.uid!==n:e.member!==a)),!0}disbandOrganization(){if(!t.canDisbandOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestDisbandOrg?(this.closeModal(),e.requestDisbandOrg(),!0):(this.showTreasuryNotice("error","Disband bridge is unavailable."),!1)}leaveOrganization(){if(!t.canLeaveOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestLeaveOrg?(this.closeModal(),e.requestLeaveOrg(),!0):(this.showTreasuryNotice("error","Leave bridge is unavailable."),!1)}runPayroll(e){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const n=r.getMembers(),a=r.getFunds();if(0===n.length)return this.showTreasuryNotice("error","No members available for payroll."),!1;if(e<=0)return this.showTreasuryNotice("error","Enter a valid payroll amount."),!1;const o=e*n.length;return o>a?(this.showTreasuryNotice("error","Insufficient org funds for payroll."),!1):(r.setFunds(a-o),this.showTreasuryNotice("success",`Payroll sent to ${n.length} members for ${t.formatCurrency(o)}.`),!0)}sendFundsToMember(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const a=r.getFunds();return e?n<=0?(this.showTreasuryNotice("error","Enter a valid transfer amount."),!1):n>a?(this.showTreasuryNotice("error","Insufficient org funds for this transfer."),!1):(r.setFunds(a-n),this.showTreasuryNotice("success",`${t.formatCurrency(n)} sent to ${e}.`),!0):(this.showTreasuryNotice("error","Select a member to receive funds."),!1)}grantCreditLine(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;if(!e)return this.showTreasuryNotice("error","Select a member for the credit line."),!1;if(n<=0)return this.showTreasuryNotice("error","Enter a valid credit line amount."),!1;const a=r.getMembers().find(n=>t.getMemberUid(n)===e),o=a?t.getMemberName(a):"";if(!o)return this.showTreasuryNotice("error","Selected member was not found in the organization roster."),!1;const i=window.RegistryApp?window.RegistryApp.bridge:null;return i&&"function"==typeof i.requestCreditLine?i.requestCreditLine({memberUid:e,memberName:o,amount:n}):(this.showTreasuryNotice("error","Credit line bridge is unavailable."),!1)}}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-navbar",o=`[${a}]`,i=`\n${o} {\n background: var(--bg-surface);\n border-bottom: 1px solid var(--border);\n box-shadow: var(--shadow);\n}\n\n${o} .app-navbar-inner {\n display: flex;\n justify-content: space-between;\n align-items: center;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 1rem 2rem;\n box-sizing: border-box;\n}\n\n${o} .app-navbar-brand {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n}\n\n${o} .app-navbar-kicker {\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-navbar-title {\n font-size: 1.25rem;\n font-weight: 700;\n color: var(--primary-hover);\n letter-spacing: -0.025em;\n}\n\n${o} .app-navbar-actions {\n display: flex;\n align-items: center;\n gap: 1.5rem;\n}\n\n${o} .app-navbar-view {\n font-size: 0.8rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-close-btn {\n background: transparent;\n color: var(--text-muted);\n border: 1px solid var(--border);\n padding: 0.5rem 1rem;\n font-size: 0.85rem;\n}\n\n${o} .app-close-btn:hover {\n background: var(--bg-surface-hover);\n color: var(--primary-hover);\n border-color: var(--primary);\n transform: none;\n box-shadow: none;\n}\n\n@media (max-width: 960px) {\n ${o} .app-navbar-inner {\n flex-direction: column;\n align-items: flex-start;\n padding: 1rem 1.5rem;\n }\n\n ${o} .app-navbar-actions {\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Navbar=function({kicker:e="ORBIS",title:n="",viewLabel:o="",actionLabel:s="",onAction:l=null}){return t("shared-navbar",i),r("nav",{className:"app-navbar",[a]:""},r("div",{className:"app-navbar-inner"},r("div",{className:"app-navbar-brand"},r("span",{className:"app-navbar-kicker"},e),r("span",{className:"app-navbar-title"},n)),r("div",{className:"app-navbar-actions"},r("span",{className:"app-navbar-view"},o),s&&"function"==typeof l?r("button",{type:"button",className:"app-close-btn",onClick:l},s):null)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Header=function({title:e,subtitle:n="Organization Registration & Management Portal",onTitleClick:t=null}){return r("div",{className:"header"},r("h1",{style:{cursor:t?"pointer":"default"},onClick:t},e),r("p",null,n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.OrgPortal=window.OrgPortal||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Hero=function({className:e="",kicker:n="",title:t="",subtitle:a="",meta:o=""}){const i=["card org-panel org-span-12 org-page-header",e].filter(Boolean).join(" ");return r("section",{className:i},r("div",{className:"org-page-heading"},r("span",{className:"org-page-kicker"},n),r("h1",{className:"org-page-title"},t),r("p",{className:"org-page-subtitle"},a),r("span",{className:"org-page-meta"},o)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Footer=function({sections:e=[]}){return r("div",{className:"footer"},r("div",{className:"wrapper"},...e.map(e=>r("div",null,r("h3",null,e.title),r("ul",{style:{listStyleType:"none",padding:0}},...(e.items||[]).map(e=>r("li",null,e)))))))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-modal",o=`[${a}]`,i=`\n${o} {\n position: fixed;\n inset: 0;\n background: rgb(15 23 42 / 0.38);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1.5rem;\n z-index: 20;\n}\n\n${o} .app-modal-card {\n width: min(100%, 30rem);\n margin-bottom: 0;\n text-align: left;\n}\n\n${o} .app-modal-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1rem;\n}\n\n${o} .app-modal-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .app-modal-close {\n width: 2.25rem;\n height: 2.25rem;\n padding: 0;\n background: var(--bg-surface);\n color: var(--text-main);\n border: 1px solid var(--border);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-close:hover {\n background: var(--bg-surface-hover);\n color: var(--text-main);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n${o} .app-modal-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${o} .app-modal-form input,\n${o} .app-modal-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\n\n${o} .app-modal-form input:focus,\n${o} .app-modal-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12);\n}\n\n${o} .app-modal-form input:disabled,\n${o} .app-modal-form select:disabled {\n background: #f1f5f9;\n color: var(--text-muted);\n cursor: not-allowed;\n}\n\n${o} .app-modal-actions {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.75rem;\n margin-top: 0.5rem;\n}\n\n${o} .app-modal-actions button + button,\n${o} .app-modal-danger-actions button + button {\n margin-left: 0;\n}\n\n${o} .app-modal-danger {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid #fecaca;\n border-radius: var(--radius);\n background: #fff1f2;\n align-items: flex-start;\n}\n\n${o} .app-modal-danger p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .app-modal-danger-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem;\n}\n\n@media (max-width: 960px) {\n ${o} .app-modal-head,\n ${o} .app-modal-danger {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Modal=function({title:e="",body:n=null,onClose:o=null}){return t("shared-modal",i),r("div",{className:"app-modal-backdrop",[a]:"",onClick:e=>{e.target===e.currentTarget&&o&&o()}},r("div",{className:"card app-modal-card"},r("div",{className:"app-modal-head"},r("div",null,r("h2",{className:"app-modal-title"},e)),r("button",{type:"button",className:"app-modal-close",onClick:o,"aria-label":"Close dialog"},"x")),n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-panel-card",o=`[${a}]`,i=`\n${o} {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n\n${o} .org-panel-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${o} .org-panel-body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-height: 0;\n}\n\n${o} .org-eyebrow {\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-muted);\n margin-bottom: 0.4rem;\n}\n\n${o} .org-panel-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .org-panel-subtitle {\n margin: 0.35rem 0 0;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n@media (max-width: 960px) {\n ${o} .org-panel-head {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.PanelCard=function({className:e="",eyebrow:n="",title:o="",subtitle:s="",headerExtras:l=null,body:d=null,rootProps:c={}}){const m=["card org-panel",e].filter(Boolean).join(" ");return t("shared-panel-card",i),r("section",{className:m,[a]:"",...c},r("div",{className:"org-panel-head"},r("div",null,n?r("div",{className:"org-eyebrow"},n):null,r("h2",{className:"org-panel-title"},o),s?r("p",{className:"org-panel-subtitle"},s):null),l),r("div",{className:"org-panel-body"},d))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-metric-card",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.45rem;\n padding: 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n}\n\n${a}:nth-child(4n + 2),\n${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%);\n border-color: rgb(100 116 139 / 0.35);\n box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);\n}\n\n${a} .org-metric-label {\n font-size: 0.76rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--text-muted);\n}\n\n${a} .org-metric-value {\n font-size: 1.8rem;\n color: var(--primary-hover);\n line-height: 1.1;\n}\n\n${a}:nth-child(4n + 2) .org-metric-value,\n${a}:nth-child(4n + 3) .org-metric-value {\n color: #334155;\n}\n\n${a} .org-metric-note {\n color: var(--text-muted);\n font-size: 0.9rem;\n}\n\n@media (max-width: 960px) {\n ${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n border-color: var(--border);\n box-shadow: none;\n }\n\n ${a}:nth-child(4n + 3) .org-metric-value {\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MetricCard=function(e,a,i){return r("portal-metric-card",o),n("div",{className:"org-metric-card",[t]:""},n("span",{className:"org-metric-label"},e),n("strong",{className:"org-metric-value"},a),n("span",{className:"org-metric-note"},i))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-simple-stat",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.2rem;\n min-width: 90px;\n}\n\n${a} .org-simple-label {\n font-size: 0.72rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${a} .org-simple-value {\n font-size: 0.95rem;\n color: var(--text-main);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.SimpleStat=function(e,a){return r("portal-simple-stat",o),n("div",{className:"org-simple-stat",[t]:""},n("span",{className:"org-simple-label"},e),n("strong",{className:"org-simple-value"},a))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.store,o=e.getters,i="data-ui-overview-card",s=`[${i}]`,l=`\n${s} .org-hero-grid {\n display: grid;\n grid-template-columns: 1.3fr 1fr;\n gap: 1.5rem;\n align-items: start;\n}\n\n${s} .org-summary {\n margin: 0;\n font-size: 1.05rem;\n color: var(--text-main);\n}\n\n${s} .org-meta-row {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 1rem;\n margin-top: 1.5rem;\n}\n\n${s} .org-meta-item {\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-meta-item:nth-child(even) {\n background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${s} .org-meta-value {\n font-size: 1rem;\n font-weight: 600;\n color: var(--primary-hover);\n}\n\n${s} .org-metric-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${s} .org-hero-grid,\n ${s} .org-meta-row,\n ${s} .org-metric-grid {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.OverviewCard=function(){const s=e.componentFns.MetricCard,d=window.SharedUI.componentFns.PanelCard,c=o.getAssetReadiness(),m=t.org.headquarters||"ArmA Verse",g=a.getAssets().length,p=a.getFleet().length,u=a.getFunds(),f=a.getMembers().length,b=a.getReputation();return r("portal-overview-card",l),d({className:"org-span-12",eyebrow:t.org.tag,title:"Organization Overview",rootProps:{[i]:""},body:n("div",{className:"org-hero-grid"},n("div",{className:"org-hero-copy"},n("p",{className:"org-summary"},t.org.type," operating from ",m,". Treasury, fleet status, inventory, and roster management are surfaced here first."),n("div",{className:"org-meta-row"},n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Director"),n("span",{className:"org-meta-value"},o.formatDisplayName(t.org.owner))),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Active Members"),n("span",{className:"org-meta-value"},`${f} total`)),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Fleet Readiness"),n("span",{className:"org-meta-value"},null===c?"N/A":`${c}%`)))),n("div",{className:"org-metric-grid"},s("Org Funds",o.formatCurrency(u),"Organization treasury balance"),s("Reputation",b,"Organization standing"),s("Asset Lines",g,"Tracked supply and equipment entries"),s("Fleet Vehicles",p,"Tracked air, ground, and naval vehicles")))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-fleet-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FleetCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getFleet();return r("portal-fleet-card",s),t({className:"org-scroll-panel org-span-7",title:"Fleet",subtitle:"Individual vehicles with type, status, and overall damage.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatVehicleType(e.type)),i("Status",e.status),i("Damage",e.damage)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r,createSignal:t}=e.runtime,{portalData:a}=e.data,o=e.store,i=e.getters,s=e.actions,l="data-ui-treasury-card",d=`[${l}]`,[c,m]=t("overview"),[g,p]=t(!1),u=`\n${d} .org-treasury-menu {\n position: relative;\n}\n\n${d} .org-menu-btn {\n width: 2.75rem;\n height: 2.75rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 1px solid var(--border);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n${d} .org-menu-btn:hover {\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.65);\n}\n\n${d} .org-menu-btn svg {\n width: 1.1rem;\n height: 1.1rem;\n}\n\n${d} .org-menu-dropdown {\n position: absolute;\n top: calc(100% + 0.6rem);\n right: 0;\n min-width: 10.5rem;\n padding: 0.45rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.12);\n display: flex;\n flex-direction: column;\n gap: 0.35rem;\n z-index: 5;\n}\n\n${d} .org-menu-option + .org-menu-option {\n margin-left: 0;\n}\n\n${d} .org-menu-option {\n width: 100%;\n justify-content: flex-start;\n background: transparent;\n color: var(--text-main);\n border: 1px solid transparent;\n}\n\n${d} .org-menu-option:hover {\n background: #f8fafc;\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-menu-option.is-active {\n background: rgb(226 232 240 / 0.7);\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-finance-meta {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${d} .org-finance-meta > div {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n}\n\n${d} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-action-grid {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n margin-bottom: 1rem;\n}\n\n${d} .org-action-grid button + button {\n margin-left: 0;\n}\n\n${d} .org-action-grid button {\n width: 100%;\n}\n\n${d} .org-access-note {\n margin: 0 0 1rem;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n${d} .org-credit-summary {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n padding: 0.85rem 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-summary strong {\n font-size: 1rem;\n}\n\n${d} .org-credit-summary span:last-child {\n font-size: 0.92rem;\n line-height: 1.45;\n}\n\n${d} .org-credit-lines-list {\n display: flex;\n flex-direction: column;\n gap: 0.85rem;\n}\n\n${d} .org-treasury-body {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: 1rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${d} .org-credit-line-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-line-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${d} .org-credit-line-member {\n display: flex;\n flex-direction: column;\n gap: 0.3rem;\n}\n\n${d} .org-credit-line-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-credit-line-empty {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n@media (max-width: 960px) {\n ${d} .org-finance-meta {\n grid-template-columns: 1fr;\n }\n\n ${d} .org-credit-line-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.TreasuryCard=function(){const e=window.SharedUI.componentFns.PanelCard,t=o.getCreditLines(),a=o.getReputation(),d=i.canManageTreasury(),f=c(),b=g(),w=1===t.length?"1 active credit line":`${t.length} active credit lines`;return r("portal-treasury-card",u),e({className:"org-scroll-panel org-span-5",title:"Treasury",subtitle:"Organization funds, reputation and payouts.",headerExtras:n("div",{className:"org-treasury-menu"},n("button",{type:"button",className:"org-menu-btn",title:"Treasury views","aria-label":"Treasury views",onClick:()=>p(e=>!e)},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("line",{x1:"4",y1:"7",x2:"20",y2:"7"}),n("line",{x1:"4",y1:"12",x2:"20",y2:"12"}),n("line",{x1:"4",y1:"17",x2:"20",y2:"17"}))),b?n("div",{className:"org-menu-dropdown"},n("button",{type:"button",className:"overview"===f?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("overview"),p(!1)}},"Overview"),n("button",{type:"button",className:"credit"===f?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("credit"),p(!1)}},"Credit Lines")):null),rootProps:{[l]:""},body:n("div",{className:"org-treasury-body"},"credit"===f?t.length>0?n("div",{className:"org-credit-lines-list"},...t.map(e=>n("article",{className:"org-credit-line-row"},n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Member"),n("strong",null,e.member)),n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Amount"),n("strong",null,i.formatCurrency(e.amount)))))):n("div",{className:"org-credit-line-empty"},"No active credit lines."):n("div",null,n("div",{className:"org-finance-meta"},n("div",null,n("span",{className:"org-meta-label"},"Funds"),n("strong",null,i.formatCurrency(o.getFunds()))),n("div",null,n("span",{className:"org-meta-label"},"Reputation"),n("strong",null,`${a}`))),d?n("div",{className:"org-action-grid"},n("button",{type:"button",onClick:()=>s.openModal("payroll")},"Run Payroll"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("transfer")},"Send Funds"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("credit")},"Credit Line")):n("p",{className:"org-access-note"},"Only the organization leader or CEO can manage treasury actions."),n("div",{className:"org-credit-summary"},n("span",{className:"org-meta-label"},"Credit Line Status"),n("strong",null,w),n("span",null,t.length>0?"Open the Credit Lines tab to review assigned members and amounts.":"Assign a credit line to create the first approved member limit."))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-assets-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.AssetsCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getAssets();return r("portal-assets-card",s),t({className:"org-scroll-panel org-span-7",title:"Assets",subtitle:"Inventory supplies and equipment with quantity totals.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatAssetType(e.type)),i("Quantity",e.quantity)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.getters,o=e.actions,i="data-ui-members-card",s=`[${i}]`,l=`\n${s} .org-name-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${s} .org-name-row {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-name-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-name-row button {\n margin-left: auto;\n}\n\n@media (max-width: 960px) {\n ${s} .org-name-row {\n flex-direction: column;\n align-items: flex-start;\n }\n\n ${s} .org-name-row button {\n margin-left: 0;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MembersCard=function(){const e=window.SharedUI.componentFns.PanelCard,s=t.getMembers(),d=a.canManageMembers();return r("portal-members-card",l),e({className:"org-scroll-panel org-span-5",title:"Members",subtitle:"Current roster listing. The organization owner and your own member entry cannot be removed.",rootProps:{[i]:""},body:n("div",{className:"org-name-list"},...s.map(e=>{const r=d&&!a.isProtectedMember(e);return n("article",{className:"org-name-row"},n("strong",null,e.name),r?n("button",{type:"button",className:"org-danger-btn org-icon-btn",title:`Remove ${e.name}`,"aria-label":`Remove ${e.name}`,onClick:()=>o.removeMember(e)},n("svg",{className:"org-icon",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("path",{d:"M9 3h6"}),n("path",{d:"M4 7h16"}),n("path",{d:"M6 7l1 13h10l1-13"}),n("path",{d:"M10 11v6"}),n("path",{d:"M14 11v6"}))):null)}))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a="data-ui-activity-card",o=`[${a}]`,i=`\n${o} .org-activity-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-activity-row {\n padding: 1rem;\n border: 1px solid var(--border);\n border-left: 3px solid #94a3b8;\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-activity-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n border-left-color: #64748b;\n}\n\n${o} .org-activity-row p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-activity-time {\n display: inline-block;\n margin-bottom: 0.35rem;\n color: var(--text-muted);\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.ActivityCard=function(){const t=window.SharedUI.componentFns.PanelCard,o=e.store.getActivity();return r("portal-activity-card",i),t({className:"org-scroll-panel org-span-6",title:"Command Feed",subtitle:"Recent organization-level actions and updates.",rootProps:{[a]:""},body:n("div",{className:"org-activity-list"},...o.map(e=>n("article",{className:"org-activity-row"},n("span",{className:"org-activity-time"},e.time),n("p",null,e.text))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-future-card",a=[{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."}],o=`[${t}]`,i=`\n${o} .org-roadmap-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-roadmap-card {\n padding: 1rem;\n display: flex;\n flex-direction: column;\n gap: 0.7rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2),\n${o} .org-roadmap-card:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(100 116 139 / 0.4);\n}\n\n${o} .org-roadmap-card p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-list-tag {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0.2rem 0.55rem;\n border-radius: 999px;\n font-size: 0.72rem;\n font-weight: 700;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n background: #e2e8f0;\n color: var(--primary-hover);\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2) .org-list-tag,\n${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #cbd5e1;\n color: #1e293b;\n}\n\n@media (max-width: 960px) {\n ${o} .org-roadmap-grid {\n grid-template-columns: 1fr;\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) {\n background: #f8fafc;\n border-color: var(--border);\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #e2e8f0;\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FutureCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-future-card",i),e({className:"org-scroll-panel org-span-6",title:"Expansion Slots",subtitle:"Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.",rootProps:{[t]:""},body:n("div",{className:"org-roadmap-grid"},...a.map(e=>n("article",{className:"org-roadmap-card"},n("span",{className:"org-list-tag"},e.status),n("strong",null,e.name),n("p",null,e.detail))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.getters,a=e.actions,o="data-ui-danger-card",i=`[${o}]`,s=`\n${i} {\n border-color: #fecaca;\n background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%);\n}\n\n${i} .org-danger-copy {\n margin-bottom: 1rem;\n}\n\n${i} .org-danger-copy strong,\n${i} .org-danger-copy p {\n display: block;\n}\n\n${i} .org-danger-copy p {\n margin: 0.4rem 0 0;\n color: var(--text-muted);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.DangerCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-danger-card",s),t.canDisbandOrg()?e({className:"org-span-12 org-danger-panel",title:"Organization Controls",subtitle:"Leader-only actions for membership and permanent organization removal.",rootProps:{[o]:""},body:n("div",null,n("div",{className:"org-danger-copy"},n("strong",null,"Disband organization"),n("p",null,"This removes the organization and revokes access to the portal for all members.")),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.openModal("disband")},"Disband Organization"))}):null}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=e.store,a=e.actions;e.componentFns=e.componentFns||{},e.componentFns.ModalLayer=function(){const e=window.SharedUI.componentFns.Modal,o=t.getModal();if(!o)return null;const i=t.getMembers(),s=0===i.length?{disabled:!0}:{};let l="",d=null;return"payroll"===o.type?(l="Run Payroll",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Amount Per Member"),n("input",{id:"treasury-payroll-amount",type:"number",min:"1",placeholder:"500",autofocus:"true"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",onClick:()=>{a.runPayroll(a.parseAmount(a.getInputValue("treasury-payroll-amount")))&&a.closeModal()}},"Run Payroll")))):"transfer"===o.type?(l="Send Funds",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-transfer-member",...s},...i.map(e=>n("option",{value:e.name},e.name)))),n("div",null,n("label",null,"Amount"),n("input",{id:"treasury-transfer-amount",type:"number",min:"1",placeholder:"1500"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...s,onClick:()=>{a.sendFundsToMember(String(a.getInputValue("treasury-transfer-member")||""),a.parseAmount(a.getInputValue("treasury-transfer-amount")))&&a.closeModal()}},"Send Funds")))):"credit"===o.type?(l="Assign Credit Line",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-credit-member",...s},...i.map(e=>n("option",{value:e.uid},e.name)))),n("div",null,n("label",null,"Credit Amount"),n("input",{id:"treasury-credit-amount",type:"number",min:"1",placeholder:"5000"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...s,onClick:()=>{a.grantCreditLine(String(a.getInputValue("treasury-credit-member")||""),a.parseAmount(a.getInputValue("treasury-credit-amount")))&&a.closeModal()}},"Assign Credit Line")))):"disband"===o.type?(l="Disband Organization",d=n("div",{className:"app-modal-danger"},n("p",null,"This action is permanent. Disband ",r.org.name,"?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.disbandOrganization()},"Confirm Disband")))):"leave"===o.type&&(l="Leave Organization",d=n("div",{className:"app-modal-danger"},n("p",null,"Leave ",r.org.name," and return to the default organization?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.leaveOrganization()},"Confirm Leave")))),e({title:l,body:d,onClose:()=>a.closeModal()})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=window.RegistryApp.store;e.componentFns=e.componentFns||{},e.componentFns.DisbandedView=function(){return(0,window.SharedUI.componentFns.PanelCard)({className:"org-span-12 org-empty-state",eyebrow:"Organization Removed",title:r.org.name,body:n("div",null,n("p",{className:"org-summary"},"This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview."),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>t.setView("home")},"Return to Registry"))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t,session:a}=e.data,o=e.store,i="[data-ui-portal-view]";r("portal-view",`\n ${i} {\n --org-row-card-max-height: 36rem;\n }\n\n ${i} .org-toast-stack {\n position: fixed;\n top: 1.5rem;\n right: 2rem;\n z-index: 20;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n pointer-events: none;\n }\n\n ${i} .org-toast {\n max-width: 24rem;\n padding: 0.9rem 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.14);\n font-size: 0.92rem;\n pointer-events: auto;\n }\n\n ${i} .org-toast.is-success {\n background: #ecfdf5;\n border-color: #bbf7d0;\n color: #166534;\n }\n\n ${i} .org-toast.is-error {\n background: #fef2f2;\n border-color: #fecaca;\n color: #991b1b;\n }\n\n ${i} .org-dashboard-grid {\n display: grid;\n grid-template-columns: repeat(12, minmax(0, 1fr));\n gap: 1.5rem;\n align-items: stretch;\n }\n\n ${i} .org-panel {\n margin-bottom: 0;\n text-align: left;\n }\n\n ${i} .org-scroll-panel {\n display: flex;\n flex-direction: column;\n min-height: 0;\n max-height: var(--org-row-card-max-height);\n overflow: hidden;\n }\n\n ${i} .org-island-root {\n display: flex;\n align-self: stretch;\n min-height: 0;\n min-width: 0;\n }\n\n ${i} .org-island-root > .org-panel {\n height: 100%;\n width: 100%;\n }\n\n ${i} .org-span-12 {\n grid-column: span 12;\n }\n\n ${i} .org-span-7 {\n grid-column: span 7;\n }\n\n ${i} .org-span-6 {\n grid-column: span 6;\n }\n\n ${i} .org-span-5 {\n grid-column: span 5;\n }\n\n @media (max-width: 960px) {\n ${i} .org-toast-stack {\n top: 1rem;\n right: 1rem;\n left: 1rem;\n }\n\n ${i} .org-toast {\n max-width: none;\n }\n\n ${i} .org-span-12,\n ${i} .org-span-7,\n ${i} .org-span-6,\n ${i} .org-span-5 {\n grid-column: span 12;\n }\n\n ${i} .org-scroll-panel {\n max-height: none;\n }\n\n }\n `),e.components=e.components||{},e.componentFns=e.componentFns||{},e.componentFns.TreasuryNoticeLayer=function(){const e=o.getTreasuryNotice();return e.text?n("div",{className:"org-toast-stack"},n("div",{className:"error"===e.type?"org-toast is-error":"org-toast is-success"},e.text)):null},e.components.App=function(){const r=window.SharedUI.componentFns.Hero,i=window.SharedUI.componentFns.Footer,s=e.componentFns.FutureCard,l=e.componentFns.DangerCard,d=e.componentFns.DisbandedView,c=[{title:"Organization Controls",items:["Roster Management","Fleet Assignment","Treasury Permissions","Asset Registry"]},{title:"Planned Extensions",items:["Contracts Board","Diplomacy Layer","Procurement Queue","Reputation History"]}];return o.getOrgDisbanded()?n("main",{"data-ui-portal-view":""},n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),d())),n("div",{id:"org-portal-modal-root"}),i({sections:c})):n("main",{"data-ui-portal-view":""},n("div",{id:"org-portal-toast-root"}),n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),n("div",{className:"org-island-root org-span-12",id:"org-overview-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-fleet-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-treasury-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-members-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-assets-card-root"}),n("div",{className:"org-island-root org-span-6",id:"org-activity-card-root"}),s(),l())),n("div",{id:"org-portal-modal-root"}),i({sections:c}))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-registration-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n align-items: center;\n width: 100%;\n}\n\n${i} .info-panel {\n text-align: left;\n padding: 1rem;\n}\n\n${i} .create-feature-list {\n text-align: left;\n margin-top: 1.5rem;\n list-style-type: none;\n padding: 0;\n}\n\n${i} .create-feature-item {\n margin-bottom: 0.5rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n${i} .create-feature-icon {\n width: 1.2rem;\n height: 1.2rem;\n flex-shrink: 0;\n}\n\n${i} .price-tag {\n margin-top: 2rem;\n padding: 1rem;\n background: var(--bg-app);\n border-radius: var(--radius);\n border: 1px solid var(--border);\n}\n\n${i} .price-label {\n display: block;\n font-size: 0.9rem;\n color: var(--text-muted);\n}\n\n${i} .price-value {\n display: block;\n font-size: 2rem;\n font-weight: 700;\n color: var(--primary);\n}\n\n${i} .form-panel {\n margin: 0;\n}\n\n${i} .app-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n text-align: left;\n}\n\n${i} .app-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${i} .app-form input,\n${i} .app-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s;\n}\n\n${i} .app-form input:focus,\n${i} .app-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1);\n}\n\n${i} .form-actions {\n margin-top: 1rem;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n align-items: center;\n}\n\n${i} .submit-btn {\n width: 100%;\n}\n\n${i} .cancel-link {\n font-size: 0.9rem;\n color: var(--text-muted);\n cursor: pointer;\n text-decoration: underline;\n}\n\n${i} .cancel-link:hover {\n color: var(--primary);\n}\n\n${i} .form-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n}\n\n${i} .form-feedback.is-error {\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.RegistrationView=function(){const e=t.getIsCreating(),i=t.getCreateError();r("main-registration-view",s);return n("div",{className:"split-container",[o]:""},n("div",{className:"info-panel"},n("h2",null,"Registration Details"),n("p",null,"Complete the form to add your organization to the Global Organization Registry."),n("ul",{className:"create-feature-list"},n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Official Organization Designator"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Secure Comms Channel"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Deployment Roster Access"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"After-Action Report Tools")),n("div",{className:"price-tag"},n("span",{className:"price-label"},"Registration Fee"),n("span",{className:"price-value"},"$50,000"))),n("div",{className:"form-panel card"},n("h2",null,"Organization Registration"),n("div",{className:"app-form"},n("div",null,n("label",null,"Organization Name"),n("input",{id:"org-create-name",type:"text",placeholder:"e.g. Task Force 141"})),n("div",null,n("label",null,"Organization Type"),n("select",{id:"org-create-type"},n("option",{value:"infantry"},"Infantry / Milsim"),n("option",{value:"aviation"},"Aviation Wing"),n("option",{value:"pmc"},"Private Military Company"),n("option",{value:"support"},"Logistics & Support"))),n("div",{className:"form-actions"},i?n("div",{className:"form-feedback is-error"},i):null,n("button",{type:"button",className:"submit-btn",disabled:e,onClick:()=>{const e={orgName:String(document.getElementById("org-create-name")?.value||"").trim(),type:String(document.getElementById("org-create-type")?.value||"")};a&&"function"==typeof a.requestCreateOrg?a.requestCreateOrg(e):t.failCreate("Registration bridge is not available.")}},e?"Submitting Registration...":"Submit Registration"),n("span",{className:"cancel-link",onClick:()=>t.setView("home")},"Cancel / Return to Main")))))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-home-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n margin-bottom: 2rem;\n}\n\n${i} .home-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.HomeView=function(){const e=t.getIsAuthenticating(),i=t.getLoginError();return r("main-home-view",s),n("div",{className:"content",[o]:""},n("div",{className:"card"},n("h2",null,"Create Organization"),n("p",null,"Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly."),n("button",{onClick:()=>t.setView("create")},"Register")),n("div",{className:"card"},n("h2",null,"Organization Portal"),n("p",null,"Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink."),i?n("div",{className:"home-feedback"},i):null,n("button",{disabled:e,onClick:()=>{a?a.requestLogin({}):t.failLogin("Login bridge is not available.")}},e?"Opening Portal...":"Login")))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n}=e.runtime,r=e.store;e.components=e.components||{},e.components.App=function(){const t=window.SharedUI.componentFns.Navbar,a=window.SharedUI.componentFns.Header,o=window.SharedUI.componentFns.Footer,i=window.SharedUI.componentFns.WindowTitleBar,s=e.componentFns.HomeView,l=e.componentFns.RegistrationView,d=window.OrgPortal&&window.OrgPortal.components?window.OrgPortal.components.App:null,c=r.getView(),m=window.OrgPortal&&window.OrgPortal.getters?window.OrgPortal.getters:null,g=window.OrgPortal&&window.OrgPortal.actions?window.OrgPortal.actions:null,p="create"===c?"Organization Registration":"portal"===c?"Organization Portal":"Entry Hub";function u(){e.bridge&&"function"==typeof e.bridge.close?e.bridge.close({}):r.setView("home")}if("portal"===c&&d){const e=m&&"function"==typeof m.canLeaveOrg&&m.canLeaveOrg();return n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),t({title:"Global Organization Network",viewLabel:p,actionLabel:e?"Leave Organization":"",onAction:e&&g&&"function"==typeof g.openModal?()=>g.openModal("leave"):null}),n("div",{id:"org-portal-frame-root"}))}let f;return"home"===c?f=s():"create"===c&&(f=l()),n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),n("main",null,t({title:"Global Organization Network",viewLabel:p}),n("div",{className:"container"},a({title:"Global Organization Network",onTitleClick:()=>r.setView("home")}),f),o({sections:[{title:"Registry Resources",items:["Registration Guidelines","Tax & Fee Schedule","Legal Compliance","Trademark Database"]},{title:"Bureau Support",items:["Office: Sector 7 Admin Block","Hours: 0800 - 1600 (GST)","Helpdesk: 555-01-REGISTRY","support@org-bureau.gov"]}]})))}}(),function(){const e=window.ForgeWebUI,n=window.RegistryApp,r=window.OrgPortal,t=[{id:"org-portal-frame-root",preserveScroll:!0,render:()=>r.components.App()},{id:"org-portal-toast-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryNoticeLayer()},{id:"org-overview-card-root",preserveScroll:!1,render:()=>r.componentFns.OverviewCard()},{id:"org-fleet-card-root",preserveScroll:!0,render:()=>r.componentFns.FleetCard()},{id:"org-treasury-card-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryCard()},{id:"org-members-card-root",preserveScroll:!0,render:()=>r.componentFns.MembersCard()},{id:"org-assets-card-root",preserveScroll:!0,render:()=>r.componentFns.AssetsCard()},{id:"org-activity-card-root",preserveScroll:!0,render:()=>r.componentFns.ActivityCard()},{id:"org-portal-modal-root",preserveScroll:!1,render:()=>r.componentFns.ModalLayer()}];e.createApp({name:"org",root:"#app",setup({root:r}){const a=function(){const n=new Map;return{sync:function(){t.forEach(r=>{const t=document.getElementById(r.id),a=n.get(r.id);if(!t)return void(a&&(a.handle.dispose(),n.delete(r.id)));if(a&&a.container===t)return;a&&a.handle.dispose();const o=e.mount(t,r.render,{preserveScroll:r.preserveScroll});n.set(r.id,{container:t,handle:o})})}}}();e.mount(r,()=>n.components.App(),{preserveScroll:!1}),n.bridge.ready({loaded:!0}),e.effect(()=>{n.store.getView(),requestAnimationFrame(()=>{a.sync()})})}}).start()}(); \ No newline at end of file +!function(){const e=window.ForgeWebUI,n=window.RegistryApp=window.RegistryApp||{},r=window.OrgPortal=window.OrgPortal||{};n.runtime=e,r.runtime=e,window.AppRuntime=e}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{createSignal:n}=e.runtime;e.store=new class{constructor(){[this.getView,this.setView]=n("home"),[this.getIsAuthenticating,this.setIsAuthenticating]=n(!1),[this.getLoginError,this.setLoginError]=n(""),[this.getIsCreating,this.setIsCreating]=n(!1),[this.getCreateError,this.setCreateError]=n("")}startLogin(){this.setLoginError(""),this.setIsAuthenticating(!0)}startCreate(){this.setCreateError(""),this.setIsCreating(!0)}failLogin(e){this.setIsAuthenticating(!1),this.setLoginError(e||"Authentication failed.")}failCreate(e){this.setIsCreating(!1),this.setCreateError(e||"Organization registration failed.")}hydratePortal(e){const n=window.OrgPortal&&window.OrgPortal.data?window.OrgPortal.data:null,r=window.OrgPortal&&window.OrgPortal.store?window.OrgPortal.store:null,t=e&&e.portalData?e.portalData:null,a=e&&e.session?e.session:null;return!!(n&&"function"==typeof n.applyLoginPayload&&r&&"function"==typeof r.hydrateFromPayload&&t&&a)&&(n.applyLoginPayload(e),r.hydrateFromPayload(e),!0)}completeLogin(e){this.hydratePortal(e)?(this.setLoginError(""),this.setIsAuthenticating(!1),this.setView("portal")):this.failLogin("Login response was missing portal data.")}completeCreate(e){this.hydratePortal(e)?(this.setCreateError(""),this.setIsCreating(!1),this.setView("portal")):this.failCreate("Organization registration response was missing portal data.")}}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},n=e.store,r=window.ForgeWebUI.createBridge({closeEvent:"org::close",globalName:"ForgeBridge",readyEvent:"org::ready"});function t(e,n){return r.send(e,n)}r.on("org::login::success",e=>{n.completeLogin(e)}),r.on("org::login::failure",e=>{n.failLogin(e.message||"Authentication failed.")}),r.on("org::create::success",e=>{n.completeCreate(e)}),r.on("org::create::failure",e=>{n.failCreate(e.message||"Organization registration failed.")}),r.on("org::sync",e=>{n&&"function"==typeof n.hydratePortal&&n.hydratePortal(e)}),r.on("org::credit::success",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("success",e.message||"Credit line assigned.")}),r.on("org::credit::failure",e=>{const n=window.OrgPortal;n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to assign credit line.")}),r.on("org::member::creditUpdated",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setCreditLines(n=>{const r={amount:e.amount||0,member:e.memberName||"",uid:e.memberUid||""},t=n.findIndex(e=>e.uid===r.uid);return-1===t?[...n,r]:n.map((e,n)=>n===t?r:e)})}),r.on("org::disband::success",()=>{const e=window.OrgPortal;e&&e.store&&(e.store.setModal(null),e.store.setOrgDisbanded(!0))}),r.on("org::disband::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Organization disbanding failed.")}),r.on("org::leave::success",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"You have left the organization."),n.setView("home")}),r.on("org::leave::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to leave the organization.")}),r.on("org::portal::revoked",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"Organization access is no longer available."),n.setView("home")}),e.bridge={close:r.close,ready:r.ready,receive:r.receive,requestLogin:function(e){n.startLogin(),t("org::login::request",e)||n.failLogin("Arma login bridge is unavailable.")},requestCreateOrg:function(e){n.startCreate(),t("org::create::request",e)||n.failCreate("Arma registration bridge is unavailable.")},requestDisbandOrg:function(){if(t("org::disband::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma disband bridge is unavailable.")},requestLeaveOrg:function(){if(t("org::leave::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma leave bridge is unavailable.")},requestCreditLine:function(e){if(t("org::credit::request",e))return!0;const n=window.OrgPortal;return n&&n.actions&&n.actions.showTreasuryNotice("error","Arma credit line bridge is unavailable."),!1},sendEvent:t}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},n={type:"Organization",status:"Operational",headquarters:"ArmA Verse"};function r(e){return JSON.parse(JSON.stringify(e))}function t(e,n){Object.keys(e).forEach(n=>delete e[n]),Object.assign(e,r(n))}function a(e,n){e.splice(0,e.length,...r(n))}function o(e){if(e&&"object"==typeof e&&!Array.isArray(e))return e;if(Array.isArray(e)){if(e.every(e=>Array.isArray(e)&&e.length>=2&&"string"==typeof e[0]))return Object.fromEntries(e)}if("string"==typeof e&&""!==e.trim())try{return o(JSON.parse(e))}catch(n){return e}return e}function i(e){return(Array.isArray(e)?e:e&&"object"==typeof e?Object.values(e):[]).map(o).filter(Boolean)}e.data={portalData:{org:Object.assign({name:"",tag:"",owner:"",ownerUid:"",isDefault:!1},n),funds:0,reputation:0,creditLines:[],members:[],fleet:[],assets:[],activity:[],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."}]},session:{actorName:"",actorUid:"",role:"",ceo:!1},applyLoginPayload(e){t(this.portalData.org,Object.assign({},e.portalData.org||{},n)),this.portalData.funds=e.portalData.funds||0,this.portalData.reputation=e.portalData.reputation||0,a(this.portalData.creditLines,i(e.portalData.creditLines)),a(this.portalData.members,i(e.portalData.members)),a(this.portalData.fleet,i(e.portalData.fleet)),a(this.portalData.assets,i(e.portalData.assets)),a(this.portalData.activity,i(e.portalData.activity)),a(this.portalData.roadmap,i(e.portalData.roadmap)),t(this.session,e.session||{})}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{createSignal:n}=window.RegistryApp.runtime,{portalData:r}=e.data;function t(e){if(e&&"object"==typeof e&&!Array.isArray(e))return e;if(Array.isArray(e)){if(e.every(e=>Array.isArray(e)&&e.length>=2&&"string"==typeof e[0]))return Object.fromEntries(e)}if("string"==typeof e&&""!==e.trim())try{return t(JSON.parse(e))}catch(n){return e}return e}function a(e){return(Array.isArray(e)?e:e&&"object"==typeof e?Object.values(e):[]).map(t).filter(Boolean)}e.store=new class{constructor(){[this.getFunds,this.setFunds]=n(r.funds),[this.getReputation,this.setReputation]=n(r.reputation),[this.getMembers,this.setMembers]=n([...r.members]),[this.getCreditLines,this.setCreditLines]=n([...r.creditLines]),[this.getFleet,this.setFleet]=n([...r.fleet]),[this.getAssets,this.setAssets]=n([...r.assets]),[this.getActivity,this.setActivity]=n([...r.activity]),[this.getTreasuryNotice,this.setTreasuryNotice]=n({type:"",text:""}),[this.getModal,this.setModal]=n(null),[this.getOrgDisbanded,this.setOrgDisbanded]=n(!1)}hydrateFromPayload(e){const n=e.portalData||{};this.setFunds(n.funds||0),this.setReputation(n.reputation||0),this.setMembers([...a(n.members)]),this.setCreditLines([...a(n.creditLines)]),this.setFleet([...a(n.fleet)]),this.setAssets([...a(n.assets)]),this.setActivity([...a(n.activity)])}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n,session:r}=e.data;e.getters=new class{formatCurrency(e){return"$"+Number(e||0).toLocaleString()}formatVehicleType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatAssetType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatDisplayName(e){return e?String(e).trim().split(/\s+/).map(e=>e?e.charAt(0).toUpperCase()+e.slice(1).toLowerCase():"").join(" "):""}getAssetReadiness(){const r=e.store?e.store.getFleet():n.fleet;if(0===r.length)return null;const t=r.reduce((e,n)=>e+(100-parseInt(n.damage,10)),0);return Math.round(t/r.length)}getNormalizedRole(){return String(r.role||"").trim().toUpperCase()}isDefaultOrg(){return!0===n.org.isDefault||"DEFAULT"===String(n.org.tag||"").trim().toUpperCase()}isOrgOwner(){const e=String(n.org.ownerUid||n.org.owner||"").trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return e&&t?t===e:String(r.actorName||"").trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isSessionCeo(){return!0===r.ceo}isOrgLeaderOrCeo(){return this.isOrgOwner()||"LEADER"===this.getNormalizedRole()||this.isDefaultOrg()&&this.isSessionCeo()}canManageMembers(){return this.isOrgLeaderOrCeo()}canManageTreasury(){return this.isOrgLeaderOrCeo()}canDisbandOrg(){return this.isOrgOwner()&&!this.isDefaultOrg()}canLeaveOrg(){return!this.isDefaultOrg()&&!this.isOrgOwner()}getMemberName(e){return String(e&&"object"==typeof e?e.name||"":e||"")}getMemberUid(e){return e&&"object"==typeof e?String(e.uid||""):""}isOwnerMember(e){return this.getMemberName(e).trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isCurrentMember(e){const n=this.getMemberUid(e).trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return n&&t?n===t:this.getMemberName(e).trim().toLowerCase()===String(r.actorName||"").trim().toLowerCase()}isProtectedMember(e){return this.isOwnerMember(e)||this.isCurrentMember(e)}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n}=e.data,r=e.store,t=e.getters,a=window.RegistryApp.store;e.actions=new class{constructor(){this.treasuryNoticeTimer=null}showTreasuryNotice(e,n){r.setTreasuryNotice({type:e,text:n}),this.treasuryNoticeTimer&&clearTimeout(this.treasuryNoticeTimer),this.treasuryNoticeTimer=setTimeout(()=>{r.setTreasuryNotice({type:"",text:""}),this.treasuryNoticeTimer=null},3500)}parseAmount(e){const n=Number(e);return Number.isFinite(n)?Math.round(n):0}getInputValue(e){const n=document.getElementById(e);return n?n.value:""}closePortal(){const e=window.RegistryApp?window.RegistryApp.bridge:null;e&&"function"==typeof e.close?e.close({}):a&&a.setView("home")}openModal(e){"payroll"!==e&&"transfer"!==e&&"credit"!==e||t.canManageTreasury()?("disband"!==e||t.canDisbandOrg())&&("leave"!==e||t.canLeaveOrg())&&r.setModal({type:e}):this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions.")}closeModal(){r.setModal(null)}removeMember(e){if(!t.canManageMembers())return!1;if(t.isProtectedMember(e))return!1;const n=t.getMemberUid(e),a=t.getMemberName(e);return r.setMembers(e=>e.filter(e=>n?e.uid!==n:e.name!==a)),r.setCreditLines(e=>e.filter(e=>n?e.uid!==n:e.member!==a)),!0}disbandOrganization(){if(!t.canDisbandOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestDisbandOrg?(this.closeModal(),e.requestDisbandOrg(),!0):(this.showTreasuryNotice("error","Disband bridge is unavailable."),!1)}leaveOrganization(){if(!t.canLeaveOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestLeaveOrg?(this.closeModal(),e.requestLeaveOrg(),!0):(this.showTreasuryNotice("error","Leave bridge is unavailable."),!1)}runPayroll(e){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const n=r.getMembers(),a=r.getFunds();if(0===n.length)return this.showTreasuryNotice("error","No members available for payroll."),!1;if(e<=0)return this.showTreasuryNotice("error","Enter a valid payroll amount."),!1;const o=e*n.length;return o>a?(this.showTreasuryNotice("error","Insufficient org funds for payroll."),!1):(r.setFunds(a-o),this.showTreasuryNotice("success",`Payroll sent to ${n.length} members for ${t.formatCurrency(o)}.`),!0)}sendFundsToMember(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const a=r.getFunds();return e?n<=0?(this.showTreasuryNotice("error","Enter a valid transfer amount."),!1):n>a?(this.showTreasuryNotice("error","Insufficient org funds for this transfer."),!1):(r.setFunds(a-n),this.showTreasuryNotice("success",`${t.formatCurrency(n)} sent to ${e}.`),!0):(this.showTreasuryNotice("error","Select a member to receive funds."),!1)}grantCreditLine(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;if(!e)return this.showTreasuryNotice("error","Select a member for the credit line."),!1;if(n<=0)return this.showTreasuryNotice("error","Enter a valid credit line amount."),!1;const a=r.getMembers().find(n=>t.getMemberUid(n)===e),o=a?t.getMemberName(a):"";if(!o)return this.showTreasuryNotice("error","Selected member was not found in the organization roster."),!1;const i=window.RegistryApp?window.RegistryApp.bridge:null;return i&&"function"==typeof i.requestCreditLine?i.requestCreditLine({memberUid:e,memberName:o,amount:n}):(this.showTreasuryNotice("error","Credit line bridge is unavailable."),!1)}}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-navbar",o=`[${a}]`,i=`\n${o} {\n background: var(--bg-surface);\n border-bottom: 1px solid var(--border);\n box-shadow: var(--shadow);\n}\n\n${o} .app-navbar-inner {\n display: flex;\n justify-content: space-between;\n align-items: center;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 1rem 2rem;\n box-sizing: border-box;\n}\n\n${o} .app-navbar-brand {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n}\n\n${o} .app-navbar-kicker {\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-navbar-title {\n font-size: 1.25rem;\n font-weight: 700;\n color: var(--primary-hover);\n letter-spacing: -0.025em;\n}\n\n${o} .app-navbar-actions {\n display: flex;\n align-items: center;\n gap: 1.5rem;\n}\n\n${o} .app-navbar-view {\n font-size: 0.8rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-close-btn {\n background: transparent;\n color: var(--text-muted);\n border: 1px solid var(--border);\n padding: 0.5rem 1rem;\n font-size: 0.85rem;\n}\n\n${o} .app-close-btn:hover {\n background: var(--bg-surface-hover);\n color: var(--primary-hover);\n border-color: var(--primary);\n transform: none;\n box-shadow: none;\n}\n\n@media (max-width: 960px) {\n ${o} .app-navbar-inner {\n flex-direction: column;\n align-items: flex-start;\n padding: 1rem 1.5rem;\n }\n\n ${o} .app-navbar-actions {\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Navbar=function({kicker:e="ORBIS",title:n="",viewLabel:o="",actionLabel:s="",onAction:l=null}){return t("shared-navbar",i),r("nav",{className:"app-navbar",[a]:""},r("div",{className:"app-navbar-inner"},r("div",{className:"app-navbar-brand"},r("span",{className:"app-navbar-kicker"},e),r("span",{className:"app-navbar-title"},n)),r("div",{className:"app-navbar-actions"},r("span",{className:"app-navbar-view"},o),s&&"function"==typeof l?r("button",{type:"button",className:"app-close-btn",onClick:l},s):null)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Header=function({title:e,subtitle:n="Organization Registration & Management Portal",onTitleClick:t=null}){return r("div",{className:"header"},r("h1",{style:{cursor:t?"pointer":"default"},onClick:t},e),r("p",null,n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.OrgPortal=window.OrgPortal||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Hero=function({className:e="",kicker:n="",title:t="",subtitle:a="",meta:o=""}){const i=["card org-panel org-span-12 org-page-header",e].filter(Boolean).join(" ");return r("section",{className:i},r("div",{className:"org-page-heading"},r("span",{className:"org-page-kicker"},n),r("h1",{className:"org-page-title"},t),r("p",{className:"org-page-subtitle"},a),r("span",{className:"org-page-meta"},o)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Footer=function({sections:e=[]}){return r("div",{className:"footer"},r("div",{className:"wrapper"},...e.map(e=>r("div",null,r("h3",null,e.title),r("ul",{style:{listStyleType:"none",padding:0}},...(e.items||[]).map(e=>r("li",null,e)))))))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-modal",o=`[${a}]`,i=`\n${o} {\n position: fixed;\n inset: 0;\n background: rgb(15 23 42 / 0.38);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1.5rem;\n z-index: 20;\n}\n\n${o} .app-modal-card {\n width: min(100%, 30rem);\n margin-bottom: 0;\n text-align: left;\n}\n\n${o} .app-modal-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1rem;\n}\n\n${o} .app-modal-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .app-modal-close {\n width: 2.25rem;\n height: 2.25rem;\n padding: 0;\n background: var(--bg-surface);\n color: var(--text-main);\n border: 1px solid var(--border);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-close:hover {\n background: var(--bg-surface-hover);\n color: var(--text-main);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n${o} .app-modal-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${o} .app-modal-form input,\n${o} .app-modal-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\n\n${o} .app-modal-form input:focus,\n${o} .app-modal-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12);\n}\n\n${o} .app-modal-form input:disabled,\n${o} .app-modal-form select:disabled {\n background: #f1f5f9;\n color: var(--text-muted);\n cursor: not-allowed;\n}\n\n${o} .app-modal-actions {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.75rem;\n margin-top: 0.5rem;\n}\n\n${o} .app-modal-actions button + button,\n${o} .app-modal-danger-actions button + button {\n margin-left: 0;\n}\n\n${o} .app-modal-danger {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid #fecaca;\n border-radius: var(--radius);\n background: #fff1f2;\n align-items: flex-start;\n}\n\n${o} .app-modal-danger p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .app-modal-danger-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem;\n}\n\n@media (max-width: 960px) {\n ${o} .app-modal-head,\n ${o} .app-modal-danger {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Modal=function({title:e="",body:n=null,onClose:o=null}){return t("shared-modal",i),r("div",{className:"app-modal-backdrop",[a]:"",onClick:e=>{e.target===e.currentTarget&&o&&o()}},r("div",{className:"card app-modal-card"},r("div",{className:"app-modal-head"},r("div",null,r("h2",{className:"app-modal-title"},e)),r("button",{type:"button",className:"app-modal-close",onClick:o,"aria-label":"Close dialog"},"x")),n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-panel-card",o=`[${a}]`,i=`\n${o} {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n\n${o} .org-panel-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${o} .org-panel-body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-height: 0;\n}\n\n${o} .org-eyebrow {\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-muted);\n margin-bottom: 0.4rem;\n}\n\n${o} .org-panel-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .org-panel-subtitle {\n margin: 0.35rem 0 0;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n@media (max-width: 960px) {\n ${o} .org-panel-head {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.PanelCard=function({className:e="",eyebrow:n="",title:o="",subtitle:s="",headerExtras:l=null,body:d=null,rootProps:c={}}){const m=["card org-panel",e].filter(Boolean).join(" ");return t("shared-panel-card",i),r("section",{className:m,[a]:"",...c},r("div",{className:"org-panel-head"},r("div",null,n?r("div",{className:"org-eyebrow"},n):null,r("h2",{className:"org-panel-title"},o),s?r("p",{className:"org-panel-subtitle"},s):null),l),r("div",{className:"org-panel-body"},d))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-metric-card",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.45rem;\n padding: 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n}\n\n${a}:nth-child(4n + 2),\n${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%);\n border-color: rgb(100 116 139 / 0.35);\n box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);\n}\n\n${a} .org-metric-label {\n font-size: 0.76rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--text-muted);\n}\n\n${a} .org-metric-value {\n font-size: 1.8rem;\n color: var(--primary-hover);\n line-height: 1.1;\n}\n\n${a}:nth-child(4n + 2) .org-metric-value,\n${a}:nth-child(4n + 3) .org-metric-value {\n color: #334155;\n}\n\n${a} .org-metric-note {\n color: var(--text-muted);\n font-size: 0.9rem;\n}\n\n@media (max-width: 960px) {\n ${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n border-color: var(--border);\n box-shadow: none;\n }\n\n ${a}:nth-child(4n + 3) .org-metric-value {\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MetricCard=function(e,a,i){return r("portal-metric-card",o),n("div",{className:"org-metric-card",[t]:""},n("span",{className:"org-metric-label"},e),n("strong",{className:"org-metric-value"},a),n("span",{className:"org-metric-note"},i))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-simple-stat",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.2rem;\n min-width: 90px;\n}\n\n${a} .org-simple-label {\n font-size: 0.72rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${a} .org-simple-value {\n font-size: 0.95rem;\n color: var(--text-main);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.SimpleStat=function(e,a){return r("portal-simple-stat",o),n("div",{className:"org-simple-stat",[t]:""},n("span",{className:"org-simple-label"},e),n("strong",{className:"org-simple-value"},a))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.store,o=e.getters,i="data-ui-overview-card",s=`[${i}]`,l=`\n${s} .org-hero-grid {\n display: grid;\n grid-template-columns: 1.3fr 1fr;\n gap: 1.5rem;\n align-items: start;\n}\n\n${s} .org-summary {\n margin: 0;\n font-size: 1.05rem;\n color: var(--text-main);\n}\n\n${s} .org-meta-row {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 1rem;\n margin-top: 1.5rem;\n}\n\n${s} .org-meta-item {\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-meta-item:nth-child(even) {\n background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${s} .org-meta-value {\n font-size: 1rem;\n font-weight: 600;\n color: var(--primary-hover);\n}\n\n${s} .org-metric-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${s} .org-hero-grid,\n ${s} .org-meta-row,\n ${s} .org-metric-grid {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.OverviewCard=function(){const s=e.componentFns.MetricCard,d=window.SharedUI.componentFns.PanelCard,c=o.getAssetReadiness(),m=t.org.headquarters||"ArmA Verse",g=a.getAssets().length,p=a.getFleet().length,u=a.getFunds(),f=a.getMembers().length,b=a.getReputation();return r("portal-overview-card",l),d({className:"org-span-12",eyebrow:t.org.tag,title:"Organization Overview",rootProps:{[i]:""},body:n("div",{className:"org-hero-grid"},n("div",{className:"org-hero-copy"},n("p",{className:"org-summary"},t.org.type," operating from ",m,". Treasury, fleet status, inventory, and roster management are surfaced here first."),n("div",{className:"org-meta-row"},n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Director"),n("span",{className:"org-meta-value"},o.formatDisplayName(t.org.owner))),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Active Members"),n("span",{className:"org-meta-value"},`${f} total`)),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Fleet Readiness"),n("span",{className:"org-meta-value"},null===c?"N/A":`${c}%`)))),n("div",{className:"org-metric-grid"},s("Org Funds",o.formatCurrency(u),"Organization treasury balance"),s("Reputation",b,"Organization standing"),s("Asset Lines",g,"Tracked supply and equipment entries"),s("Fleet Vehicles",p,"Tracked air, ground, and naval vehicles")))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-fleet-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FleetCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getFleet();return r("portal-fleet-card",s),t({className:"org-scroll-panel org-span-7",title:"Fleet",subtitle:"Individual vehicles with type, status, and overall damage.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatVehicleType(e.type)),i("Status",e.status),i("Damage",e.damage)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r,createSignal:t}=e.runtime,{portalData:a}=e.data,o=e.store,i=e.getters,s=e.actions,l="data-ui-treasury-card",d=`[${l}]`,[c,m]=t("overview"),[g,p]=t(!1),u=`\n${d} .org-treasury-menu {\n position: relative;\n}\n\n${d} .org-menu-btn {\n width: 2.75rem;\n height: 2.75rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 1px solid var(--border);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n${d} .org-menu-btn:hover {\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.65);\n}\n\n${d} .org-menu-btn svg {\n width: 1.1rem;\n height: 1.1rem;\n}\n\n${d} .org-menu-dropdown {\n position: absolute;\n top: calc(100% + 0.6rem);\n right: 0;\n min-width: 10.5rem;\n padding: 0.45rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.12);\n display: flex;\n flex-direction: column;\n gap: 0.35rem;\n z-index: 5;\n}\n\n${d} .org-menu-option + .org-menu-option {\n margin-left: 0;\n}\n\n${d} .org-menu-option {\n width: 100%;\n justify-content: flex-start;\n background: transparent;\n color: var(--text-main);\n border: 1px solid transparent;\n}\n\n${d} .org-menu-option:hover {\n background: #f8fafc;\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-menu-option.is-active {\n background: rgb(226 232 240 / 0.7);\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-finance-meta {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${d} .org-finance-meta > div {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n}\n\n${d} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-action-grid {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n margin-bottom: 1rem;\n}\n\n${d} .org-action-grid button + button {\n margin-left: 0;\n}\n\n${d} .org-action-grid button {\n width: 100%;\n}\n\n${d} .org-access-note {\n margin: 0 0 1rem;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n${d} .org-credit-summary {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n padding: 0.85rem 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-summary strong {\n font-size: 1rem;\n}\n\n${d} .org-credit-summary span:last-child {\n font-size: 0.92rem;\n line-height: 1.45;\n}\n\n${d} .org-credit-lines-list {\n display: flex;\n flex-direction: column;\n gap: 0.85rem;\n}\n\n${d} .org-treasury-body {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: 1rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${d} .org-credit-line-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-line-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${d} .org-credit-line-member {\n display: flex;\n flex-direction: column;\n gap: 0.3rem;\n}\n\n${d} .org-credit-line-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-credit-line-empty {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n@media (max-width: 960px) {\n ${d} .org-finance-meta {\n grid-template-columns: 1fr;\n }\n\n ${d} .org-credit-line-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.TreasuryCard=function(){const e=window.SharedUI.componentFns.PanelCard,t=o.getCreditLines(),a=o.getReputation(),d=i.canManageTreasury(),f=c(),b=g(),w=1===t.length?"1 active credit line":`${t.length} active credit lines`;return r("portal-treasury-card",u),e({className:"org-scroll-panel org-span-5",title:"Treasury",subtitle:"Organization funds, reputation and payouts.",headerExtras:n("div",{className:"org-treasury-menu"},n("button",{type:"button",className:"org-menu-btn",title:"Treasury views","aria-label":"Treasury views",onClick:()=>p(e=>!e)},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("line",{x1:"4",y1:"7",x2:"20",y2:"7"}),n("line",{x1:"4",y1:"12",x2:"20",y2:"12"}),n("line",{x1:"4",y1:"17",x2:"20",y2:"17"}))),b?n("div",{className:"org-menu-dropdown"},n("button",{type:"button",className:"overview"===f?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("overview"),p(!1)}},"Overview"),n("button",{type:"button",className:"credit"===f?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("credit"),p(!1)}},"Credit Lines")):null),rootProps:{[l]:""},body:n("div",{className:"org-treasury-body"},"credit"===f?t.length>0?n("div",{className:"org-credit-lines-list"},...t.map(e=>n("article",{className:"org-credit-line-row"},n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Member"),n("strong",null,e.member)),n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Amount"),n("strong",null,i.formatCurrency(e.amount)))))):n("div",{className:"org-credit-line-empty"},"No active credit lines."):n("div",null,n("div",{className:"org-finance-meta"},n("div",null,n("span",{className:"org-meta-label"},"Funds"),n("strong",null,i.formatCurrency(o.getFunds()))),n("div",null,n("span",{className:"org-meta-label"},"Reputation"),n("strong",null,`${a}`))),d?n("div",{className:"org-action-grid"},n("button",{type:"button",onClick:()=>s.openModal("payroll")},"Run Payroll"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("transfer")},"Send Funds"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("credit")},"Credit Line")):n("p",{className:"org-access-note"},"Only the organization leader or CEO can manage treasury actions."),n("div",{className:"org-credit-summary"},n("span",{className:"org-meta-label"},"Credit Line Status"),n("strong",null,w),n("span",null,t.length>0?"Open the Credit Lines tab to review assigned members and amounts.":"Assign a credit line to create the first approved member limit."))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-assets-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.AssetsCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getAssets();return r("portal-assets-card",s),t({className:"org-scroll-panel org-span-7",title:"Assets",subtitle:"Inventory supplies and equipment with quantity totals.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatAssetType(e.type)),i("Quantity",e.quantity)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.getters,o=e.actions,i="data-ui-members-card",s=`[${i}]`,l=`\n${s} .org-name-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${s} .org-name-row {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-name-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-name-row button {\n margin-left: auto;\n}\n\n@media (max-width: 960px) {\n ${s} .org-name-row {\n flex-direction: column;\n align-items: flex-start;\n }\n\n ${s} .org-name-row button {\n margin-left: 0;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MembersCard=function(){const e=window.SharedUI.componentFns.PanelCard,s=t.getMembers(),d=a.canManageMembers();return r("portal-members-card",l),e({className:"org-scroll-panel org-span-5",title:"Members",subtitle:"Current roster listing. The organization owner and your own member entry cannot be removed.",rootProps:{[i]:""},body:n("div",{className:"org-name-list"},...s.map(e=>{const r=d&&!a.isProtectedMember(e);return n("article",{className:"org-name-row"},n("strong",null,e.name),r?n("button",{type:"button",className:"org-danger-btn org-icon-btn",title:`Remove ${e.name}`,"aria-label":`Remove ${e.name}`,onClick:()=>o.removeMember(e)},n("svg",{className:"org-icon",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("path",{d:"M9 3h6"}),n("path",{d:"M4 7h16"}),n("path",{d:"M6 7l1 13h10l1-13"}),n("path",{d:"M10 11v6"}),n("path",{d:"M14 11v6"}))):null)}))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a="data-ui-activity-card",o=`[${a}]`,i=`\n${o} .org-activity-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-activity-row {\n padding: 1rem;\n border: 1px solid var(--border);\n border-left: 3px solid #94a3b8;\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-activity-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n border-left-color: #64748b;\n}\n\n${o} .org-activity-row p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-activity-time {\n display: inline-block;\n margin-bottom: 0.35rem;\n color: var(--text-muted);\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.ActivityCard=function(){const t=window.SharedUI.componentFns.PanelCard,o=e.store.getActivity();return r("portal-activity-card",i),t({className:"org-scroll-panel org-span-6",title:"Command Feed",subtitle:"Recent organization-level actions and updates.",rootProps:{[a]:""},body:n("div",{className:"org-activity-list"},...o.map(e=>n("article",{className:"org-activity-row"},n("span",{className:"org-activity-time"},e.time),n("p",null,e.text))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-future-card",a=[{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."}],o=`[${t}]`,i=`\n${o} .org-roadmap-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-roadmap-card {\n padding: 1rem;\n display: flex;\n flex-direction: column;\n gap: 0.7rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2),\n${o} .org-roadmap-card:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(100 116 139 / 0.4);\n}\n\n${o} .org-roadmap-card p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-list-tag {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0.2rem 0.55rem;\n border-radius: 999px;\n font-size: 0.72rem;\n font-weight: 700;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n background: #e2e8f0;\n color: var(--primary-hover);\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2) .org-list-tag,\n${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #cbd5e1;\n color: #1e293b;\n}\n\n@media (max-width: 960px) {\n ${o} .org-roadmap-grid {\n grid-template-columns: 1fr;\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) {\n background: #f8fafc;\n border-color: var(--border);\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #e2e8f0;\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FutureCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-future-card",i),e({className:"org-scroll-panel org-span-6",title:"Expansion Slots",subtitle:"Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.",rootProps:{[t]:""},body:n("div",{className:"org-roadmap-grid"},...a.map(e=>n("article",{className:"org-roadmap-card"},n("span",{className:"org-list-tag"},e.status),n("strong",null,e.name),n("p",null,e.detail))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.getters,a=e.actions,o="data-ui-danger-card",i=`[${o}]`,s=`\n${i} {\n border-color: #fecaca;\n background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%);\n}\n\n${i} .org-danger-copy {\n margin-bottom: 1rem;\n}\n\n${i} .org-danger-copy strong,\n${i} .org-danger-copy p {\n display: block;\n}\n\n${i} .org-danger-copy p {\n margin: 0.4rem 0 0;\n color: var(--text-muted);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.DangerCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-danger-card",s),t.canDisbandOrg()?e({className:"org-span-12 org-danger-panel",title:"Organization Controls",subtitle:"Leader-only actions for membership and permanent organization removal.",rootProps:{[o]:""},body:n("div",null,n("div",{className:"org-danger-copy"},n("strong",null,"Disband organization"),n("p",null,"This removes the organization and revokes access to the portal for all members.")),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.openModal("disband")},"Disband Organization"))}):null}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=e.store,a=e.actions;e.componentFns=e.componentFns||{},e.componentFns.ModalLayer=function(){const e=window.SharedUI.componentFns.Modal,o=t.getModal();if(!o)return null;const i=t.getMembers(),s=0===i.length?{disabled:!0}:{};let l="",d=null;return"payroll"===o.type?(l="Run Payroll",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Amount Per Member"),n("input",{id:"treasury-payroll-amount",type:"number",min:"1",placeholder:"500",autofocus:"true"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",onClick:()=>{a.runPayroll(a.parseAmount(a.getInputValue("treasury-payroll-amount")))&&a.closeModal()}},"Run Payroll")))):"transfer"===o.type?(l="Send Funds",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-transfer-member",...s},...i.map(e=>n("option",{value:e.name},e.name)))),n("div",null,n("label",null,"Amount"),n("input",{id:"treasury-transfer-amount",type:"number",min:"1",placeholder:"1500"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...s,onClick:()=>{a.sendFundsToMember(String(a.getInputValue("treasury-transfer-member")||""),a.parseAmount(a.getInputValue("treasury-transfer-amount")))&&a.closeModal()}},"Send Funds")))):"credit"===o.type?(l="Assign Credit Line",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-credit-member",...s},...i.map(e=>n("option",{value:e.uid},e.name)))),n("div",null,n("label",null,"Credit Amount"),n("input",{id:"treasury-credit-amount",type:"number",min:"1",placeholder:"5000"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...s,onClick:()=>{a.grantCreditLine(String(a.getInputValue("treasury-credit-member")||""),a.parseAmount(a.getInputValue("treasury-credit-amount")))&&a.closeModal()}},"Assign Credit Line")))):"disband"===o.type?(l="Disband Organization",d=n("div",{className:"app-modal-danger"},n("p",null,"This action is permanent. Disband ",r.org.name,"?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.disbandOrganization()},"Confirm Disband")))):"leave"===o.type&&(l="Leave Organization",d=n("div",{className:"app-modal-danger"},n("p",null,"Leave ",r.org.name," and return to the default organization?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.leaveOrganization()},"Confirm Leave")))),e({title:l,body:d,onClose:()=>a.closeModal()})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=window.RegistryApp.store;e.componentFns=e.componentFns||{},e.componentFns.DisbandedView=function(){return(0,window.SharedUI.componentFns.PanelCard)({className:"org-span-12 org-empty-state",eyebrow:"Organization Removed",title:r.org.name,body:n("div",null,n("p",{className:"org-summary"},"This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview."),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>t.setView("home")},"Return to Registry"))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t,session:a}=e.data,o=e.store,i="[data-ui-portal-view]";r("portal-view",`\n ${i} {\n --org-row-card-max-height: 36rem;\n }\n\n ${i} .org-toast-stack {\n position: fixed;\n top: 1.5rem;\n right: 2rem;\n z-index: 20;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n pointer-events: none;\n }\n\n ${i} .org-toast {\n max-width: 24rem;\n padding: 0.9rem 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.14);\n font-size: 0.92rem;\n pointer-events: auto;\n }\n\n ${i} .org-toast.is-success {\n background: #ecfdf5;\n border-color: #bbf7d0;\n color: #166534;\n }\n\n ${i} .org-toast.is-error {\n background: #fef2f2;\n border-color: #fecaca;\n color: #991b1b;\n }\n\n ${i} .org-dashboard-grid {\n display: grid;\n grid-template-columns: repeat(12, minmax(0, 1fr));\n gap: 1.5rem;\n align-items: stretch;\n }\n\n ${i} .org-panel {\n margin-bottom: 0;\n text-align: left;\n }\n\n ${i} .org-scroll-panel {\n display: flex;\n flex-direction: column;\n min-height: 0;\n max-height: var(--org-row-card-max-height);\n overflow: hidden;\n }\n\n ${i} .org-island-root {\n display: flex;\n align-self: stretch;\n min-height: 0;\n min-width: 0;\n }\n\n ${i} .org-island-root > .org-panel {\n height: 100%;\n width: 100%;\n }\n\n ${i} .org-span-12 {\n grid-column: span 12;\n }\n\n ${i} .org-span-7 {\n grid-column: span 7;\n }\n\n ${i} .org-span-6 {\n grid-column: span 6;\n }\n\n ${i} .org-span-5 {\n grid-column: span 5;\n }\n\n @media (max-width: 960px) {\n ${i} .org-toast-stack {\n top: 1rem;\n right: 1rem;\n left: 1rem;\n }\n\n ${i} .org-toast {\n max-width: none;\n }\n\n ${i} .org-span-12,\n ${i} .org-span-7,\n ${i} .org-span-6,\n ${i} .org-span-5 {\n grid-column: span 12;\n }\n\n ${i} .org-scroll-panel {\n max-height: none;\n }\n\n }\n `),e.components=e.components||{},e.componentFns=e.componentFns||{},e.componentFns.TreasuryNoticeLayer=function(){const e=o.getTreasuryNotice();return e.text?n("div",{className:"org-toast-stack"},n("div",{className:"error"===e.type?"org-toast is-error":"org-toast is-success"},e.text)):null},e.components.App=function(){const r=window.SharedUI.componentFns.Hero,i=window.SharedUI.componentFns.Footer,s=e.componentFns.FutureCard,l=e.componentFns.DangerCard,d=e.componentFns.DisbandedView,c=[{title:"Organization Controls",items:["Roster Management","Fleet Assignment","Treasury Permissions","Asset Registry"]},{title:"Planned Extensions",items:["Contracts Board","Diplomacy Layer","Procurement Queue","Reputation History"]}];return o.getOrgDisbanded()?n("main",{"data-ui-portal-view":""},n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),d())),n("div",{id:"org-portal-modal-root"}),i({sections:c})):n("main",{"data-ui-portal-view":""},n("div",{id:"org-portal-toast-root"}),n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),n("div",{className:"org-island-root org-span-12",id:"org-overview-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-fleet-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-treasury-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-members-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-assets-card-root"}),n("div",{className:"org-island-root org-span-6",id:"org-activity-card-root"}),s(),l())),n("div",{id:"org-portal-modal-root"}),i({sections:c}))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-registration-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n align-items: center;\n width: 100%;\n}\n\n${i} .info-panel {\n text-align: left;\n padding: 1rem;\n}\n\n${i} .create-feature-list {\n text-align: left;\n margin-top: 1.5rem;\n list-style-type: none;\n padding: 0;\n}\n\n${i} .create-feature-item {\n margin-bottom: 0.5rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n${i} .create-feature-icon {\n width: 1.2rem;\n height: 1.2rem;\n flex-shrink: 0;\n}\n\n${i} .price-tag {\n margin-top: 2rem;\n padding: 1rem;\n background: var(--bg-app);\n border-radius: var(--radius);\n border: 1px solid var(--border);\n}\n\n${i} .price-label {\n display: block;\n font-size: 0.9rem;\n color: var(--text-muted);\n}\n\n${i} .price-value {\n display: block;\n font-size: 2rem;\n font-weight: 700;\n color: var(--primary);\n}\n\n${i} .form-panel {\n margin: 0;\n}\n\n${i} .app-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n text-align: left;\n}\n\n${i} .app-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${i} .app-form input,\n${i} .app-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s;\n}\n\n${i} .app-form input:focus,\n${i} .app-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1);\n}\n\n${i} .form-actions {\n margin-top: 1rem;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n align-items: center;\n}\n\n${i} .submit-btn {\n width: 100%;\n}\n\n${i} .cancel-link {\n font-size: 0.9rem;\n color: var(--text-muted);\n cursor: pointer;\n text-decoration: underline;\n}\n\n${i} .cancel-link:hover {\n color: var(--primary);\n}\n\n${i} .form-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n}\n\n${i} .form-feedback.is-error {\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.RegistrationView=function(){const e=t.getIsCreating(),i=t.getCreateError();r("main-registration-view",s);return n("div",{className:"split-container",[o]:""},n("div",{className:"info-panel"},n("h2",null,"Registration Details"),n("p",null,"Complete the form to add your organization to the Global Organization Registry."),n("ul",{className:"create-feature-list"},n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Official Organization Designator"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Secure Comms Channel"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Deployment Roster Access"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"After-Action Report Tools")),n("div",{className:"price-tag"},n("span",{className:"price-label"},"Registration Fee"),n("span",{className:"price-value"},"$50,000"))),n("div",{className:"form-panel card"},n("h2",null,"Organization Registration"),n("div",{className:"app-form"},n("div",null,n("label",null,"Organization Name"),n("input",{id:"org-create-name",type:"text",placeholder:"e.g. Task Force 141"})),n("div",null,n("label",null,"Organization Type"),n("select",{id:"org-create-type"},n("option",{value:"infantry"},"Infantry / Milsim"),n("option",{value:"aviation"},"Aviation Wing"),n("option",{value:"pmc"},"Private Military Company"),n("option",{value:"support"},"Logistics & Support"))),n("div",{className:"form-actions"},i?n("div",{className:"form-feedback is-error"},i):null,n("button",{type:"button",className:"submit-btn",disabled:e,onClick:()=>{const e={orgName:String(document.getElementById("org-create-name")?.value||"").trim(),type:String(document.getElementById("org-create-type")?.value||"")};a&&"function"==typeof a.requestCreateOrg?a.requestCreateOrg(e):t.failCreate("Registration bridge is not available.")}},e?"Submitting Registration...":"Submit Registration"),n("span",{className:"cancel-link",onClick:()=>t.setView("home")},"Cancel / Return to Main")))))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-home-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n margin-bottom: 2rem;\n}\n\n${i} .home-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.HomeView=function(){const e=t.getIsAuthenticating(),i=t.getLoginError();return r("main-home-view",s),n("div",{className:"content",[o]:""},n("div",{className:"card"},n("h2",null,"Create Organization"),n("p",null,"Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly."),n("button",{onClick:()=>t.setView("create")},"Register")),n("div",{className:"card"},n("h2",null,"Organization Portal"),n("p",null,"Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink."),i?n("div",{className:"home-feedback"},i):null,n("button",{disabled:e,onClick:()=>{a?a.requestLogin({}):t.failLogin("Login bridge is not available.")}},e?"Opening Portal...":"Login")))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n}=e.runtime,r=e.store;e.components=e.components||{},e.components.App=function(){const t=window.SharedUI.componentFns.Navbar,a=window.SharedUI.componentFns.Header,o=window.SharedUI.componentFns.Footer,i=window.SharedUI.componentFns.WindowTitleBar,s=e.componentFns.HomeView,l=e.componentFns.RegistrationView,d=window.OrgPortal&&window.OrgPortal.components?window.OrgPortal.components.App:null,c=r.getView(),m=window.OrgPortal&&window.OrgPortal.getters?window.OrgPortal.getters:null,g=window.OrgPortal&&window.OrgPortal.actions?window.OrgPortal.actions:null,p="create"===c?"Organization Registration":"portal"===c?"Organization Portal":"Entry Hub";function u(){e.bridge&&"function"==typeof e.bridge.close?e.bridge.close({}):r.setView("home")}if("portal"===c&&d){const e=m&&"function"==typeof m.canLeaveOrg&&m.canLeaveOrg();return n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),t({title:"Global Organization Network",viewLabel:p,actionLabel:e?"Leave Organization":"",onAction:e&&g&&"function"==typeof g.openModal?()=>g.openModal("leave"):null}),n("div",{id:"org-portal-frame-root"}))}let f;return"home"===c?f=s():"create"===c&&(f=l()),n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),n("main",null,t({title:"Global Organization Network",viewLabel:p}),n("div",{className:"container"},a({title:"Global Organization Network",onTitleClick:()=>r.setView("home")}),f),o({sections:[{title:"Registry Resources",items:["Registration Guidelines","Tax & Fee Schedule","Legal Compliance","Trademark Database"]},{title:"Bureau Support",items:["Office: Sector 7 Admin Block","Hours: 0800 - 1600 (GST)","Helpdesk: 555-01-REGISTRY","support@org-bureau.gov"]}]})))}}(),function(){const e=window.ForgeWebUI,n=window.RegistryApp,r=window.OrgPortal,t=[{id:"org-portal-frame-root",preserveScroll:!0,render:()=>r.components.App()},{id:"org-portal-toast-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryNoticeLayer()},{id:"org-overview-card-root",preserveScroll:!1,render:()=>r.componentFns.OverviewCard()},{id:"org-fleet-card-root",preserveScroll:!0,render:()=>r.componentFns.FleetCard()},{id:"org-treasury-card-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryCard()},{id:"org-members-card-root",preserveScroll:!0,render:()=>r.componentFns.MembersCard()},{id:"org-assets-card-root",preserveScroll:!0,render:()=>r.componentFns.AssetsCard()},{id:"org-activity-card-root",preserveScroll:!0,render:()=>r.componentFns.ActivityCard()},{id:"org-portal-modal-root",preserveScroll:!1,render:()=>r.componentFns.ModalLayer()}];e.createApp({name:"org",root:"#app",setup({root:r}){const a=function(){const n=new Map;return{sync:function(){t.forEach(r=>{const t=document.getElementById(r.id),a=n.get(r.id);if(!t)return void(a&&(a.handle.dispose(),n.delete(r.id)));if(a&&a.container===t)return;a&&a.handle.dispose();const o=e.mount(t,r.render,{preserveScroll:r.preserveScroll});n.set(r.id,{container:t,handle:o})})}}}();e.mount(r,()=>n.components.App(),{preserveScroll:!1}),n.bridge.ready({loaded:!0}),e.effect(()=>{n.store.getView(),requestAnimationFrame(()=>{a.sync()})})}}).start()}(); \ No newline at end of file diff --git a/arma/client/addons/org/ui/src/portal/data.js b/arma/client/addons/org/ui/src/portal/data.js index 72b4c3b..a544baf 100644 --- a/arma/client/addons/org/ui/src/portal/data.js +++ b/arma/client/addons/org/ui/src/portal/data.js @@ -19,6 +19,45 @@ target.splice(0, target.length, ...cloneValue(source)); } + function normalizeRecord(value) { + if (value && typeof value === "object" && !Array.isArray(value)) { + return value; + } + + if (Array.isArray(value)) { + const isEntryArray = value.every( + (entry) => + Array.isArray(entry) && + entry.length >= 2 && + typeof entry[0] === "string", + ); + + if (isEntryArray) { + return Object.fromEntries(value); + } + } + + if (typeof value === "string" && value.trim() !== "") { + try { + return normalizeRecord(JSON.parse(value)); + } catch (_error) { + return value; + } + } + + return value; + } + + function normalizeCollection(value) { + const source = Array.isArray(value) + ? value + : value && typeof value === "object" + ? Object.values(value) + : []; + + return source.map(normalizeRecord).filter(Boolean); + } + OrgPortal.data = { portalData: { org: Object.assign( @@ -80,25 +119,28 @@ this.portalData.reputation = payload.portalData.reputation || 0; replaceArray( this.portalData.creditLines, - payload.portalData.creditLines || [], + normalizeCollection(payload.portalData.creditLines), ); replaceArray( this.portalData.members, - payload.portalData.members || [], + normalizeCollection(payload.portalData.members), + ); + replaceArray( + this.portalData.fleet, + normalizeCollection(payload.portalData.fleet), ); - replaceArray(this.portalData.fleet, payload.portalData.fleet || []); replaceArray( this.portalData.assets, - payload.portalData.assets || [], + normalizeCollection(payload.portalData.assets), ); replaceArray( this.portalData.activity, - payload.portalData.activity || [], + normalizeCollection(payload.portalData.activity), ); replaceArray( this.portalData.roadmap, - payload.portalData.roadmap || [], + normalizeCollection(payload.portalData.roadmap), ); replaceObject(this.session, payload.session || {}); diff --git a/arma/client/addons/org/ui/src/portal/store.js b/arma/client/addons/org/ui/src/portal/store.js index d17a366..abdb361 100644 --- a/arma/client/addons/org/ui/src/portal/store.js +++ b/arma/client/addons/org/ui/src/portal/store.js @@ -3,6 +3,45 @@ const { createSignal } = window.RegistryApp.runtime; const { portalData } = OrgPortal.data; + function normalizeRecord(value) { + if (value && typeof value === "object" && !Array.isArray(value)) { + return value; + } + + if (Array.isArray(value)) { + const isEntryArray = value.every( + (entry) => + Array.isArray(entry) && + entry.length >= 2 && + typeof entry[0] === "string", + ); + + if (isEntryArray) { + return Object.fromEntries(value); + } + } + + if (typeof value === "string" && value.trim() !== "") { + try { + return normalizeRecord(JSON.parse(value)); + } catch (_error) { + return value; + } + } + + return value; + } + + function normalizeCollection(value) { + const source = Array.isArray(value) + ? value + : value && typeof value === "object" + ? Object.values(value) + : []; + + return source.map(normalizeRecord).filter(Boolean); + } + class OrgPortalStore { constructor() { [this.getFunds, this.setFunds] = createSignal(portalData.funds); @@ -37,11 +76,13 @@ this.setFunds(nextPortalData.funds || 0); this.setReputation(nextPortalData.reputation || 0); - this.setMembers([...(nextPortalData.members || [])]); - this.setCreditLines([...(nextPortalData.creditLines || [])]); - this.setFleet([...(nextPortalData.fleet || [])]); - this.setAssets([...(nextPortalData.assets || [])]); - this.setActivity([...(nextPortalData.activity || [])]); + this.setMembers([...normalizeCollection(nextPortalData.members)]); + this.setCreditLines([ + ...normalizeCollection(nextPortalData.creditLines), + ]); + this.setFleet([...normalizeCollection(nextPortalData.fleet)]); + this.setAssets([...normalizeCollection(nextPortalData.assets)]); + this.setActivity([...normalizeCollection(nextPortalData.activity)]); } } diff --git a/arma/server/addons/bank/XEH_preInit.sqf b/arma/server/addons/bank/XEH_preInit.sqf index 9e2fd5c..c29e294 100644 --- a/arma/server/addons/bank/XEH_preInit.sqf +++ b/arma/server/addons/bank/XEH_preInit.sqf @@ -20,15 +20,6 @@ PREP_RECOMPILE_END; GVAR(BankStore) call ["hydrateSession", [_uid, _mode, _resetAuthorization]]; }] call CFUNC(addEventHandler); -[QGVAR(requestSaveBank), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" }; - - private _finalData = GVAR(BankStore) call ["save", [_uid]]; - GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalData]]; -}] call CFUNC(addEventHandler); - [QGVAR(requestDeposit), { params [["_uid", "", [""]], ["_amount", 0, [0]]]; diff --git a/arma/server/addons/garage/XEH_preInit.sqf b/arma/server/addons/garage/XEH_preInit.sqf index 6f5982c..8222cf3 100644 --- a/arma/server/addons/garage/XEH_preInit.sqf +++ b/arma/server/addons/garage/XEH_preInit.sqf @@ -13,40 +13,6 @@ PREP_RECOMPILE_END; GVAR(GarageStore) call ["init", [_uid]]; }] call CFUNC(addEventHandler); -[QGVAR(requestGetGarage), { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" }; - - private _finalData = GVAR(GarageStore) call ["get", [_uid, _field]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(garage,responseSyncGarage), [_finalData], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestSetGarage), { - params [["_uid", "", [""]], ["_key", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "" || _key isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID or Key!" }; - - private _hashMap = GVAR(GarageStore) call ["set", [_uid, _key, _value, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(garage,responseSyncGarage), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestMSetGarage), { - params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" }; - if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid field pairs!" }; - - private _hashMap = GVAR(GarageStore) call ["mset", [_uid, _fieldValuePairs, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(garage,responseSyncGarage), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - [QGVAR(requestSaveGarage), { params [["_uid", "", [""]]]; @@ -58,13 +24,6 @@ PREP_RECOMPILE_END; [CRPC(garage,responseSyncGarage), [_finalData], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); -[QGVAR(requestRemoveGarage), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" }; - GVAR(GarageStore) call ["remove", [_uid]]; -}] call CFUNC(addEventHandler); - [QGVAR(requestStoreVehicle), { params [ ["_uid", "", [""]], @@ -144,40 +103,6 @@ PREP_RECOMPILE_END; GVAR(VGarageStore) call ["init", [_uid]]; }] call CFUNC(addEventHandler); -[QGVAR(requestGetVG), { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" }; - - private _finalData = GVAR(VGarageStore) call ["get", [_uid, _field]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(garage,responseSyncVG), [_finalData], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestSetVG), { - params [["_uid", "", [""]], ["_key", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "" || _key isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID or Key!" }; - - private _hashMap = GVAR(VGarageStore) call ["set", [_uid, _key, _value, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(garage,responseSyncVG), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestMSetVG), { - params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" }; - if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid field pairs!" }; - - private _hashMap = GVAR(VGarageStore) call ["mset", [_uid, _fieldValuePairs, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(garage,responseSyncVG), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - [QGVAR(requestSaveVG), { params [["_uid", "", [""]]]; @@ -189,9 +114,3 @@ PREP_RECOMPILE_END; [CRPC(garage,responseSyncVG), [_finalData], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); -[QGVAR(requestRemoveVG), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" }; - GVAR(VGarageStore) call ["remove", [_uid]]; -}] call CFUNC(addEventHandler); diff --git a/arma/server/addons/garage/functions/fnc_initGarageStore.sqf b/arma/server/addons/garage/functions/fnc_initGarageStore.sqf index 1ae37d5..6e35c3c 100644 --- a/arma/server/addons/garage/functions/fnc_initGarageStore.sqf +++ b/arma/server/addons/garage/functions/fnc_initGarageStore.sqf @@ -67,93 +67,12 @@ GVAR(GarageBaseStore) = compileFinal createHashMapFromArray [ [CRPC(garage,responseInitGarage), [_garage], _player] call CFUNC(targetEvent); _garage }], - ["get", compileFinal { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - private _garage = _self call ["loadHotGarage", [_uid, false]]; - if (_garage isEqualTo createHashMap) then { - _garage = _self call ["loadHotGarage", [_uid, true]]; - }; - - if (_field isEqualTo "") exitWith { _garage }; - _garage getOrDefault [_field, createHashMap] - }], - ["override", compileFinal { - params [ - ["_uid", "", [""]], - ["_data", createHashMap, [createHashMap]], - ["_save", false, [false]] - ]; - - if (_uid isEqualTo "") exitWith { createHashMap }; - if !(_data isEqualType createHashMap) exitWith { createHashMap }; - - private _garage = _self call ["callHotGarage", ["garage:hot:override", [_uid, toJSON _data]]]; - if (_save && { _garage isNotEqualTo createHashMap }) then { - private _savedGarage = _self call ["callHotGarage", ["garage:hot:save", [_uid]]]; - if (_savedGarage isNotEqualTo createHashMap) then { - _garage = _savedGarage; - } else { - _garage = createHashMap; - }; - }; - - _garage - }], - ["set", compileFinal { - params [ - ["_uid", "", [""]], - ["_field", "", [""]], - ["_value", nil, [0, "", [], false, createHashMap, objNull, grpNull]], - ["_sync", false, [false]] - ]; - - if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap }; - - private _garage = _self call ["get", [_uid, ""]]; - if !(_garage isEqualType createHashMap) exitWith { createHashMap }; - - _garage set [_field, _value]; - private _updatedGarage = _self call ["override", [_uid, _garage, _sync]]; - if !(_updatedGarage isEqualType createHashMap) exitWith { createHashMap }; - if (_updatedGarage isEqualTo createHashMap) exitWith { createHashMap }; - - createHashMapFromArray [[_field, _updatedGarage getOrDefault [_field, _value]]] - }], - ["mset", compileFinal { - params [ - ["_uid", "", [""]], - ["_fieldValuePairs", createHashMap, [createHashMap]], - ["_sync", false, [false]] - ]; - - if (_uid isEqualTo "") exitWith { createHashMap }; - if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap }; - - private _garage = _self call ["get", [_uid, ""]]; - if !(_garage isEqualType createHashMap) exitWith { createHashMap }; - - { _garage set [_x, _y]; } forEach _fieldValuePairs; - private _updatedGarage = _self call ["override", [_uid, _garage, _sync]]; - if !(_updatedGarage isEqualType createHashMap) exitWith { createHashMap }; - if (_updatedGarage isEqualTo createHashMap) exitWith { createHashMap }; - - +_fieldValuePairs - }], ["save", compileFinal { params [["_uid", "", [""]]]; if (_uid isEqualTo "") exitWith { createHashMap }; _self call ["callHotGarage", ["garage:hot:save", [_uid]]] }], - ["remove", compileFinal { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { false }; - - ["garage:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - _isSuccess && { _result isEqualTo "OK" } - }], ["storeVehicle", compileFinal { params [ ["_uid", "", [""]], diff --git a/arma/server/addons/garage/functions/fnc_initVGStore.sqf b/arma/server/addons/garage/functions/fnc_initVGStore.sqf index f4f34a8..a17b9c5 100644 --- a/arma/server/addons/garage/functions/fnc_initVGStore.sqf +++ b/arma/server/addons/garage/functions/fnc_initVGStore.sqf @@ -84,159 +84,11 @@ GVAR(VGBaseStore) = compileFinal createHashMapFromArray [ [CRPC(garage,responseInitVG), [_garage], _player] call CFUNC(targetEvent); _garage }], - ["get", compileFinal { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - private _garage = _self call ["loadHotVGarage", [_uid, false]]; - if (_garage isEqualTo createHashMap) then { - _garage = _self call ["loadHotVGarage", [_uid, true]]; - }; - - if (_field isEqualTo "") exitWith { _garage }; - _garage getOrDefault [_field, []] - }], - ["override", compileFinal { - params [ - ["_uid", "", [""]], - ["_data", createHashMap, [createHashMap]], - ["_save", false, [false]] - ]; - - if (_uid isEqualTo "") exitWith { createHashMap }; - if !(_data isEqualType createHashMap) exitWith { createHashMap }; - - private _garage = _self call ["callHotVGarage", ["owned:garage:hot:override", [_uid, toJSON _data]]]; - if (_save && { _garage isNotEqualTo createHashMap }) then { - private _savedGarage = _self call ["callHotVGarage", ["owned:garage:hot:save", [_uid]]]; - if (_savedGarage isNotEqualTo createHashMap) then { - _garage = _savedGarage; - } else { - _garage = createHashMap; - }; - }; - - _garage - }], - ["set", compileFinal { - params [ - ["_uid", "", [""]], - ["_field", "", [""]], - ["_value", nil, [[], "", 0, false, createHashMap]], - ["_sync", false, [false]] - ]; - - if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap }; - - private _garage = _self call ["loadHotVGarage", [_uid, false]]; - if !(_garage isEqualType createHashMap) exitWith { createHashMap }; - - _garage set [_field, _value]; - private _updatedGarage = _self call ["override", [_uid, _garage, _sync]]; - if !(_updatedGarage isEqualType createHashMap) exitWith { createHashMap }; - if (_updatedGarage isEqualTo createHashMap) exitWith { createHashMap }; - - createHashMapFromArray [[_field, _updatedGarage getOrDefault [_field, _value]]] - }], - ["mset", compileFinal { - params [ - ["_uid", "", [""]], - ["_fieldValuePairs", createHashMap, [createHashMap]], - ["_sync", false, [false]] - ]; - - if (_uid isEqualTo "") exitWith { createHashMap }; - if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap }; - - private _garage = _self call ["loadHotVGarage", [_uid, false]]; - if !(_garage isEqualType createHashMap) exitWith { createHashMap }; - - { _garage set [_x, _y]; } forEach _fieldValuePairs; - private _updatedGarage = _self call ["override", [_uid, _garage, _sync]]; - if !(_updatedGarage isEqualType createHashMap) exitWith { createHashMap }; - if (_updatedGarage isEqualTo createHashMap) exitWith { createHashMap }; - - +_fieldValuePairs - }], ["save", compileFinal { params [["_uid", "", [""]]]; if (_uid isEqualTo "") exitWith { createHashMap }; _self call ["callHotVGarage", ["owned:garage:hot:save", [_uid]]] - }], - ["remove", compileFinal { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { false }; - - ["owned:garage:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - _isSuccess && { _result isEqualTo "OK" } - }], - ["grantVehicles", compileFinal { - params [["_uid", "", [""]], ["_vehicles", [], [[]]], ["_commit", false, [false]]]; - - private _result = createHashMapFromArray [ - ["success", false], - ["message", "Virtual garage grant failed."], - ["patch", createHashMap], - ["granted", []], - ["garage", createHashMap] - ]; - - private _garage = +(_self call ["loadHotVGarage", [_uid, false]]); - if (_garage isEqualTo createHashMap) then { - _garage = GVAR(VGarageModel) call ["defaults", []]; - }; - - private _patch = createHashMap; - private _granted = []; - private _categoriesToSync = []; - - { - private _className = _x getOrDefault ["classname", ""]; - private _category = toLowerANSI (_x getOrDefault ["category", ""]); - - if (_className isEqualTo "") exitWith { - _result set ["message", "Vehicle checkout entry was missing a classname."]; - }; - - if !(_category in ["cars", "armor", "helis", "planes", "naval", "other"]) exitWith { - _result set ["message", format ["Vehicle category '%1' is unsupported.", _category]]; - }; - - private _categoryUnlocks = +(_garage getOrDefault [_category, []]); - _categoryUnlocks pushBackUnique _className; - _garage set [_category, _categoryUnlocks]; - _categoriesToSync pushBackUnique _category; - _granted pushBack (createHashMapFromArray [ - ["classname", _className], - ["category", _category] - ]); - } forEach _vehicles; - - { - private _category = _x; - _patch set [_category, _garage getOrDefault [_category, []]]; - } forEach _categoriesToSync; - - if (_commit) then { - private _savedGarage = _self call ["override", [_uid, _garage, false]]; - if !(_savedGarage isEqualType createHashMap) exitWith { - _result set ["message", "Virtual garage cache update returned invalid data."]; - _result - }; - if (_savedGarage isEqualTo createHashMap) exitWith { - _result set ["message", "Failed to update virtual garage cache."]; - _result - }; - _garage = _savedGarage; - }; - - _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result set ["granted", _granted]; - _result set ["garage", _garage]; - _result }] ]; diff --git a/arma/server/addons/locker/XEH_preInit.sqf b/arma/server/addons/locker/XEH_preInit.sqf index f9747e6..bfd7343 100644 --- a/arma/server/addons/locker/XEH_preInit.sqf +++ b/arma/server/addons/locker/XEH_preInit.sqf @@ -13,40 +13,6 @@ PREP_RECOMPILE_END; GVAR(LockerStore) call ["init", [_uid]]; }] call CFUNC(addEventHandler); -[QGVAR(requestGetLocker), { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" }; - - private _finalData = GVAR(LockerStore) call ["get", [_uid, _field]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(locker,responseSyncLocker), [_finalData], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestSetLocker), { - params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID or Field!" }; - - private _hashMap = GVAR(LockerStore) call ["set", [_uid, _field, _value, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(locker,responseSyncLocker), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestMSetLocker), { - params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" }; - if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid field pairs!" }; - - private _hashMap = GVAR(LockerStore) call ["mset", [_uid, _fieldValuePairs, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(locker,responseSyncLocker), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - [QGVAR(requestSaveLocker), { params [["_uid", "", [""]]]; @@ -68,13 +34,6 @@ PREP_RECOMPILE_END; [CRPC(locker,responseSyncLocker), [_finalData], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); -[QGVAR(requestRemoveLocker), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" }; - GVAR(LockerStore) call ["remove", [_uid]]; -}] call CFUNC(addEventHandler); - [QGVAR(requestInitVA), { params [["_uid", "", [""]]]; @@ -82,40 +41,6 @@ PREP_RECOMPILE_END; GVAR(VAStore) call ["init", [_uid]]; }] call CFUNC(addEventHandler); -[QGVAR(requestGetVA), { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" }; - - private _finalData = GVAR(VAStore) call ["get", [_uid, _field]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(locker,responseSyncVA), [_finalData], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestSetVA), { - params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID or Field!" }; - - private _hashMap = GVAR(VAStore) call ["set", [_uid, _field, _value, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(locker,responseSyncVA), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestMSetVA), { - params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" }; - if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid field pairs!" }; - - private _hashMap = GVAR(VAStore) call ["mset", [_uid, _fieldValuePairs, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(locker,responseSyncVA), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - [QGVAR(requestSaveVA), { params [["_uid", "", [""]]]; @@ -127,9 +52,3 @@ PREP_RECOMPILE_END; [CRPC(locker,responseSyncVA), [_finalData], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); -[QGVAR(requestRemoveVA), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" }; - GVAR(VAStore) call ["remove", [_uid]]; -}] call CFUNC(addEventHandler); diff --git a/arma/server/addons/locker/functions/fnc_initLockerStore.sqf b/arma/server/addons/locker/functions/fnc_initLockerStore.sqf index ac4251a..a98e8e7 100644 --- a/arma/server/addons/locker/functions/fnc_initLockerStore.sqf +++ b/arma/server/addons/locker/functions/fnc_initLockerStore.sqf @@ -67,17 +67,6 @@ GVAR(LockerBaseStore) = compileFinal createHashMapFromArray [ [CRPC(locker,responseInitLocker), [_locker], _player] call CFUNC(targetEvent); _locker }], - ["get", compileFinal { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - private _locker = _self call ["loadHotLocker", [_uid, false]]; - if (_locker isEqualTo createHashMap) then { - _locker = _self call ["loadHotLocker", [_uid, true]]; - }; - - if (_field isEqualTo "") exitWith { _locker }; - _locker getOrDefault [_field, createHashMap] - }], ["override", compileFinal { params [ ["_uid", "", [""]], @@ -100,133 +89,11 @@ GVAR(LockerBaseStore) = compileFinal createHashMapFromArray [ _locker }], - ["set", compileFinal { - params [ - ["_uid", "", [""]], - ["_field", "", [""]], - ["_value", nil, [0, "", [], false, createHashMap, objNull, grpNull]], - ["_sync", false, [false]] - ]; - - if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap }; - - private _locker = _self call ["get", [_uid, ""]]; - if !(_locker isEqualType createHashMap) exitWith { createHashMap }; - - _locker set [_field, _value]; - private _updatedLocker = _self call ["override", [_uid, _locker, _sync]]; - if !(_updatedLocker isEqualType createHashMap) exitWith { createHashMap }; - if (_updatedLocker isEqualTo createHashMap) exitWith { createHashMap }; - - createHashMapFromArray [[_field, _updatedLocker getOrDefault [_field, _value]]] - }], - ["mset", compileFinal { - params [ - ["_uid", "", [""]], - ["_fieldValuePairs", createHashMap, [createHashMap]], - ["_sync", false, [false]] - ]; - - if (_uid isEqualTo "") exitWith { createHashMap }; - if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap }; - - private _locker = _self call ["get", [_uid, ""]]; - if !(_locker isEqualType createHashMap) exitWith { createHashMap }; - - { _locker set [_x, _y]; } forEach _fieldValuePairs; - private _updatedLocker = _self call ["override", [_uid, _locker, _sync]]; - if !(_updatedLocker isEqualType createHashMap) exitWith { createHashMap }; - if (_updatedLocker isEqualTo createHashMap) exitWith { createHashMap }; - - +_fieldValuePairs - }], ["save", compileFinal { params [["_uid", "", [""]]]; if (_uid isEqualTo "") exitWith { createHashMap }; _self call ["callHotLocker", ["locker:hot:save", [_uid]]] - }], - ["remove", compileFinal { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { false }; - - ["locker:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - _isSuccess && { _result isEqualTo "OK" } - }], - ["grantItems", compileFinal { - params [["_uid", "", [""]], ["_items", [], [[]]], ["_commit", false, [false]]]; - - private _result = createHashMapFromArray [ - ["success", false], - ["message", "Locker grant failed."], - ["patch", createHashMap], - ["granted", []], - ["locker", createHashMap] - ]; - - private _locker = +(_self call ["get", [_uid, ""]]); - private _patch = createHashMap; - private _granted = []; - - { - private _className = _x getOrDefault ["classname", ""]; - private _category = toLowerANSI (_x getOrDefault ["category", ""]); - private _quantity = floor ((_x getOrDefault ["quantity", 0]) max 0); - private _lockerCategory = switch (_category) do { - case "item"; - case "attachment": { "item" }; - case "weapon": { "weapon" }; - case "magazine": { "magazine" }; - case "backpack": { "backpack" }; - default { "" }; - }; - - if (_className isEqualTo "" || { _lockerCategory isEqualTo "" } || { _quantity <= 0 }) then { - ["WARN", format ["Skipping invalid locker grant entry: %1 (category: %2)", _className, _category]] call EFUNC(common,log); - } else { - private _entry = +(_locker getOrDefault [_className, createHashMap]); - private _amount = _entry getOrDefault ["amount", 0]; - private _updatedEntry = createHashMapFromArray [ - ["amount", (_amount + _quantity)], - ["classname", _className], - ["category", _lockerCategory] - ]; - - _locker set [_className, _updatedEntry]; - _patch set [_className, _updatedEntry]; - _granted pushBack (createHashMapFromArray [ - ["classname", _className], - ["category", _lockerCategory], - ["quantity", _quantity] - ]); - }; - } forEach _items; - - if ((count (keys _locker)) > 25) exitWith { - _result set ["message", "Locker capacity would exceed 25 unique items. Clear space before checkout."]; - _result - }; - - if (_commit) then { - private _savedLocker = _self call ["override", [_uid, _locker, false]]; - if !(_savedLocker isEqualType createHashMap) exitWith { - _result set ["message", "Locker cache update returned invalid data."]; - _result - }; - if (_savedLocker isEqualTo createHashMap) exitWith { - _result set ["message", "Failed to update locker cache."]; - _result - }; - _locker = _savedLocker; - }; - - _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result set ["granted", _granted]; - _result set ["locker", _locker]; - _result }] ]; diff --git a/arma/server/addons/locker/functions/fnc_initVAStore.sqf b/arma/server/addons/locker/functions/fnc_initVAStore.sqf index acbd598..567010b 100644 --- a/arma/server/addons/locker/functions/fnc_initVAStore.sqf +++ b/arma/server/addons/locker/functions/fnc_initVAStore.sqf @@ -82,152 +82,11 @@ GVAR(VABaseStore) = compileFinal createHashMapFromArray [ [CRPC(locker,responseInitVA), [_arsenal], _player] call CFUNC(targetEvent); _arsenal }], - ["get", compileFinal { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - private _arsenal = _self call ["loadHotVArsenal", [_uid, false]]; - if (_arsenal isEqualTo createHashMap) then { - _arsenal = _self call ["loadHotVArsenal", [_uid, true]]; - }; - - if (_field isEqualTo "") exitWith { _arsenal }; - _arsenal getOrDefault [_field, []] - }], - ["override", compileFinal { - params [ - ["_uid", "", [""]], - ["_data", createHashMap, [createHashMap]], - ["_save", false, [false]] - ]; - - if (_uid isEqualTo "") exitWith { createHashMap }; - if !(_data isEqualType createHashMap) exitWith { createHashMap }; - - private _arsenal = _self call ["callHotVArsenal", ["owned:locker:hot:override", [_uid, toJSON _data]]]; - if (_save && { _arsenal isNotEqualTo createHashMap }) then { - private _savedArsenal = _self call ["callHotVArsenal", ["owned:locker:hot:save", [_uid]]]; - if (_savedArsenal isNotEqualTo createHashMap) then { - _arsenal = _savedArsenal; - } else { - _arsenal = createHashMap; - }; - }; - - _arsenal - }], - ["set", compileFinal { - params [ - ["_uid", "", [""]], - ["_field", "", [""]], - ["_value", nil, [[], "", 0, false, createHashMap]], - ["_sync", false, [false]] - ]; - - if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap }; - - private _arsenal = _self call ["get", [_uid, ""]]; - if !(_arsenal isEqualType createHashMap) exitWith { createHashMap }; - - _arsenal set [_field, _value]; - private _updatedArsenal = _self call ["override", [_uid, _arsenal, _sync]]; - if !(_updatedArsenal isEqualType createHashMap) exitWith { createHashMap }; - if (_updatedArsenal isEqualTo createHashMap) exitWith { createHashMap }; - - createHashMapFromArray [[_field, _updatedArsenal getOrDefault [_field, _value]]] - }], - ["mset", compileFinal { - params [ - ["_uid", "", [""]], - ["_fieldValuePairs", createHashMap, [createHashMap]], - ["_sync", false, [false]] - ]; - - if (_uid isEqualTo "") exitWith { createHashMap }; - if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap }; - - private _arsenal = _self call ["get", [_uid, ""]]; - if !(_arsenal isEqualType createHashMap) exitWith { createHashMap }; - - { _arsenal set [_x, _y]; } forEach _fieldValuePairs; - private _updatedArsenal = _self call ["override", [_uid, _arsenal, _sync]]; - if !(_updatedArsenal isEqualType createHashMap) exitWith { createHashMap }; - if (_updatedArsenal isEqualTo createHashMap) exitWith { createHashMap }; - - +_fieldValuePairs - }], ["save", compileFinal { params [["_uid", "", [""]]]; if (_uid isEqualTo "") exitWith { createHashMap }; _self call ["callHotVArsenal", ["owned:locker:hot:save", [_uid]]] - }], - ["remove", compileFinal { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { false }; - - ["owned:locker:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - _isSuccess && { _result isEqualTo "OK" } - }], - ["unlockItems", compileFinal { - params [["_uid", "", [""]], ["_items", [], [[]]], ["_commit", false, [false]]]; - - private _result = createHashMapFromArray [ - ["success", false], - ["message", "VA unlock failed."], - ["patch", createHashMap], - ["arsenal", createHashMap] - ]; - - private _arsenal = +(_self call ["get", [_uid, ""]]); - if (_arsenal isEqualTo createHashMap) then { - _arsenal = GVAR(VArsenalModel) call ["defaults", []]; - }; - private _patch = createHashMap; - private _categoriesToSync = []; - - { - private _item = _x; - private _className = _item getOrDefault ["classname", ""]; - private _category = toLowerANSI (_item getOrDefault ["category", ""]); - private _arsenalCategory = switch (_category) do { - case "item": { "items" }; - case "weapon": { "weapons" }; - case "magazine": { "magazines" }; - case "backpack": { "backpacks" }; - default { "items" }; - }; - - private _categoryUnlocks = +(_arsenal getOrDefault [_arsenalCategory, []]); - _categoryUnlocks pushBackUnique _className; - _arsenal set [_arsenalCategory, _categoryUnlocks]; - _categoriesToSync pushBackUnique _arsenalCategory; - } forEach _items; - - { - private _category = _x; - private _categoryUnlocks = _arsenal getOrDefault [_category, []]; - _patch set [_category, _categoryUnlocks]; - } forEach _categoriesToSync; - - if (_commit) then { - private _savedArsenal = _self call ["override", [_uid, _arsenal, false]]; - if !(_savedArsenal isEqualType createHashMap) exitWith { - _result set ["message", "Virtual arsenal cache update returned invalid data."]; - _result - }; - if (_savedArsenal isEqualTo createHashMap) exitWith { - _result set ["message", "Failed to update virtual arsenal cache."]; - _result - }; - _arsenal = _savedArsenal; - }; - - _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result set ["arsenal", _arsenal]; - _result }] ]; diff --git a/arma/server/addons/main/functions/fnc_initStores.sqf b/arma/server/addons/main/functions/fnc_initStores.sqf index 801605a..69f5259 100644 --- a/arma/server/addons/main/functions/fnc_initStores.sqf +++ b/arma/server/addons/main/functions/fnc_initStores.sqf @@ -42,6 +42,7 @@ if (isNil QEGVAR(locker,LockerStore)) then { call EFUNC(locker,initLockerStore); if (isNil QEGVAR(locker,VAStore)) then { call EFUNC(locker,initVAStore); }; // Org +if (isNil QEGVAR(org,OrgPayloadBuilder)) then { call EFUNC(org,initPayloadBuilder); }; if (isNil QEGVAR(org,OrgStore)) then { call EFUNC(org,initOrgStore); }; // Store diff --git a/arma/server/addons/org/XEH_PREP.hpp b/arma/server/addons/org/XEH_PREP.hpp index dc78ebd..6a5b9f9 100644 --- a/arma/server/addons/org/XEH_PREP.hpp +++ b/arma/server/addons/org/XEH_PREP.hpp @@ -1,3 +1,2 @@ +PREP(initPayloadBuilder); PREP(initOrgStore); -PREP(memberService); -PREP(treasuryService); diff --git a/arma/server/addons/org/XEH_preInit.sqf b/arma/server/addons/org/XEH_preInit.sqf index 1c098d2..fcc4c80 100644 --- a/arma/server/addons/org/XEH_preInit.sqf +++ b/arma/server/addons/org/XEH_preInit.sqf @@ -52,38 +52,6 @@ PREP_RECOMPILE_END; [CRPC(org,responseCreateOrg), [_result], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); -[QGVAR(requestGetOrg), { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; - - private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]]; - private _finalData = GVAR(OrgStore) call ["get", [_key, _field]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(org,responseSyncOrg), [_finalData], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestSetOrg), { - params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID or Field!" }; - - private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]]; - GVAR(OrgStore) call ["set", [_key, _field, _value, _sync]]; -}] call CFUNC(addEventHandler); - -[QGVAR(requestMSetOrg), { - params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; - if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid field pairs!" }; - - private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]]; - - GVAR(OrgStore) call ["mset", [_key, _fieldValuePairs, _sync]]; -}] call CFUNC(addEventHandler); - [QGVAR(requestAssignCreditLine), { params [ ["_uid", "", [""]], @@ -117,24 +85,6 @@ PREP_RECOMPILE_END; ]], _requester] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); -[QGVAR(requestSaveOrg), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; - - private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]]; - GVAR(OrgStore) call ["saveById", [_key]]; -}] call CFUNC(addEventHandler); - -[QGVAR(requestRemoveOrg), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; - - private _key = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]]; - GVAR(OrgStore) call ["delete", [_key]]; -}] call CFUNC(addEventHandler); - [QGVAR(requestLeaveOrg), { params [["_uid", "", [""]]]; diff --git a/arma/server/addons/org/functions/fnc_initOrgStore.sqf b/arma/server/addons/org/functions/fnc_initOrgStore.sqf index 67fc084..cf7ad81 100644 --- a/arma/server/addons/org/functions/fnc_initOrgStore.sqf +++ b/arma/server/addons/org/functions/fnc_initOrgStore.sqf @@ -21,9 +21,6 @@ * call forge_server_org_fnc_initOrgStore */ -if (isNil QGVAR(OrgMembershipService)) then { call FUNC(memberService); }; -if (isNil QGVAR(OrgTreasuryService)) then { call FUNC(treasuryService); }; - #pragma hemtt ignore_variables ["_self"] GVAR(OrgModel) = compileFinal createHashMapObject [[ ["#type", "OrgModel"], @@ -190,6 +187,31 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _self call ["syncHotOrg", [_data]] }], + ["callHotOrgEnvelope", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + if (_function isEqualTo "") exitWith { createHashMap }; + + [_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !(_isSuccess) exitWith { createHashMap }; + if !(_result isEqualType "") exitWith { createHashMap }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Org extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log); + createHashMap + }; + + private _data = fromJSON _result; + if !(_data isEqualType createHashMap) exitWith { createHashMap }; + + if ("org" in _data) then { + private _syncedOrg = _self call ["syncHotOrg", [_data getOrDefault ["org", createHashMap]]]; + if (_syncedOrg isNotEqualTo createHashMap) then { + _data set ["org", _syncedOrg]; + }; + }; + + _data + }], ["syncHotOrg", compileFinal { params [["_org", createHashMap, [createHashMap]]]; @@ -211,10 +233,48 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ if (_orgID isEqualTo "") then { _orgID = "default"; }; _orgID }], - ["loadForUid", compileFinal { - params [["_uid", "", [""]]]; - private _orgID = _self call ["resolveOrgIdForUid", [_uid]]; - _self call ["loadById", [_orgID]] + ["resolveActorName", compileFinal { + params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]]; + + private _memberName = _actor getOrDefault ["name", ""]; + if (_memberName isEqualTo "" && { _player isNotEqualTo objNull }) then { + _memberName = name _player; + }; + if (_memberName isEqualTo "") then { _memberName = "Unknown"; }; + _memberName + }], + ["applyActorOrganization", compileFinal { + params [["_uid", "", [""]], ["_orgID", "", [""]], ["_actor", createHashMap, [createHashMap]]]; + + if (_uid isEqualTo "" || { _orgID isEqualTo "" }) exitWith { createHashMap }; + + private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, false]]; + private _updatedActor = EGVAR(actor,ActorStore) call ["get", [_uid, ""]]; + if ( + !(_updatedActor isEqualType createHashMap) + || { _updatedActor isEqualTo createHashMap } + || { (_updatedActor getOrDefault ["organization", ""]) isNotEqualTo _orgID } + ) then { + private _forcedActor = +_actor; + if !(_forcedActor isEqualType createHashMap) then { + _forcedActor = EGVAR(actor,ActorModel) call ["defaults", []]; + _forcedActor set ["uid", _uid]; + }; + + _forcedActor set ["organization", _orgID]; + _updatedActor = EGVAR(actor,ActorStore) call ["override", [_uid, _forcedActor, false]]; + if (_updatedActor isEqualType createHashMap && { _updatedActor isNotEqualTo createHashMap }) then { + _actorPatch = createHashMapFromArray [["organization", _orgID]]; + }; + }; + + if ( + !(_updatedActor isEqualType createHashMap) + || { _updatedActor isEqualTo createHashMap } + || { (_updatedActor getOrDefault ["organization", ""]) isNotEqualTo _orgID } + ) exitWith { createHashMap }; + + _actorPatch }], ["loadHotOrg", compileFinal { params [["_orgID", "", [""]], ["_initialize", false, [false]]]; @@ -235,80 +295,6 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ if (_field isEqualTo "") exitWith { _org }; _org getOrDefault [_field, createHashMap] }], - ["override", compileFinal { - params [ - ["_orgID", "", [""]], - ["_org", createHashMap, [createHashMap]], - ["_save", false, [false]] - ]; - - if (_orgID isEqualTo "") exitWith { createHashMap }; - if !(_org isEqualType createHashMap) exitWith { createHashMap }; - - private _normalizedOrg = +_org; - _normalizedOrg set ["id", _normalizedOrg getOrDefault ["id", _orgID]]; - - private _result = _self call ["callHotOrg", ["org:hot:override", [_orgID, toJSON _normalizedOrg]]]; - if (_save && { _result isNotEqualTo createHashMap }) then { - private _savedOrg = _self call ["callHotOrg", ["org:hot:save", [_orgID]]]; - if (_savedOrg isNotEqualTo createHashMap) then { - _result = _savedOrg; - } else { - _result = createHashMap; - }; - }; - - _result - }], - ["set", compileFinal { - params [ - ["_orgID", "", [""]], - ["_field", "", [""]], - ["_value", nil, [[], "", 0, false, createHashMap]], - ["_sync", false, [false]] - ]; - - if (_orgID isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap }; - - private _org = _self call ["get", [_orgID, ""]]; - if !(_org isEqualType createHashMap) exitWith { createHashMap }; - - _org set [_field, _value]; - private _updatedOrg = _self call ["override", [_orgID, _org, _sync]]; - if !(_updatedOrg isEqualType createHashMap) exitWith { createHashMap }; - if (_updatedOrg isEqualTo createHashMap) exitWith { createHashMap }; - - createHashMapFromArray [[_field, _updatedOrg getOrDefault [_field, _value]]] - }], - ["mset", compileFinal { - params [ - ["_orgID", "", [""]], - ["_fieldValuePairs", createHashMap, [createHashMap]], - ["_sync", false, [false]] - ]; - - if (_orgID isEqualTo "") exitWith { createHashMap }; - if !(_fieldValuePairs isEqualType createHashMap) exitWith { createHashMap }; - - private _org = _self call ["get", [_orgID, ""]]; - if !(_org isEqualType createHashMap) exitWith { createHashMap }; - - { _org set [_x, _y]; } forEach _fieldValuePairs; - private _updatedOrg = _self call ["override", [_orgID, _org, _sync]]; - if !(_updatedOrg isEqualType createHashMap) exitWith { createHashMap }; - if (_updatedOrg isEqualTo createHashMap) exitWith { createHashMap }; - - +_fieldValuePairs - }], - ["verifyMember", compileFinal { - GVAR(OrgMembershipService) call ["verifyMember", _this] - }], - ["addMember", compileFinal { - GVAR(OrgMembershipService) call ["addMember", _this] - }], - ["removeMember", compileFinal { - GVAR(OrgMembershipService) call ["removeMember", _this] - }], ["delete", compileFinal { params [["_orgID", "", [""]]]; @@ -332,158 +318,213 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result set ["success", true]; _result }], - ["restoreDefaultMembership", compileFinal { - GVAR(OrgMembershipService) call ["restoreDefaultMembership", _this] + ["ensureMember", compileFinal { + params [["_orgID", "", [""]], ["_uid", "", [""]], ["_memberName", "", [""]]]; + + if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap }; + + private _context = createHashMapFromArray [ + ["orgId", _orgID], + ["memberUid", _uid], + ["memberName", _memberName] + ]; + + _self call ["callHotOrg", ["org:hot:ensure_member", [toJSON _context]]] }], ["leave", compileFinal { - GVAR(OrgMembershipService) call ["leave", _this] + 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", "default"]; + private _memberName = _self call ["resolveActorName", [_uid, _player, _actor]]; + private _context = createHashMapFromArray [ + ["requesterUid", _uid], + ["requesterName", _memberName], + ["orgId", _orgID] + ]; + + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:leave", [toJSON _context]]]; + if (_envelope isEqualTo createHashMap) exitWith { + _result set ["message", "Unable to leave the organization."]; + _result + }; + + private _actorOrg = _envelope getOrDefault ["actorOrganization", "default"]; + private _actorPatch = _self call ["applyActorOrganization", [_uid, _actorOrg, _actor]]; + if (_actorPatch isEqualTo createHashMap) exitWith { + _result set ["message", "Failed to restore default organization membership."]; + _result + }; + + _result set ["success", true]; + _result set ["message", _envelope getOrDefault ["message", "You returned to the default organization."]]; + _result set ["actorPatch", _actorPatch]; + _result set ["notification", ["info", "Organization Left", _result get "message", 6000]]; + _result }], ["disband", compileFinal { - GVAR(OrgMembershipService) call ["disband", _this] + params [["_uid", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", ""], + ["members", []] + ]; + + 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", "default"]; + private _memberName = _self call ["resolveActorName", [_uid, _player, _actor]]; + private _context = createHashMapFromArray [ + ["requesterUid", _uid], + ["requesterName", _memberName], + ["orgId", _orgID] + ]; + + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:disband", [toJSON _context]]]; + if (_envelope isEqualTo createHashMap) exitWith { + _result set ["message", "Failed to disband organization."]; + _result + }; + + private _memberResults = []; + { + private _memberUid = _x getOrDefault ["uid", ""]; + if (_memberUid isEqualTo "") then { continue; }; + + private _memberActor = EGVAR(actor,Registry) getOrDefault [_memberUid, createHashMap]; + private _actorPatch = _self call ["applyActorOrganization", [_memberUid, _x getOrDefault ["actorOrganization", "default"], _memberActor]]; + if (_actorPatch isEqualTo createHashMap) then { + ["WARNING", format ["Failed to restore actor organization for %1 after org disband.", _memberUid]] call EFUNC(common,log); + }; + + private _responseMessage = _x getOrDefault ["message", _envelope getOrDefault ["message", "Organization disbanded."]]; + private _notificationParams = [ + ["warning", "Organization Disbanded", _responseMessage, 6000], + ["success", "Organization Disbanded", _responseMessage, 6000] + ] select (_x getOrDefault ["requester", false]); + + _memberResults pushBack (createHashMapFromArray [ + ["uid", _memberUid], + ["requester", _x getOrDefault ["requester", false]], + ["message", _responseMessage], + ["notification", _notificationParams], + ["actorPatch", _actorPatch] + ]); + } forEach (_envelope getOrDefault ["members", []]); + + _result set ["success", true]; + _result set ["message", _envelope getOrDefault ["message", "Organization disbanded."]]; + _result set ["members", _memberResults]; + _result }], ["assignCreditLine", compileFinal { - GVAR(OrgTreasuryService) call ["assignCreditLine", _this] + 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 _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer); + private _requesterIsDefaultOrgCeo = ( + _requesterPlayer isNotEqualTo objNull + && { _orgID isEqualTo "default" } + && { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" } + ); + + private _context = createHashMapFromArray [ + ["requesterUid", _requesterUid], + ["orgId", _orgID], + ["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo], + ["memberUid", _memberUid], + ["memberName", _memberName], + ["amount", _amount] + ]; + + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:assign_credit_line", [toJSON _context]]]; + if (_envelope isEqualTo createHashMap) exitWith { + _result set ["message", "Unable to assign credit line."]; + _result + }; + + _result set ["success", true]; + _result set ["message", _envelope getOrDefault ["message", "Credit line assigned."]]; + _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; + _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; + _result }], ["buildPortalPayload", compileFinal { params [["_uid", "", [""]]]; - if (_uid isEqualTo "") exitWith { createHashMap }; - - private _player = [_uid] call EFUNC(common,getPlayer); - if (isNull _player) exitWith { createHashMap }; - - private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; - private _orgID = _actor getOrDefault ["organization", "default"]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; - - private _org = _self call ["loadById", [_orgID]]; - if (_org isEqualTo createHashMap) then { - _org = _self call ["init", [_uid]]; - }; - if (_org isEqualTo createHashMap) exitWith { createHashMap }; - - // Ensure the requesting player's membership is present in the cached roster - // before shaping the portal payload. This prevents stale org caches from - // omitting the current member while still resolving owner metadata. - _org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]]; - - private _name = _org getOrDefault ["name", ""]; - private _id = _org getOrDefault ["id", _orgID]; - private _ownerUid = _org getOrDefault ["owner", ""]; - private _funds = _org getOrDefault ["funds", 0]; - private _reputation = _org getOrDefault ["reputation", 0]; - private _creditLinesRaw = _org getOrDefault ["credit_lines", createHashMap]; - private _assetsRaw = _org getOrDefault ["assets", createHashMap]; - private _fleetRaw = _org getOrDefault ["fleet", createHashMap]; - private _membersRaw = _org getOrDefault ["members", createHashMap]; - private _isDefaultOrg = (_org getOrDefault ["default", false]) - || { toLower _id isEqualTo "default" } - || { toLower _ownerUid isEqualTo "server" }; - - private _playerName = name _player; - private _playerVar = vehicleVarName _player; - private _sessionRole = "Member"; - private _sessionIsCeo = _isDefaultOrg && { _playerVar isEqualTo "ceo" }; - private _ownerName = ["", "Server"] select (toLower _ownerUid isEqualTo "server"); - - private _membersList = []; - { - private _memberData = _y; - private _memberName = _memberData getOrDefault ["name", "Unknown"]; - private _memberUid = _memberData getOrDefault ["uid", ""]; - - if (_memberUid isEqualTo _ownerUid && { _ownerName isEqualTo "" }) then { _ownerName = _memberName; }; - if (_memberUid isEqualTo _uid) then { _sessionRole = "Member"; }; - - _membersList pushBack (createHashMapFromArray [ - ["uid", _memberUid], - ["name", _memberName] - ]); - } forEach _membersRaw; - - if (_ownerName isEqualTo "" && { _ownerUid isEqualTo _uid }) then { _ownerName = _playerName; }; - if (_ownerName isEqualTo "" && { _ownerUid isNotEqualTo "" }) then { _ownerName = "Unknown Owner"; }; - if (_ownerUid isEqualTo _uid) then { _sessionRole = "Leader"; }; - - private _assetsList = []; - { - private _category = _x; - { - private _assetData = _y; - private _className = _assetData getOrDefault ["classname", ""]; - private _displayName = _className; - { - private _cfg = _x >> _className; - if (isClass _cfg) exitWith { - private _resolvedName = getText (_cfg >> "displayName"); - if (_resolvedName isNotEqualTo "") then { _displayName = _resolvedName; }; - }; - } forEach [ - configFile >> "CfgWeapons", - configFile >> "CfgMagazines", - configFile >> "CfgVehicles", - configFile >> "CfgGlasses" - ]; - - _assetsList pushBack (createHashMapFromArray [ - ["name", _displayName], - ["type", _assetData getOrDefault ["type", _category]], - ["quantity", str (_assetData getOrDefault ["quantity", 0])] - ]); - } forEach _y; - } forEach _assetsRaw; - - private _fleetList = []; - { - private _vehicleData = _y; - _fleetList pushBack (createHashMapFromArray [ - ["name", _vehicleData getOrDefault ["name", "Unknown Vehicle"]], - ["type", _vehicleData getOrDefault ["type", "other"]], - ["status", _vehicleData getOrDefault ["status", "Unknown"]], - ["damage", _vehicleData getOrDefault ["damage", "0%"]] - ]); - } 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], - ["actorUid", _uid], - ["role", _sessionRole], - ["ceo", _sessionIsCeo] - ]], - ["portalData", createHashMapFromArray [ - ["org", createHashMapFromArray [ - ["name", _name], - ["tag", _id], - ["owner", _ownerName], - ["ownerUid", _ownerUid], - ["isDefault", _isDefaultOrg] - ]], - ["funds", _funds], - ["reputation", _reputation], - ["creditLines", _creditLinesList], - ["members", _membersList], - ["fleet", _fleetList], - ["assets", _assetsList], - ["activity", []] - ]] - ] - }], - ["buildChargeResult", compileFinal { - GVAR(OrgTreasuryService) call ["buildChargeResult", _this] + GVAR(OrgPayloadBuilder) call ["buildPortalPayload", [_uid]] }], ["chargeCheckout", compileFinal { - GVAR(OrgTreasuryService) call ["chargeCheckout", _this] + params [["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]], ["_source", "org_funds", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to process organization payment."], + ["patch", createHashMap], + ["memberUids", []] + ]; + + private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; + private _orgID = _requesterActor getOrDefault ["organization", "default"]; + if (_orgID isEqualTo "") then { _orgID = "default"; }; + + private _requesterIsDefaultOrgCeo = ( + _requesterPlayer isNotEqualTo objNull + && { _orgID isEqualTo "default" } + && { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" } + ); + + private _context = createHashMapFromArray [ + ["requesterUid", _requesterUid], + ["orgId", _orgID], + ["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo], + ["source", _source], + ["amount", _amount], + ["commit", _commit] + ]; + + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:charge_checkout", [toJSON _context]]]; + if (_envelope isEqualTo createHashMap) exitWith { _result }; + + _result set ["success", true]; + _result set ["message", _envelope getOrDefault ["message", ""]]; + _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; + _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; + _result }], ["saveById", compileFinal { params [["_orgID", "", [""]]]; @@ -515,42 +556,29 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ }; if (_resolvedOrgID isEqualTo "") then { _resolvedOrgID = "default"; }; - private _org = _self call ["loadById", [_resolvedOrgID]]; - if (_org isEqualTo createHashMap) exitWith { - _result set ["message", "Organization data is unavailable for asset updates."]; - _result + private _context = createHashMapFromArray [ + ["requesterUid", _requesterUid], + ["orgId", _resolvedOrgID], + ["commit", _commit] + ]; + private _assetSeeds = _assets apply { + createHashMapFromArray [ + ["classname", _x getOrDefault ["classname", ""]], + ["category", toLowerANSI (_x getOrDefault ["category", "items"])], + ["quantity", floor ((_x getOrDefault ["quantity", 0]) max 0)] + ] }; - private _assetMap = +(_org getOrDefault ["assets", createHashMap]); - - { - private _className = _x getOrDefault ["classname", ""]; - private _category = toLowerANSI (_x getOrDefault ["category", "items"]); - private _quantity = floor ((_x getOrDefault ["quantity", 0]) max 0); - if (_className isEqualTo "" || { _quantity <= 0 }) then { continue; }; - - private _categoryMap = +(_assetMap getOrDefault [_category, createHashMap]); - private _assetEntry = +(_categoryMap getOrDefault [_className, createHashMap]); - - private _existingQuantity = _assetEntry getOrDefault ["quantity", 0]; - _categoryMap set [_className, createHashMapFromArray [ - ["classname", _className], - ["type", _category], - ["quantity", (_existingQuantity + _quantity)] - ]]; - _assetMap set [_category, _categoryMap]; - } forEach _assets; - - private _patch = _self call ["mset", [_resolvedOrgID, createHashMapFromArray [["assets", _assetMap]], false]]; - if (_patch isEqualTo createHashMap) exitWith { + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:add_assets", [toJSON _context, toJSON _assetSeeds]]]; + if (_envelope isEqualTo createHashMap) exitWith { _result set ["message", "Failed to update organization asset cache."]; _result }; _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result set ["memberUids", GVAR(OrgTreasuryService) call ["resolveOrgMemberUids", [_org, _requesterUid]]]; + _result set ["message", _envelope getOrDefault ["message", ""]]; + _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; + _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; _result }], ["addFleetVehicles", compileFinal { @@ -576,52 +604,28 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ }; if (_resolvedOrgID isEqualTo "") then { _resolvedOrgID = "default"; }; - private _org = _self call ["loadById", [_resolvedOrgID]]; - if (_org isEqualTo createHashMap) exitWith { - _result set ["message", "Organization data is unavailable for fleet updates."]; - _result + private _context = createHashMapFromArray [ + ["requesterUid", _requesterUid], + ["orgId", _resolvedOrgID], + ["commit", _commit] + ]; + private _fleetSeeds = _vehicles apply { + createHashMapFromArray [ + ["classname", _x getOrDefault ["classname", ""]], + ["category", toLowerANSI (_x getOrDefault ["category", "other"])] + ] }; - private _fleet = +(_org getOrDefault ["fleet", createHashMap]); - private _fleetIndex = count (keys _fleet); - - { - private _className = _x getOrDefault ["classname", ""]; - private _category = toLowerANSI (_x getOrDefault ["category", "other"]); - if (_className isEqualTo "") exitWith { - _result set ["message", "Vehicle fleet entry was missing a classname."]; - }; - - private _fleetKey = format ["%1_%2", _className, _fleetIndex]; - while { _fleetKey in (keys _fleet) } do { - _fleetIndex = _fleetIndex + 1; - _fleetKey = format ["%1_%2", _className, _fleetIndex]; - }; - - private _displayName = getText (configFile >> "CfgVehicles" >> _className >> "displayName"); - if (_displayName isEqualTo "") then { _displayName = _className; }; - - _fleet set [_fleetKey, createHashMapFromArray [ - ["classname", _className], - ["name", _displayName], - ["type", _category], - ["status", "Ready"], - ["damage", "0%"] - ]]; - - _fleetIndex = _fleetIndex + 1; - } forEach _vehicles; - - private _patch = _self call ["mset", [_resolvedOrgID, createHashMapFromArray [["fleet", _fleet]], false]]; - if (_patch isEqualTo createHashMap) exitWith { + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:add_fleet", [toJSON _context, toJSON _fleetSeeds]]]; + if (_envelope isEqualTo createHashMap) exitWith { _result set ["message", "Failed to update organization fleet cache."]; _result }; _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result set ["memberUids", GVAR(OrgTreasuryService) call ["resolveOrgMemberUids", [_org, _requesterUid]]]; + _result set ["message", _envelope getOrDefault ["message", ""]]; + _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; + _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; _result }], ["loadById", compileFinal { @@ -646,13 +650,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result }; - private _player = [_uid] call EFUNC(common,getPlayer); private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; private _existingOrgID = _actor getOrDefault ["organization", ""]; - if (_existingOrgID isNotEqualTo "" && { toLower _existingOrgID isNotEqualTo "default" }) exitWith { - _result set ["message", "Player already belongs to an organization."]; - _result - }; private _orgID = _actor getOrDefault ["phone_number", ""]; if (_orgID isEqualTo "") exitWith { @@ -660,80 +659,29 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result }; - ["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_existsResult", "_existsSuccess"]; - if (!_existsSuccess) exitWith { - _result set ["message", "Unable to verify organization ID availability."]; - _result - }; - - if (_existsResult isEqualTo "true") exitWith { - _result set ["message", "An organization already exists for this phone number."]; - _result - }; - - private _org = createHashMapFromArray [ - ["id", _orgID], - ["owner", _uid], - ["name", _orgName], - ["funds", 0], - ["reputation", 0], - ["credit_lines", createHashMap], - ["members", createHashMap] + private _context = createHashMapFromArray [ + ["requesterUid", _uid], + ["requesterName", _self call ["resolveActorName", [_uid, [_uid] call EFUNC(common,getPlayer), _actor]]], + ["orgId", _orgID], + ["orgName", _orgName], + ["existingOrgId", _existingOrgID] ]; - private _json = _self call ["toJSON", [_org]]; - ["org:create", [_orgID, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"]; - if (!_createSuccess) exitWith { - _result set ["message", format ["Failed to create organization: %1", _createResult]]; + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:register", [toJSON _context]]]; + if (_envelope isEqualTo createHashMap) exitWith { + _result set ["message", "Organization registration failed."]; _result }; - if (_createResult isNotEqualTo "") then { - _org = _self call ["toHashMap", [_createResult]]; - }; - - _org set ["members", createHashMap]; - _org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]]; - - 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, false]]; - private _updatedActor = EGVAR(actor,ActorStore) call ["get", [_uid, ""]]; - if ( - !(_updatedActor isEqualType createHashMap) - || { _updatedActor isEqualTo createHashMap } - || { (_updatedActor getOrDefault ["organization", ""]) isNotEqualTo _orgID } - ) then { - private _forcedActor = +_actor; - if !(_forcedActor isEqualType createHashMap) then { - _forcedActor = EGVAR(actor,ActorModel) call ["defaults", []]; - _forcedActor set ["uid", _uid]; - }; - - _forcedActor set ["organization", _orgID]; - _updatedActor = EGVAR(actor,ActorStore) call ["override", [_uid, _forcedActor, false]]; - if (_updatedActor isEqualType createHashMap && { _updatedActor isNotEqualTo createHashMap }) then { - _actorPatch = createHashMapFromArray [["organization", _orgID]]; - }; - }; - - if ( - !(_updatedActor isEqualType createHashMap) - || { _updatedActor isEqualTo createHashMap } - || { (_updatedActor getOrDefault ["organization", ""]) isNotEqualTo _orgID } - ) exitWith { + private _actorPatch = _self call ["applyActorOrganization", [_uid, _envelope getOrDefault ["actorOrganization", _orgID], _actor]]; + if (_actorPatch isEqualTo createHashMap) exitWith { _result set ["message", "Failed to assign the player to the new organization."]; _result }; - _org = _self call ["override", [_orgID, _org, false]]; _result set ["success", true]; - _result set ["org", _org]; + _result set ["message", _envelope getOrDefault ["message", ""]]; + _result set ["org", _envelope getOrDefault ["org", createHashMap]]; _result set ["actorPatch", _actorPatch]; _result }], @@ -754,9 +702,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _orgID = "default"; }; - private _finalOwner = _finalOrg getOrDefault ["owner", ""]; - if (_orgID isEqualTo "default" || { _finalOwner isEqualTo _uid }) then { - _finalOrg = _self call ["verifyMember", [_finalOrg, _orgID, _uid, _player, _actor]]; + private _verifiedOrg = _self call ["ensureMember", [_orgID, _uid, _self call ["resolveActorName", [_uid, _player, _actor]]]]; + if (_verifiedOrg isNotEqualTo createHashMap) then { + _finalOrg = _verifiedOrg; }; [CRPC(org,responseInitOrg), [_finalOrg], _player] call CFUNC(targetEvent); diff --git a/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf b/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf new file mode 100644 index 0000000..2a049f1 --- /dev/null +++ b/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf @@ -0,0 +1,213 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initPayloadBuilder.sqf + * Author: IDSolutions + * Date: 2026-04-02 + * Public: No + * + * Description: + * Initializes the org payload builder for portal/read-model shaping. + * Keeps hydrate construction out of OrgStore so the store can focus on + * extension-backed org operations and actor coordination. + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(OrgPayloadBuilder) = createHashMapObject [[ + ["#type", "OrgPayloadBuilder"], + ["resolveOrgForUid", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _orgID = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]]; + private _org = GVAR(OrgStore) call ["loadById", [_orgID]]; + if (_org isEqualTo createHashMap) then { + _org = GVAR(OrgStore) call ["init", [_uid]]; + }; + + _org + }], + ["resolveOwnerName", compileFinal { + params [["_ownerUid", "", [""]], ["_uid", "", [""]], ["_playerName", "", [""]], ["_membersRaw", createHashMap, [createHashMap]]]; + + private _ownerName = ["", "Server"] select (toLowerANSI _ownerUid isEqualTo "server"); + { + private _memberData = _y; + private _memberUid = _memberData getOrDefault ["uid", ""]; + if (_memberUid isEqualTo _ownerUid && { _ownerName isEqualTo "" }) exitWith { + _ownerName = _memberData getOrDefault ["name", "Unknown"]; + }; + } forEach _membersRaw; + + if (_ownerName isEqualTo "" && { _ownerUid isEqualTo _uid }) then { _ownerName = _playerName; }; + if (_ownerName isEqualTo "" && { _ownerUid isNotEqualTo "" }) then { _ownerName = "Unknown Owner"; }; + if !(_ownerName isEqualType "") then { _ownerName = str _ownerName; }; + _ownerName + }], + ["buildMembersList", compileFinal { + params [["_membersRaw", createHashMap, [createHashMap]], ["_uid", "", [""]], ["_ownerUid", "", [""]]]; + + private _sessionRole = "Member"; + private _membersList = []; + + { + private _memberData = _y; + private _memberName = _memberData getOrDefault ["name", "Unknown"]; + private _memberUid = _memberData getOrDefault ["uid", ""]; + + if (_memberUid isEqualTo _uid) then { _sessionRole = "Member"; }; + if (_memberUid isEqualTo _ownerUid) then { _sessionRole = ["Member", "Leader"] select (_ownerUid isEqualTo _uid); }; + + _membersList pushBack [ + ["uid", _memberUid], + ["name", _memberName] + ]; + } forEach _membersRaw; + + createHashMapFromArray [ + ["members", _membersList], + ["sessionRole", _sessionRole] + ] + }], + ["resolveDisplayName", compileFinal { + params [["_className", "", [""]], ["_configRoots", [], [[]]]]; + + if (_className isEqualTo "") exitWith { "" }; + + private _displayName = _className; + { + private _cfg = _x >> _className; + if (isClass _cfg) exitWith { + private _resolvedName = getText (_cfg >> "displayName"); + if (_resolvedName isNotEqualTo "") then { _displayName = _resolvedName; }; + }; + } forEach _configRoots; + + _displayName + }], + ["buildAssetsList", compileFinal { + params [["_assetsRaw", createHashMap, [createHashMap]]]; + + private _assetsList = []; + { + private _category = _x; + { + private _assetData = _y; + private _className = _assetData getOrDefault ["classname", ""]; + private _displayName = _self call ["resolveDisplayName", [_className, [ + configFile >> "CfgWeapons", + configFile >> "CfgMagazines", + configFile >> "CfgVehicles", + configFile >> "CfgGlasses" + ]]]; + + _assetsList pushBack [ + ["name", _displayName], + ["type", _assetData getOrDefault ["type", _category]], + ["quantity", str (_assetData getOrDefault ["quantity", 0])] + ]; + } forEach _y; + } forEach _assetsRaw; + + _assetsList + }], + ["buildFleetList", compileFinal { + params [["_fleetRaw", createHashMap, [createHashMap]]]; + + private _fleetList = []; + { + private _vehicleData = _y; + _fleetList pushBack [ + ["name", _vehicleData getOrDefault ["name", "Unknown Vehicle"]], + ["type", _vehicleData getOrDefault ["type", "other"]], + ["status", _vehicleData getOrDefault ["status", "Unknown"]], + ["damage", _vehicleData getOrDefault ["damage", "0%"]] + ]; + } forEach _fleetRaw; + + _fleetList + }], + ["buildCreditLinesList", compileFinal { + params [["_creditLinesRaw", createHashMap, [createHashMap]]]; + + private _creditLinesList = []; + { + private _creditLineData = _y; + _creditLinesList pushBack [ + ["uid", _creditLineData getOrDefault ["uid", _x]], + ["member", _creditLineData getOrDefault ["name", "Unknown Member"]], + ["amount", _creditLineData getOrDefault ["amount", 0]] + ]; + } forEach _creditLinesRaw; + + _creditLinesList + }], + ["buildPortalPayload", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (isNull _player) exitWith { createHashMap }; + + private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; + private _orgID = _actor getOrDefault ["organization", "default"]; + if (_orgID isEqualTo "") then { _orgID = "default"; }; + + private _org = _self call ["resolveOrgForUid", [_uid]]; + if (_org isEqualTo createHashMap) exitWith { createHashMap }; + + private _verifiedOrg = GVAR(OrgStore) call ["ensureMember", [_orgID, _uid, GVAR(OrgStore) call ["resolveActorName", [_uid, _player, _actor]]]]; + if (_verifiedOrg isNotEqualTo createHashMap) then { _org = _verifiedOrg; }; + + private _name = _org getOrDefault ["name", ""]; + private _id = _org getOrDefault ["id", _orgID]; + private _ownerUid = _org getOrDefault ["owner", ""]; + private _funds = _org getOrDefault ["funds", 0]; + private _reputation = _org getOrDefault ["reputation", 0]; + private _creditLinesRaw = _org getOrDefault ["credit_lines", createHashMap]; + private _assetsRaw = _org getOrDefault ["assets", createHashMap]; + private _fleetRaw = _org getOrDefault ["fleet", createHashMap]; + private _membersRaw = _org getOrDefault ["members", createHashMap]; + private _isDefaultOrg = (_org getOrDefault ["default", false]) + || { toLowerANSI _id isEqualTo "default" } + || { toLowerANSI _ownerUid isEqualTo "server" }; + + private _playerName = name _player; + private _playerVar = vehicleVarName _player; + private _sessionIsCeo = _isDefaultOrg && { _playerVar isEqualTo "ceo" }; + private _memberShape = _self call ["buildMembersList", [_membersRaw, _uid, _ownerUid]]; + private _sessionRole = _memberShape getOrDefault ["sessionRole", "Member"]; + private _ownerName = _self call ["resolveOwnerName", [_ownerUid, _uid, _playerName, _membersRaw]]; + + if (_ownerUid isEqualTo _uid) then { _sessionRole = "Leader"; }; + + createHashMapFromArray [ + ["session", createHashMapFromArray [ + ["actorName", _playerName], + ["actorUid", _uid], + ["role", _sessionRole], + ["ceo", _sessionIsCeo] + ]], + ["portalData", createHashMapFromArray [ + ["org", createHashMapFromArray [ + ["name", _name], + ["tag", _id], + ["owner", _ownerName], + ["ownerUid", _ownerUid], + ["isDefault", _isDefaultOrg] + ]], + ["funds", _funds], + ["reputation", _reputation], + ["creditLines", _self call ["buildCreditLinesList", [_creditLinesRaw]]], + ["members", _memberShape getOrDefault ["members", []]], + ["fleet", _self call ["buildFleetList", [_fleetRaw]]], + ["assets", _self call ["buildAssetsList", [_assetsRaw]]], + ["activity", []] + ]] + ] + }] +]]; + +GVAR(OrgPayloadBuilder) diff --git a/arma/server/addons/org/functions/fnc_memberService.sqf b/arma/server/addons/org/functions/fnc_memberService.sqf deleted file mode 100644 index a25f9f9..0000000 --- a/arma/server/addons/org/functions/fnc_memberService.sqf +++ /dev/null @@ -1,273 +0,0 @@ -#include "..\script_component.hpp" - -#pragma hemtt ignore_variables ["_self"] -GVAR(OrgMembershipServiceBase) = compileFinal createHashMapFromArray [ - ["#type", "OrgMembershipService"], - ["buildMembershipResult", compileFinal { - params [["_message", "", [""]]]; - - createHashMapFromArray [ - ["success", false], - ["message", _message], - ["actorPatch", createHashMap] - ] - }], - ["verifyMember", compileFinal { - params [["_org", createHashMap, [createHashMap]], ["_orgID", "", [""]], ["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]]; - - if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { _org }; - - private _members = _org getOrDefault ["members", createHashMap]; - if ((_members getOrDefault [_uid, objNull]) isNotEqualTo objNull) exitWith { _org }; - - ["org:members:add", [_orgID, _uid]] call EFUNC(extension,extCall) params ["_memberResult", "_memberSuccess"]; - if (!_memberSuccess) then { - ["WARNING", format ["Failed to add %1 to org %2 members: %3", _uid, _orgID, _memberResult]] call EFUNC(common,log); - }; - - private _memberName = _actor getOrDefault ["name", ""]; - if (_memberName isEqualTo "" && { _player isNotEqualTo objNull }) then { - _memberName = name _player; - }; - if (_memberName isEqualTo "") then { - _memberName = "Unknown"; - }; - - private _updatedMembers = +_members; - _updatedMembers set [_uid, createHashMapFromArray [["uid", _uid], ["name", _memberName]]]; - _org set ["members", _updatedMembers]; - _org = GVAR(OrgStore) call ["override", [_orgID, _org, false]]; - - _org - }], - ["addMember", compileFinal { - params [["_orgID", "", [""]], ["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]]; - - if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap }; - - private _org = GVAR(OrgStore) call ["loadById", [_orgID]]; - if (_org isEqualTo createHashMap) exitWith { _org }; - - _org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]]; - - _org - }], - ["removeMember", compileFinal { - params [["_orgID", "", [""]], ["_uid", "", [""]]]; - - if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap }; - - private _org = GVAR(OrgStore) 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 _updatedMembers = +(_org getOrDefault ["members", createHashMap]); - _updatedMembers deleteAt _uid; - _org set ["members", _updatedMembers]; - _org = GVAR(OrgStore) call ["override", [_orgID, _org, false]]; - - _org - }], - ["restoreDefaultMembership", compileFinal { - params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]]; - - private _result = _self call ["buildMembershipResult", []]; - 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", false]]; - private _defaultActor = EGVAR(actor,ActorStore) call ["get", [_uid, ""]]; - - if !(_defaultActor isEqualType createHashMap) then { - _defaultActor = +_resolvedActor; - }; - - if ( - (_defaultActor isEqualTo createHashMap) - || { toLowerANSI (_defaultActor getOrDefault ["organization", ""]) isNotEqualTo "default" } - ) then { - private _forcedActor = +_resolvedActor; - if (_forcedActor isEqualTo createHashMap) then { - _forcedActor = EGVAR(actor,ActorModel) call ["defaults", []]; - _forcedActor set ["uid", _uid]; - }; - - _forcedActor set ["organization", "default"]; - _defaultActor = EGVAR(actor,ActorStore) call ["override", [_uid, _forcedActor, false]]; - if (_defaultActor isEqualType createHashMap && { _defaultActor isNotEqualTo createHashMap }) then { - _actorPatch = createHashMapFromArray [["organization", "default"]]; - }; - }; - - if ( - !(_defaultActor isEqualType createHashMap) - || { _defaultActor isEqualTo createHashMap } - || { toLowerANSI (_defaultActor getOrDefault ["organization", ""]) isNotEqualTo "default" } - ) exitWith { - _result set ["message", "Failed to restore default organization membership."]; - _result - }; - - private _defaultOrg = _self call ["addMember", ["default", _uid, _resolvedPlayer, _defaultActor]]; - if (_defaultOrg isEqualTo createHashMap) exitWith { - _result set ["message", "Failed to restore default organization membership."]; - _result - }; - - _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 = GVAR(OrgStore) 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 = GVAR(OrgStore) 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 = GVAR(OrgStore) 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 _memberActor = EGVAR(actor,Registry) getOrDefault [_memberUid, createHashMap]; - private _defaultResult = _self call ["restoreDefaultMembership", [_memberUid, _memberPlayer, _memberActor]]; - 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 - }] -]; - -GVAR(OrgMembershipService) = createHashMapObject [GVAR(OrgMembershipServiceBase)]; diff --git a/arma/server/addons/org/functions/fnc_treasuryService.sqf b/arma/server/addons/org/functions/fnc_treasuryService.sqf deleted file mode 100644 index f8a0803..0000000 --- a/arma/server/addons/org/functions/fnc_treasuryService.sqf +++ /dev/null @@ -1,166 +0,0 @@ -#include "..\script_component.hpp" - -#pragma hemtt ignore_variables ["_self"] -GVAR(OrgTreasuryServiceBase) = compileFinal createHashMapFromArray [ - ["#type", "OrgTreasuryService"], - ["buildChargeResult", compileFinal { - params [["_message", "Unable to process organization payment.", [""]]]; - - createHashMapFromArray [ - ["success", false], - ["message", _message], - ["patch", createHashMap], - ["memberUids", []] - ] - }], - ["resolveOrgMemberUids", compileFinal { - params [["_org", createHashMap, [createHashMap]], ["_requesterUid", "", [""]]]; - - private _memberUids = keys (_org getOrDefault ["members", createHashMap]); - if (_requesterUid isNotEqualTo "" && { !(_requesterUid in _memberUids) }) then { - _memberUids pushBack _requesterUid; - }; - - _memberUids - }], - ["canManageTreasury", compileFinal { - params [["_orgID", "", [""]], ["_org", createHashMap, [createHashMap]], ["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]]]; - - private _ownerUid = _org getOrDefault ["owner", ""]; - private _isDefaultOrg = (_orgID isEqualTo "default") || { toLowerANSI _ownerUid isEqualTo "server" }; - private _isDefaultOrgCeo = _isDefaultOrg - && { _requesterPlayer isNotEqualTo objNull } - && { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" }; - - (_ownerUid isEqualTo _requesterUid) || _isDefaultOrgCeo - }], - ["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 = GVAR(OrgStore) call ["loadById", [_orgID]]; - if (_org isEqualTo createHashMap) exitWith { - _result set ["message", "Unable to load organization data for credit line assignment."]; - _result - }; - - private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer); - if !(_self call ["canManageTreasury", [_orgID, _org, _requesterUid, _requesterPlayer]]) 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 = GVAR(OrgStore) call ["set", [_orgID, "credit_lines", _creditLines, false]]; - private _memberUids = _self call ["resolveOrgMemberUids", [_org, _requesterUid]]; - - _result set ["success", true]; - _result set ["message", format ["Credit line of $%1 assigned to %2.", [_amount] call EFUNC(common,formatNumber), _resolvedMemberName]]; - _result set ["patch", _patch]; - _result set ["memberUids", _memberUids]; - _result - }], - ["chargeCheckout", compileFinal { - params [["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]], ["_source", "org_funds", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]]; - - private _result = _self call ["buildChargeResult", []]; - private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; - private _orgID = _requesterActor getOrDefault ["organization", "default"]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; - - private _org = GVAR(OrgStore) call ["loadById", [_orgID]]; - if (_org isEqualTo createHashMap) exitWith { - _result set ["message", "Organization data is unavailable for checkout."]; - _result - }; - - private _memberUids = _self call ["resolveOrgMemberUids", [_org, _requesterUid]]; - - switch (toLowerANSI _source) do { - case "org_funds": { - if !(_self call ["canManageTreasury", [_orgID, _org, _requesterUid, _requesterPlayer]]) exitWith { - _result set ["message", "Only the organization leader or CEO can charge org funds."]; - _result - }; - - private _funds = _org getOrDefault ["funds", 0]; - if (_funds < _amount) exitWith { - _result set ["message", "Organization funds cannot cover this checkout."]; - _result - }; - - private _patch = createHashMapFromArray [["funds", (_funds - _amount)]]; - if (_commit) then { _patch = GVAR(OrgStore) call ["mset", [_orgID, _patch, false]]; }; - - _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result set ["memberUids", _memberUids]; - _result - }; - case "credit_line": { - private _creditLines = +(_org getOrDefault ["credit_lines", createHashMap]); - private _memberCredit = +(_creditLines getOrDefault [_requesterUid, createHashMap]); - private _creditAmount = _memberCredit getOrDefault ["amount", 0]; - if (_creditAmount < _amount) exitWith { - _result set ["message", "Assigned credit line cannot cover this checkout."]; - _result - }; - - _memberCredit set ["uid", _requesterUid]; - _memberCredit set ["amount", (_creditAmount - _amount)]; - _creditLines set [_requesterUid, _memberCredit]; - - private _patch = createHashMapFromArray [["credit_lines", _creditLines]]; - if (_commit) then { _patch = GVAR(OrgStore) call ["mset", [_orgID, _patch, false]]; }; - - _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result set ["memberUids", _memberUids]; - _result - }; - default { - _result set ["message", "Selected organization payment source is unsupported."]; - _result - }; - }; - }] -]; - -GVAR(OrgTreasuryService) = createHashMapObject [GVAR(OrgTreasuryServiceBase)]; diff --git a/arma/server/addons/store/functions/fnc_initCatalogService.sqf b/arma/server/addons/store/functions/fnc_initCatalogService.sqf index 8eb6b52..a27f364 100644 --- a/arma/server/addons/store/functions/fnc_initCatalogService.sqf +++ b/arma/server/addons/store/functions/fnc_initCatalogService.sqf @@ -399,12 +399,20 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ _resolved }], - ["calculateCheckoutTotal", compileFinal { + ["buildCheckoutRequest", compileFinal { params [["_items", [], [[]]], ["_vehicles", [], [[]]]]; - private _result = createHashMapFromArray [["success", false], ["total", 0], ["message", "Checkout total must be greater than zero."]]; + private _result = createHashMapFromArray [ + ["success", false], + ["total", 0], + ["message", "Checkout total must be greater than zero."], + ["items", []], + ["vehicles", []] + ]; private _total = 0; private _message = ""; + private _resolvedItems = []; + private _resolvedVehicles = []; { if (_message isEqualTo "") then { @@ -419,7 +427,14 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ if (_catalogEntry isEqualTo createHashMap) then { _message = format ["Unsupported store item: %1", _className]; } else { - _total = _total + ((_catalogEntry getOrDefault ["priceValue", 0]) * _quantity); + private _priceValue = _catalogEntry getOrDefault ["priceValue", 0]; + _total = _total + (_priceValue * _quantity); + _resolvedItems pushBack (createHashMapFromArray [ + ["classname", _className], + ["category", _catalogEntry getOrDefault ["category", "item"]], + ["priceValue", _priceValue], + ["quantity", _quantity] + ]); }; }; }; @@ -436,7 +451,13 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ if (_catalogEntry isEqualTo createHashMap) then { _message = format ["Unsupported store vehicle: %1", _className]; } else { - _total = _total + (_catalogEntry getOrDefault ["priceValue", 0]); + private _priceValue = _catalogEntry getOrDefault ["priceValue", 0]; + _total = _total + _priceValue; + _resolvedVehicles pushBack (createHashMapFromArray [ + ["classname", _className], + ["category", _catalogEntry getOrDefault ["category", _x getOrDefault ["category", "other"]]], + ["priceValue", _priceValue] + ]); }; }; }; @@ -452,7 +473,19 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ _result set ["success", true]; _result set ["total", floor _total]; _result set ["message", ""]; + _result set ["items", _resolvedItems]; + _result set ["vehicles", _resolvedVehicles]; _result + }], + ["calculateCheckoutTotal", compileFinal { + params [["_items", [], [[]]], ["_vehicles", [], [[]]]]; + + private _checkout = _self call ["buildCheckoutRequest", [_items, _vehicles]]; + createHashMapFromArray [ + ["success", _checkout getOrDefault ["success", false]], + ["total", _checkout getOrDefault ["total", 0]], + ["message", _checkout getOrDefault ["message", "Checkout total must be greater than zero."]] + ] }] ]; diff --git a/arma/server/addons/store/functions/fnc_initStoreStore.sqf b/arma/server/addons/store/functions/fnc_initStoreStore.sqf index 1e4b188..8a06965 100644 --- a/arma/server/addons/store/functions/fnc_initStoreStore.sqf +++ b/arma/server/addons/store/functions/fnc_initStoreStore.sqf @@ -160,45 +160,92 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ format ["$%1", [_amount max 0] call EFUNC(common,formatNumber)] }], - ["applyPaymentPatch", compileFinal { - params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_paymentMethod", "cash", [""]], ["_total", 0, [0]], ["_commit", false, [false]]]; + ["callCheckoutBackendEnvelope", compileFinal { + params [["_context", createHashMap, [createHashMap]]]; - private _result = _self call ["buildResult", ["Unable to process payment.", _paymentMethod]]; - private _payment = switch (toLowerANSI _paymentMethod) do { - case "cash"; - case "bank": { - EGVAR(bank,BankStore) call ["chargeCheckout", [_uid, _paymentMethod, _total, _commit]] - }; - case "org_funds"; - case "credit_line": { - EGVAR(org,OrgStore) call ["chargeCheckout", [_uid, _player, _paymentMethod, _total, _commit]] - }; - default { - createHashMapFromArray [ - ["success", false], - ["message", "Selected payment source is unsupported."], - ["patch", createHashMap], - ["memberUids", []] - ] - }; + private _envelope = createHashMapFromArray [["data", createHashMap], ["error", ""]]; + if (_context isEqualTo createHashMap) exitWith { + _envelope set ["error", "Checkout request was invalid."]; + _envelope }; - if !(_payment getOrDefault ["success", false]) exitWith { - _result set ["message", _payment getOrDefault ["message", "Unable to process payment."]]; - _result + ["store:checkout", [toJSON _context]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !(_isSuccess) exitWith { + _envelope set ["error", "Store backend call failed."]; + _envelope + }; + if !(_result isEqualType "") exitWith { + _envelope set ["error", "Store backend returned an invalid response."]; + _envelope + }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Store extension checkout failed: %1", _result]] call EFUNC(common,log); + _envelope set ["error", _result select [7]]; + _envelope }; - private _patch = _payment getOrDefault ["patch", createHashMap]; - if ((_paymentMethod isEqualTo "cash") || { _paymentMethod isEqualTo "bank" }) then { - _result set ["bankPatch", _patch]; - } else { - _result set ["orgPatch", _patch]; - _result set ["orgTargetUids", _payment getOrDefault ["memberUids", []]]; + private _data = fromJSON _result; + if !(_data isEqualType createHashMap) exitWith { + _envelope set ["error", "Store backend returned unreadable JSON."]; + _envelope }; - _result set ["success", true]; - _result set ["message", ""]; - _result + _envelope set ["data", _data]; + _envelope + }], + ["buildCheckoutContext", compileFinal { + params [ + ["_uid", "", [""]], + ["_player", objNull, [objNull]], + ["_paymentMethod", "cash", [""]], + ["_items", [], [[]]], + ["_vehicles", [], [[]]] + ]; + + if (_uid isEqualTo "" || { isNull _player }) exitWith { createHashMap }; + + private _orgID = EGVAR(org,OrgStore) call ["resolveOrgIdForUid", [_uid]]; + private _requesterIsDefaultOrgCeo = ( + _orgID isEqualTo "default" + && { toLowerANSI (vehicleVarName _player) isEqualTo "ceo" } + ); + + createHashMapFromArray [ + ["requesterUid", _uid], + ["requesterName", name _player], + ["orgId", _orgID], + ["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo], + ["paymentMethod", toLowerANSI _paymentMethod], + ["items", _items], + ["vehicles", _vehicles] + ] + }], + ["syncCheckoutResult", compileFinal { + params [["_player", objNull, [objNull]], ["_result", createHashMap, [createHashMap]]]; + + if (isNull _player || { _result isEqualTo createHashMap }) exitWith { false }; + + private _lockerPatch = _result getOrDefault ["lockerPatch", createHashMap]; + private _vaPatch = _result getOrDefault ["vaPatch", createHashMap]; + private _vgPatch = _result getOrDefault ["vgaragePatch", createHashMap]; + private _bankPatch = _result getOrDefault ["bankPatch", createHashMap]; + private _orgPatch = _result getOrDefault ["orgPatch", createHashMap]; + + if (keys _lockerPatch isNotEqualTo []) then { [CRPC(locker,responseSyncLocker), [_lockerPatch], _player] call CFUNC(targetEvent); }; + if (keys _vaPatch isNotEqualTo []) then { [CRPC(locker,responseSyncVA), [_vaPatch], _player] call CFUNC(targetEvent); }; + if (keys _vgPatch isNotEqualTo []) then { [CRPC(garage,responseSyncVG), [_vgPatch], _player] call CFUNC(targetEvent); }; + if (keys _bankPatch isNotEqualTo []) then { [CRPC(bank,responseSyncBank), [_bankPatch], _player] call CFUNC(targetEvent); }; + + if (keys _orgPatch isNotEqualTo []) then { + { + private _memberPlayer = [_x] call EFUNC(common,getPlayer); + if (_memberPlayer isNotEqualTo objNull) then { + [CRPC(org,responseSyncOrg), [_orgPatch], _memberPlayer] call CFUNC(targetEvent); + }; + } forEach (_result getOrDefault ["orgTargetUids", []]); + }; + + true }], ["checkout", compileFinal { params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_payloadJson", "", [""]]]; @@ -219,8 +266,8 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ _result }; - private _priceResult = GVAR(StoreCatalogService) call ["calculateCheckoutTotal", [_items, _vehicles]]; - private _totalPrice = _priceResult getOrDefault ["total", 0]; + private _checkoutRequest = GVAR(StoreCatalogService) call ["buildCheckoutRequest", [_items, _vehicles]]; + private _totalPrice = _checkoutRequest getOrDefault ["total", 0]; _result set ["paymentMethod", _paymentMethod]; _result set ["chargedTotal", _totalPrice]; @@ -230,90 +277,41 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ _result }; - if !(_priceResult getOrDefault ["success", false]) exitWith { - _result set ["message", _priceResult getOrDefault ["message", "Checkout total must be greater than zero."]]; + if !(_checkoutRequest getOrDefault ["success", false]) exitWith { + _result set ["message", _checkoutRequest getOrDefault ["message", "Checkout total must be greater than zero."]]; _result }; - private _lockerPreview = EGVAR(locker,LockerStore) call ["grantItems", [_uid, _items, false]]; - if !(_lockerPreview getOrDefault ["success", false]) exitWith { - _result set ["message", _lockerPreview getOrDefault ["message", "Locker grant failed."]]; + private _checkoutContext = _self call ["buildCheckoutContext", [ + _uid, + _player, + _paymentMethod, + _checkoutRequest getOrDefault ["items", []], + _checkoutRequest getOrDefault ["vehicles", []] + ]]; + if (_checkoutContext isEqualTo createHashMap) exitWith { + _result set ["message", "Checkout request context was invalid."]; _result }; - private _vaPreview = EGVAR(locker,VAStore) call ["unlockItems", [_uid, _items, false]]; - if !(_vaPreview getOrDefault ["success", false]) exitWith { - _result set ["message", _vaPreview getOrDefault ["message", "VA unlock failed."]]; + private _envelope = _self call ["callCheckoutBackendEnvelope", [_checkoutContext]]; + private _backendResult = _envelope getOrDefault ["data", createHashMap]; + if (_backendResult isEqualTo createHashMap) exitWith { + _result set ["message", _envelope getOrDefault ["error", "Checkout failed."]]; _result }; - private _vgPreview = EGVAR(garage,VGarageStore) call ["grantVehicles", [_uid, _vehicles, false]]; - if !(_vgPreview getOrDefault ["success", false]) exitWith { - _result set ["message", _vgPreview getOrDefault ["message", "Vehicle unlock failed."]]; - _result - }; - - private _orgFleetPreview = createHashMapFromArray [["success", true], ["message", ""], ["patch", createHashMap], ["memberUids", []]]; - if (_paymentMethod isEqualTo "org_funds" && { _vehicles isNotEqualTo [] }) then { - _orgFleetPreview = EGVAR(org,OrgStore) call ["addFleetVehicles", [_uid, _vehicles, false]]; - if !(_orgFleetPreview getOrDefault ["success", false]) exitWith { - _result set ["message", _orgFleetPreview getOrDefault ["message", "Organization fleet update failed."]]; - _result - }; - }; - - _result set ["lockerGranted", _lockerPreview getOrDefault ["granted", []]]; - _result set ["vehicleGranted", _vgPreview getOrDefault ["granted", []]]; - - private _paymentPreview = _self call ["applyPaymentPatch", [_uid, _player, _paymentMethod, _totalPrice, false]]; - if !(_paymentPreview getOrDefault ["success", false]) exitWith { - _result set ["message", _paymentPreview getOrDefault ["message", "Payment failed."]]; - _result - }; - - private _payment = _self call ["applyPaymentPatch", [_uid, _player, _paymentMethod, _totalPrice, true]]; - private _lockerResult = EGVAR(locker,LockerStore) call ["grantItems", [_uid, _items, true]]; - private _vaResult = EGVAR(locker,VAStore) call ["unlockItems", [_uid, _items, true]]; - private _vgResult = EGVAR(garage,VGarageStore) call ["grantVehicles", [_uid, _vehicles, true]]; - private _orgFleetResult = createHashMapFromArray [["success", true], ["message", ""], ["patch", createHashMap], ["memberUids", []]]; - if (_paymentMethod isEqualTo "org_funds" && { _vehicles isNotEqualTo [] }) then { - _orgFleetResult = EGVAR(org,OrgStore) call ["addFleetVehicles", [_uid, _vehicles, true]]; - }; - - private _lockerPatch = _lockerResult getOrDefault ["patch", createHashMap]; - private _vaPatch = _vaResult getOrDefault ["patch", createHashMap]; - private _vgPatch = _vgResult getOrDefault ["patch", createHashMap]; - if (keys _lockerPatch isNotEqualTo []) then { [CRPC(locker,responseSyncLocker), [_lockerPatch], _player] call CFUNC(targetEvent); }; - if (keys _vaPatch isNotEqualTo []) then { [CRPC(locker,responseSyncVA), [_vaPatch], _player] call CFUNC(targetEvent); }; - if (keys _vgPatch isNotEqualTo []) then { [CRPC(garage,responseSyncVG), [_vgPatch], _player] call CFUNC(targetEvent); }; - - private _bankPatch = _payment getOrDefault ["bankPatch", createHashMap]; - if (keys _bankPatch isNotEqualTo []) then { [CRPC(bank,responseSyncBank), [_bankPatch], _player] call CFUNC(targetEvent); }; - - private _orgPatch = _payment getOrDefault ["orgPatch", createHashMap]; - private _orgFleetPatch = _orgFleetResult getOrDefault ["patch", createHashMap]; - if (keys _orgFleetPatch isNotEqualTo []) then { { _orgPatch set [_x, _y]; } forEach _orgFleetPatch; }; - if (keys _orgPatch isNotEqualTo []) then { - private _orgTargetUids = _payment getOrDefault ["orgTargetUids", []]; - { - if !(_x in _orgTargetUids) then { _orgTargetUids pushBack _x; }; - } forEach (_orgFleetResult getOrDefault ["memberUids", []]); - - { - private _memberPlayer = [_x] call EFUNC(common,getPlayer); - if (_memberPlayer isNotEqualTo objNull) then { [CRPC(org,responseSyncOrg), [_orgPatch], _memberPlayer] call CFUNC(targetEvent); }; - } forEach _orgTargetUids; - }; + _self call ["syncCheckoutResult", [_player, _backendResult]]; _result set ["success", true]; - _result set ["message", format [ + _result set ["message", _backendResult getOrDefault ["message", format [ "Checkout completed. %1 charged, %2 locker grant(s), %3 vehicle unlock(s).", _self call ["formatCurrency", [_totalPrice]], - count (_lockerResult getOrDefault ["granted", []]), - count (_vgResult getOrDefault ["granted", []]) - ]]; - _result set ["lockerGranted", _lockerResult getOrDefault ["granted", []]]; - _result set ["vehicleGranted", _vgResult getOrDefault ["granted", []]]; + count (_backendResult getOrDefault ["lockerGranted", []]), + count (_backendResult getOrDefault ["vehicleGranted", []]) + ]]]; + _result set ["lockerGranted", _backendResult getOrDefault ["lockerGranted", []]]; + _result set ["vehicleGranted", _backendResult getOrDefault ["vehicleGranted", []]]; _result }] ]; diff --git a/arma/server/extension/src/actor.rs b/arma/server/extension/src/actor.rs index e621056..53f3a68 100644 --- a/arma/server/extension/src/actor.rs +++ b/arma/server/extension/src/actor.rs @@ -31,6 +31,14 @@ static HOT_ACTOR_SERVICE: LazyLock< ActorHotStateService::new(repository, hot_repository) }); +#[allow(dead_code)] +pub(crate) fn hot_service() -> &'static ActorHotStateService< + RedisActorRepository, + InMemoryActorHotRepository, +> { + &HOT_ACTOR_SERVICE +} + /// Creates the Arma 3 command group for actor operations. /// /// Registers commands: `get`, `exists`, `create`, `update`, `delete`. diff --git a/arma/server/extension/src/bank.rs b/arma/server/extension/src/bank.rs index dce90e0..64f6492 100644 --- a/arma/server/extension/src/bank.rs +++ b/arma/server/extension/src/bank.rs @@ -35,6 +35,13 @@ static HOT_BANK_SERVICE: LazyLock< BankHotStateService::new(repository, hot_repository) }); +pub(crate) fn hot_service() -> &'static BankHotStateService< + RedisBankRepository, + InMemoryBankHotRepository, +> { + &HOT_BANK_SERVICE +} + /// Creates the Arma 3 command group for bank operations. /// /// Registers commands: `get`, `exists`, `create`, `update`, `delete`. diff --git a/arma/server/extension/src/garage.rs b/arma/server/extension/src/garage.rs index dcc6204..a5e8a67 100644 --- a/arma/server/extension/src/garage.rs +++ b/arma/server/extension/src/garage.rs @@ -30,6 +30,14 @@ static HOT_GARAGE_SERVICE: LazyLock< GarageHotStateService::new(repository, hot_repository) }); +#[allow(dead_code)] +pub(crate) fn hot_service() -> &'static GarageHotStateService< + RedisGarageRepository, + InMemoryGarageHotRepository, +> { + &HOT_GARAGE_SERVICE +} + /// Creates the Arma 3 command group for garage operations. /// /// Registers commands: `create`, `get`, `add`, `update`, `remove`, `delete`, `exists`. diff --git a/arma/server/extension/src/lib.rs b/arma/server/extension/src/lib.rs index 4c1fabe..c9a93ef 100644 --- a/arma/server/extension/src/lib.rs +++ b/arma/server/extension/src/lib.rs @@ -22,6 +22,7 @@ pub mod locker; mod log; pub mod org; pub mod redis; +pub mod store; pub mod terrain; pub mod transport; pub mod v_garage; @@ -85,6 +86,7 @@ fn init() -> Extension { .group("icom", icom::group()) .group("locker", locker::group()) .group("org", org::group()) + .group("store", store::group()) .group("terrain", terrain::group()) .group("transport", transport::group()) .group( diff --git a/arma/server/extension/src/locker.rs b/arma/server/extension/src/locker.rs index 99ea271..91d8e10 100644 --- a/arma/server/extension/src/locker.rs +++ b/arma/server/extension/src/locker.rs @@ -25,6 +25,13 @@ static HOT_LOCKER_SERVICE: LazyLock< LockerHotStateService::new(repository, hot_repository) }); +pub(crate) fn hot_service() -> &'static LockerHotStateService< + RedisLockerRepository, + InMemoryLockerHotRepository, +> { + &HOT_LOCKER_SERVICE +} + /// Creates the Arma 3 command group for locker operations. /// /// Registers commands: `create`, `get`, `add`, `update`, `remove`, `delete`, `exists`. diff --git a/arma/server/extension/src/org.rs b/arma/server/extension/src/org.rs index ac2fe06..77f0c10 100644 --- a/arma/server/extension/src/org.rs +++ b/arma/server/extension/src/org.rs @@ -4,7 +4,11 @@ //! Handles SQF command mapping and parameter validation. use arma_rs::Group; -use forge_models::HotOrgRecord; +use forge_models::{ + HotOrgRecord, OrgAssetGrantSeed, OrgCheckoutContext, OrgCreditLineContext, OrgDisbandResult, + OrgEnsureMemberContext, OrgFleetGrantSeed, OrgGrantContext, OrgLeaveContext, OrgLeaveResult, + OrgRegisterContext, +}; use forge_repositories::{InMemoryOrgHotRepository, RedisOrgRepository}; use forge_services::{OrgHotStateService, OrgService}; use std::sync::LazyLock; @@ -31,6 +35,11 @@ static HOT_ORG_SERVICE: LazyLock< OrgHotStateService::new(repository, hot_repository) }); +pub(crate) fn hot_service() +-> &'static OrgHotStateService, InMemoryOrgHotRepository> { + &HOT_ORG_SERVICE +} + /// Creates the Arma 3 command group for organization operations. /// /// Registers commands: `get`, `exists`, `create`, `update`, `delete`. @@ -47,6 +56,14 @@ pub fn group() -> Group { .command("init", init_hot_org) .command("get", get_hot_org) .command("override", override_hot_org) + .command("ensure_member", ensure_hot_org_member) + .command("register", register_hot_org) + .command("assign_credit_line", assign_credit_line_hot_org) + .command("charge_checkout", charge_checkout_hot_org) + .command("add_assets", add_assets_hot_org) + .command("add_fleet", add_fleet_hot_org) + .command("leave", leave_hot_org) + .command("disband", disband_hot_org) .command("save", save_hot_org) .command("remove", remove_hot_org), ) @@ -78,6 +95,13 @@ fn serialize_hot_org(org: HotOrgRecord) -> String { } } +fn serialize_result(value: &T, label: &str) -> String { + match serde_json::to_string(value) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize {}: {}", label, error), + } +} + pub(crate) fn init_hot_org(org_id: String) -> String { match HOT_ORG_SERVICE.init_org(org_id) { Ok(org) => serialize_hot_org(org), @@ -104,6 +128,110 @@ pub(crate) fn override_hot_org(org_id: String, json_data: String) -> String { } } +pub(crate) fn ensure_hot_org_member(json_data: String) -> String { + let context: OrgEnsureMemberContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid ensure-member JSON: {}", error), + }; + + match HOT_ORG_SERVICE.ensure_member(context) { + Ok(org) => serialize_hot_org(org), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn register_hot_org(json_data: String) -> String { + let context: OrgRegisterContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid register org JSON: {}", error), + }; + + match HOT_ORG_SERVICE.register_org(context) { + Ok(result) => serialize_result(&result, "org register result"), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn assign_credit_line_hot_org(json_data: String) -> String { + let context: OrgCreditLineContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org credit-line JSON: {}", error), + }; + + match HOT_ORG_SERVICE.assign_credit_line(context) { + Ok(result) => serialize_result(&result, "org mutation result"), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn charge_checkout_hot_org(json_data: String) -> String { + let context: OrgCheckoutContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org checkout JSON: {}", error), + }; + + match HOT_ORG_SERVICE.charge_checkout(context) { + Ok(result) => serialize_result(&result, "org mutation result"), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn add_assets_hot_org(context_json: String, assets_json: String) -> String { + let context: OrgGrantContext = match serde_json::from_str(&context_json) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org asset context JSON: {}", error), + }; + let assets: Vec = match serde_json::from_str(&assets_json) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org asset seed JSON: {}", error), + }; + + match HOT_ORG_SERVICE.add_assets(context, assets) { + Ok(result) => serialize_result(&result, "org mutation result"), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn add_fleet_hot_org(context_json: String, fleet_json: String) -> String { + let context: OrgGrantContext = match serde_json::from_str(&context_json) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org fleet context JSON: {}", error), + }; + let fleet: Vec = match serde_json::from_str(&fleet_json) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org fleet seed JSON: {}", error), + }; + + match HOT_ORG_SERVICE.add_fleet_vehicles(context, fleet) { + Ok(result) => serialize_result(&result, "org mutation result"), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn leave_hot_org(json_data: String) -> String { + let context: OrgLeaveContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org leave JSON: {}", error), + }; + + match HOT_ORG_SERVICE.leave_org(context) { + Ok(result) => serialize_result::(&result, "org leave result"), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn disband_hot_org(json_data: String) -> String { + let context: OrgLeaveContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org disband JSON: {}", error), + }; + + match HOT_ORG_SERVICE.disband_org(context) { + Ok(result) => serialize_result::(&result, "org disband result"), + Err(error) => format!("Error: {}", error), + } +} + pub(crate) fn save_hot_org(org_id: String) -> String { match HOT_ORG_SERVICE.get_org(org_id.clone()) { Ok(org) => { diff --git a/arma/server/extension/src/store.rs b/arma/server/extension/src/store.rs new file mode 100644 index 0000000..7842ca7 --- /dev/null +++ b/arma/server/extension/src/store.rs @@ -0,0 +1,37 @@ +use arma_rs::Group; +use forge_models::{StoreCheckoutContext, StoreCheckoutResult}; +use forge_services::StoreService; + +pub fn group() -> Group { + Group::new().command("checkout", checkout) +} + +fn serialize_result(result: &StoreCheckoutResult) -> String { + match serde_json::to_string(result) { + Ok(json) => json, + Err(error) => format!( + "Error: Failed to serialize store checkout result: {}", + error + ), + } +} + +pub fn checkout(json_data: String) -> String { + let context: StoreCheckoutContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid store checkout JSON: {}", error), + }; + + let service = StoreService::new( + crate::bank::hot_service(), + crate::org::hot_service(), + crate::locker::hot_service(), + crate::v_locker::hot_service(), + crate::v_garage::hot_service(), + ); + + match service.checkout(context) { + Ok(result) => serialize_result(&result), + Err(error) => format!("Error: {}", error), + } +} diff --git a/arma/server/extension/src/transport.rs b/arma/server/extension/src/transport.rs index 1ebbfe2..b1af67d 100644 --- a/arma/server/extension/src/transport.rs +++ b/arma/server/extension/src/transport.rs @@ -351,6 +351,44 @@ fn route_command( arguments[1].clone(), )) } + "org:hot:ensure_member" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::ensure_hot_org_member(arguments[0].clone())) + } + "org:hot:register" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::register_hot_org(arguments[0].clone())) + } + "org:hot:assign_credit_line" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::assign_credit_line_hot_org(arguments[0].clone())) + } + "org:hot:charge_checkout" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::charge_checkout_hot_org(arguments[0].clone())) + } + "org:hot:add_assets" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::add_assets_hot_org( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "org:hot:add_fleet" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::add_fleet_hot_org( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "org:hot:leave" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::leave_hot_org(arguments[0].clone())) + } + "org:hot:disband" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::disband_hot_org(arguments[0].clone())) + } "org:hot:save" => { expect_arg_count(function_name, &arguments, 1)?; Ok(org::save_hot_org(arguments[0].clone())) @@ -396,6 +434,10 @@ fn route_command( arguments[1].clone(), )) } + "store:checkout" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(crate::store::checkout(arguments[0].clone())) + } "garage:create" => { expect_arg_count(function_name, &arguments, 1)?; Ok(garage::create_garage(call_context, arguments[0].clone())) diff --git a/arma/server/extension/src/v_garage.rs b/arma/server/extension/src/v_garage.rs index 5e55594..1382d63 100644 --- a/arma/server/extension/src/v_garage.rs +++ b/arma/server/extension/src/v_garage.rs @@ -27,6 +27,13 @@ static HOT_VGARAGE_SERVICE: LazyLock< VGarageHotStateService::new(repository, hot_repository) }); +pub(crate) fn hot_service() -> &'static VGarageHotStateService< + RedisVGarageRepository, + InMemoryVGarageHotRepository, +> { + &HOT_VGARAGE_SERVICE +} + /// Creates the Arma 3 command group for virtual garage operations. /// /// Registers commands: `create`, `fetch`, `get`, `add`, `remove`, `delete`, `exists`. diff --git a/arma/server/extension/src/v_locker.rs b/arma/server/extension/src/v_locker.rs index 22d06be..7064e47 100644 --- a/arma/server/extension/src/v_locker.rs +++ b/arma/server/extension/src/v_locker.rs @@ -27,6 +27,13 @@ static HOT_VLOCKER_SERVICE: LazyLock< VLockerHotStateService::new(repository, hot_repository) }); +pub(crate) fn hot_service() -> &'static VLockerHotStateService< + RedisVLockerRepository, + InMemoryVLockerHotRepository, +> { + &HOT_VLOCKER_SERVICE +} + /// Creates the Arma 3 command group for virtual locker operations. /// /// Registers commands: `create`, `fetch`, `get`, `add`, `remove`, `delete`, `exists`. diff --git a/lib/models/src/lib.rs b/lib/models/src/lib.rs index 4bc404c..0267e91 100644 --- a/lib/models/src/lib.rs +++ b/lib/models/src/lib.rs @@ -4,6 +4,7 @@ pub mod cad; pub mod garage; pub mod locker; pub mod org; +pub mod store; pub mod v_garage; pub mod v_locker; @@ -20,6 +21,15 @@ pub use cad::{ }; pub use garage::{Garage, HitPoints, Vehicle}; pub use locker::{Item, Locker}; -pub use org::{CreditLineSummary, HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgFleetEntry}; +pub use org::{ + CreditLineSummary, HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgAssetGrantSeed, + OrgCheckoutContext, OrgCreditLineContext, OrgDisbandMemberResult, OrgDisbandResult, + OrgEnsureMemberContext, OrgFleetEntry, OrgFleetGrantSeed, OrgGrantContext, OrgLeaveContext, + OrgLeaveResult, OrgMutationResult, OrgRegisterContext, OrgRegisterResult, +}; +pub use store::{ + StoreCheckoutContext, StoreCheckoutItemSeed, StoreCheckoutResult, StoreCheckoutVehicleSeed, + StoreGrantedItem, StoreGrantedVehicle, +}; pub use v_garage::{VGarage, VehicleCategory}; pub use v_locker::{EquipmentCategory, VLocker}; diff --git a/lib/models/src/org.rs b/lib/models/src/org.rs index c792967..c2c026e 100644 --- a/lib/models/src/org.rs +++ b/lib/models/src/org.rs @@ -65,6 +65,117 @@ pub struct HotOrgRecord { pub members: HashMap, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgEnsureMemberContext { + pub org_id: String, + pub member_uid: String, + pub member_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgRegisterContext { + pub requester_uid: String, + pub requester_name: String, + pub org_id: String, + pub org_name: String, + pub existing_org_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgRegisterResult { + pub org: HotOrgRecord, + pub actor_organization: String, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgCreditLineContext { + pub requester_uid: String, + pub org_id: String, + pub requester_is_default_org_ceo: bool, + pub member_uid: String, + pub member_name: String, + pub amount: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgCheckoutContext { + pub requester_uid: String, + pub org_id: String, + pub requester_is_default_org_ceo: bool, + pub source: String, + pub amount: f64, + pub commit: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgAssetGrantSeed { + pub classname: String, + pub category: String, + pub quantity: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgFleetGrantSeed { + pub classname: String, + pub category: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgGrantContext { + pub requester_uid: String, + pub org_id: String, + pub commit: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgMutationResult { + pub org: HotOrgRecord, + pub patch: HashMap, + pub member_uids: Vec, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgLeaveContext { + pub requester_uid: String, + pub requester_name: String, + pub org_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgLeaveResult { + pub actor_organization: String, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgDisbandMemberResult { + pub uid: String, + pub requester: bool, + pub actor_organization: String, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgDisbandResult { + pub message: String, + pub members: Vec, +} + impl Org { pub fn new>(id: S, owner: S, name: S) -> Result { let org = Self { diff --git a/lib/models/src/store.rs b/lib/models/src/store.rs new file mode 100644 index 0000000..9665c5c --- /dev/null +++ b/lib/models/src/store.rs @@ -0,0 +1,72 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoreCheckoutItemSeed { + pub classname: String, + pub category: String, + pub price_value: f64, + pub quantity: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoreCheckoutVehicleSeed { + pub classname: String, + pub category: String, + pub price_value: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoreCheckoutContext { + pub requester_uid: String, + pub requester_name: String, + pub org_id: String, + pub requester_is_default_org_ceo: bool, + pub payment_method: String, + #[serde(default)] + pub items: Vec, + #[serde(default)] + pub vehicles: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoreGrantedItem { + pub classname: String, + pub category: String, + pub quantity: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoreGrantedVehicle { + pub classname: String, + pub category: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoreCheckoutResult { + pub charged_total: f64, + pub payment_method: String, + pub message: String, + #[serde(default)] + pub locker_granted: Vec, + #[serde(default)] + pub vehicle_granted: Vec, + #[serde(default)] + pub locker_patch: HashMap, + #[serde(default)] + pub va_patch: HashMap, + #[serde(default)] + pub vgarage_patch: HashMap, + #[serde(default)] + pub bank_patch: HashMap, + #[serde(default)] + pub org_patch: HashMap, + #[serde(default)] + pub org_target_uids: Vec, +} diff --git a/lib/services/src/lib.rs b/lib/services/src/lib.rs index 6259d74..61fa35a 100644 --- a/lib/services/src/lib.rs +++ b/lib/services/src/lib.rs @@ -4,6 +4,7 @@ pub mod cad; pub mod garage; pub mod locker; pub mod org; +pub mod store; pub mod v_garage; pub mod v_locker; @@ -13,5 +14,6 @@ pub use cad::{CadStateService, CadViewService}; pub use garage::{GarageHotStateService, GarageService}; pub use locker::{LockerHotStateService, LockerService}; pub use org::{OrgHotStateService, OrgService}; +pub use store::StoreService; pub use v_garage::{VGarageHotStateService, VGarageService}; pub use v_locker::{VLockerHotStateService, VLockerService}; diff --git a/lib/services/src/org.rs b/lib/services/src/org.rs index c83235d..590427a 100644 --- a/lib/services/src/org.rs +++ b/lib/services/src/org.rs @@ -6,9 +6,13 @@ //! For full documentation, architecture, and examples, see the [crate README](../README.md). use forge_models::{ - CreditLineSummary, HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgFleetEntry, + CreditLineSummary, HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgAssetGrantSeed, + OrgCheckoutContext, OrgCreditLineContext, OrgDisbandMemberResult, OrgDisbandResult, + OrgEnsureMemberContext, OrgFleetEntry, OrgFleetGrantSeed, OrgGrantContext, OrgLeaveContext, + OrgLeaveResult, OrgMutationResult, OrgRegisterContext, OrgRegisterResult, }; use forge_repositories::{OrgHotRepository, OrgRepository}; +use serde_json::{Value, json}; use std::collections::{HashMap, HashSet}; /// Service layer implementation for organization business logic and operations. @@ -328,6 +332,19 @@ impl OrgHotStateService { pub fn init_org(&self, id: String) -> Result { if let Some(org) = self.repository.get(&id)? { + if !org.members.is_empty() || !org.assets.is_empty() || !org.fleet.is_empty() { + return Ok(org); + } + + let hydrated_org = self.hydrate_org(&id)?; + if !hydrated_org.members.is_empty() + || !hydrated_org.assets.is_empty() + || !hydrated_org.fleet.is_empty() + { + self.repository.save(&hydrated_org)?; + return Ok(hydrated_org); + } + return Ok(org); } @@ -392,6 +409,428 @@ impl OrgHotStateService { self.repository.delete(&id) } + pub fn ensure_member(&self, context: OrgEnsureMemberContext) -> Result { + if context.org_id.trim().is_empty() || context.member_uid.trim().is_empty() { + return Err("A valid organization and member UID are required.".to_string()); + } + + let mut org = self.get_org(context.org_id)?; + if !org.members.contains_key(&context.member_uid) { + let member_name = if context.member_name.trim().is_empty() { + "Unknown".to_string() + } else { + context.member_name + }; + org.members.insert( + context.member_uid.clone(), + MemberSummary { + uid: context.member_uid, + name: member_name, + }, + ); + self.repository.save(&org)?; + } + + Ok(org) + } + + pub fn register_org(&self, context: OrgRegisterContext) -> Result { + if context.requester_uid.trim().is_empty() || context.org_id.trim().is_empty() { + return Err("A valid requester and organization ID are required.".to_string()); + } + if context.org_name.trim().is_empty() { + return Err("Organization name cannot be empty.".to_string()); + } + if !context.existing_org_id.trim().is_empty() + && !context.existing_org_id.eq_ignore_ascii_case("default") + { + return Err("Player already belongs to an organization.".to_string()); + } + if self.service.org_exists(context.org_id.clone())? + || self.repository.get(&context.org_id)?.is_some() + { + return Err("An organization already exists for this phone number.".to_string()); + } + + let org = Org { + id: context.org_id.clone(), + owner: context.requester_uid.clone(), + name: context.org_name, + funds: 0.0, + reputation: 0, + credit_lines: HashMap::new(), + }; + org.validate() + .map_err(|error| format!("Validation failed: {}", error))?; + + let json_data = serde_json::to_string(&org) + .map_err(|error| format!("Failed to serialize org: {}", error))?; + let persisted_org = self.service.create_org(context.org_id.clone(), json_data)?; + let mut hot_org = + HotOrgRecord::from_parts(persisted_org, HashMap::new(), HashMap::new(), Vec::new()); + hot_org.members.insert( + context.requester_uid.clone(), + MemberSummary { + uid: context.requester_uid.clone(), + name: if context.requester_name.trim().is_empty() { + "Unknown".to_string() + } else { + context.requester_name + }, + }, + ); + self.repository.save(&hot_org)?; + + if context.existing_org_id.eq_ignore_ascii_case("default") { + let mut default_org = self.init_org("default".to_string())?; + default_org.members.remove(&context.requester_uid); + self.repository.save(&default_org)?; + } + + Ok(OrgRegisterResult { + org: hot_org, + actor_organization: context.org_id, + message: String::new(), + }) + } + + pub fn assign_credit_line( + &self, + context: OrgCreditLineContext, + ) -> Result { + if context.requester_uid.trim().is_empty() + || context.member_uid.trim().is_empty() + || context.org_id.trim().is_empty() + { + return Err("A valid requester, member, and organization are required.".to_string()); + } + if context.amount <= 0.0 { + return Err("A valid credit amount is required.".to_string()); + } + + let mut org = self.get_org(context.org_id)?; + if !can_manage_treasury( + &org, + &context.requester_uid, + context.requester_is_default_org_ceo, + ) { + return Err( + "Only the organization leader or CEO can manage treasury actions.".to_string(), + ); + } + + let member_record = org + .members + .get(&context.member_uid) + .cloned() + .ok_or_else(|| { + "Selected member was not found in the organization roster.".to_string() + })?; + let member_name = if context.member_name.trim().is_empty() { + member_record.name + } else { + context.member_name + }; + + org.credit_lines.insert( + context.member_uid.clone(), + CreditLineSummary { + uid: context.member_uid.clone(), + name: member_name.clone(), + amount: context.amount, + }, + ); + self.repository.save(&org)?; + + Ok(OrgMutationResult { + patch: build_org_patch(&org, &["credit_lines"])?, + member_uids: resolve_member_uids(&org, Some(&context.requester_uid)), + message: format!( + "Credit line of ${} assigned to {}.", + format_currency(context.amount), + member_name + ), + org, + }) + } + + pub fn charge_checkout( + &self, + context: OrgCheckoutContext, + ) -> Result { + if context.requester_uid.trim().is_empty() || context.org_id.trim().is_empty() { + return Err("A valid requester and organization are required.".to_string()); + } + if context.amount <= 0.0 { + return Err("Checkout amount must be greater than zero.".to_string()); + } + + let mut org = self.get_org(context.org_id)?; + let member_uids = resolve_member_uids(&org, Some(&context.requester_uid)); + + match context.source.trim().to_ascii_lowercase().as_str() { + "org_funds" => { + if !can_manage_treasury( + &org, + &context.requester_uid, + context.requester_is_default_org_ceo, + ) { + return Err( + "Only the organization leader or CEO can charge org funds.".to_string() + ); + } + if org.funds < context.amount { + return Err("Organization funds cannot cover this checkout.".to_string()); + } + + org.funds -= context.amount; + self.repository.save(&org)?; + + Ok(OrgMutationResult { + patch: build_org_patch(&org, &["funds"])?, + member_uids, + message: String::new(), + org, + }) + } + "credit_line" => { + let mut credit_line = org + .credit_lines + .get(&context.requester_uid) + .cloned() + .ok_or_else(|| { + "Assigned credit line cannot cover this checkout.".to_string() + })?; + + if credit_line.amount < context.amount { + return Err("Assigned credit line cannot cover this checkout.".to_string()); + } + + credit_line.amount -= context.amount; + org.credit_lines + .insert(context.requester_uid.clone(), credit_line); + self.repository.save(&org)?; + + Ok(OrgMutationResult { + patch: build_org_patch(&org, &["credit_lines"])?, + member_uids, + message: String::new(), + org, + }) + } + _ => Err("Selected organization payment source is unsupported.".to_string()), + } + } + + pub fn add_assets( + &self, + context: OrgGrantContext, + assets: Vec, + ) -> Result { + if context.org_id.trim().is_empty() { + return Err("A valid organization is required for asset updates.".to_string()); + } + if assets.is_empty() { + let org = self.get_org(context.org_id)?; + return Ok(OrgMutationResult { + org, + patch: HashMap::new(), + member_uids: Vec::new(), + message: String::new(), + }); + } + + let mut org = self.get_org(context.org_id)?; + for asset in assets { + if asset.classname.trim().is_empty() || asset.quantity <= 0 { + continue; + } + let category = asset.category.trim().to_ascii_lowercase(); + let category_assets = org.assets.entry(category.clone()).or_default(); + let entry = category_assets + .entry(asset.classname.clone()) + .or_insert_with(|| OrgAssetEntry { + classname: asset.classname.clone(), + asset_type: category.clone(), + quantity: 0, + }); + entry.quantity += asset.quantity; + } + + self.repository.save(&org)?; + + Ok(OrgMutationResult { + patch: build_org_patch(&org, &["assets"])?, + member_uids: resolve_member_uids(&org, Some(&context.requester_uid)), + message: String::new(), + org, + }) + } + + pub fn add_fleet_vehicles( + &self, + context: OrgGrantContext, + vehicles: Vec, + ) -> Result { + if context.org_id.trim().is_empty() { + return Err("A valid organization is required for fleet updates.".to_string()); + } + if vehicles.is_empty() { + let org = self.get_org(context.org_id)?; + return Ok(OrgMutationResult { + org, + patch: HashMap::new(), + member_uids: Vec::new(), + message: String::new(), + }); + } + + let mut org = self.get_org(context.org_id)?; + let mut fleet_index = org.fleet.len(); + for vehicle in vehicles { + if vehicle.classname.trim().is_empty() { + continue; + } + let fleet_type = vehicle.category.trim().to_ascii_lowercase(); + let mut fleet_key = format!("{}_{}", vehicle.classname, fleet_index); + while org.fleet.contains_key(&fleet_key) { + fleet_index += 1; + fleet_key = format!("{}_{}", vehicle.classname, fleet_index); + } + + org.fleet.insert( + fleet_key, + OrgFleetEntry { + classname: vehicle.classname.clone(), + name: vehicle.classname, + fleet_type, + status: "Ready".to_string(), + damage: "0%".to_string(), + }, + ); + fleet_index += 1; + } + + self.repository.save(&org)?; + + Ok(OrgMutationResult { + patch: build_org_patch(&org, &["fleet"])?, + member_uids: resolve_member_uids(&org, Some(&context.requester_uid)), + message: String::new(), + org, + }) + } + + pub fn leave_org(&self, context: OrgLeaveContext) -> Result { + if context.requester_uid.trim().is_empty() { + return Err("A valid player UID is required.".to_string()); + } + if context.org_id.trim().is_empty() || context.org_id.eq_ignore_ascii_case("default") { + return Err("You are already assigned to the default organization.".to_string()); + } + + let mut org = self.get_org(context.org_id)?; + if org.owner == context.requester_uid { + return Err( + "Organization owners must disband the organization instead of leaving it." + .to_string(), + ); + } + + let org_name = org.name.clone(); + org.members.remove(&context.requester_uid); + self.repository.save(&org)?; + + let mut default_org = self.init_org("default".to_string())?; + let requester_uid = context.requester_uid.clone(); + default_org.members.insert( + requester_uid.clone(), + MemberSummary { + uid: requester_uid, + name: if context.requester_name.trim().is_empty() { + "Unknown".to_string() + } else { + context.requester_name + }, + }, + ); + self.repository.save(&default_org)?; + + Ok(OrgLeaveResult { + actor_organization: "default".to_string(), + message: format!( + "You left {} and returned to the default organization.", + org_name + ), + }) + } + + pub fn disband_org(&self, context: OrgLeaveContext) -> Result { + if context.requester_uid.trim().is_empty() { + return Err("A valid player UID is required.".to_string()); + } + if context.org_id.trim().is_empty() || context.org_id.eq_ignore_ascii_case("default") { + return Err("Only active player organizations can be disbanded.".to_string()); + } + + let org = self.get_org(context.org_id.clone())?; + if org.owner != context.requester_uid { + return Err("Only the organization owner can disband this organization.".to_string()); + } + + let org_name = org.name.clone(); + let mut default_org = self.init_org("default".to_string())?; + let mut member_results = Vec::new(); + let mut seen = HashSet::new(); + + for (member_uid, member) in &org.members { + if seen.insert(member_uid.clone()) { + default_org + .members + .insert(member_uid.clone(), member.clone()); + member_results.push(OrgDisbandMemberResult { + uid: member_uid.clone(), + requester: member_uid == &context.requester_uid, + actor_organization: "default".to_string(), + message: if member_uid == &context.requester_uid { + format!("Your organization, {}, has been disbanded.", org_name) + } else { + format!("{} has been disbanded.", org_name) + }, + }); + } + } + + if seen.insert(context.requester_uid.clone()) { + default_org.members.insert( + context.requester_uid.clone(), + MemberSummary { + uid: context.requester_uid.clone(), + name: if context.requester_name.trim().is_empty() { + "Unknown".to_string() + } else { + context.requester_name + }, + }, + ); + member_results.push(OrgDisbandMemberResult { + uid: context.requester_uid, + requester: true, + actor_organization: "default".to_string(), + message: format!("Your organization, {}, has been disbanded.", org_name), + }); + } + + self.repository.save(&default_org)?; + self.service.delete_org(context.org_id.clone())?; + self.repository.delete(&context.org_id)?; + + Ok(OrgDisbandResult { + message: format!("{} has been disbanded.", org_name), + members: member_results, + }) + } + fn hydrate_org(&self, id: &str) -> Result { let org = self .service @@ -403,3 +842,65 @@ impl OrgHotStateService { Ok(HotOrgRecord::from_parts(org, assets, fleet, members)) } } + +fn can_manage_treasury( + org: &HotOrgRecord, + requester_uid: &str, + requester_is_default_org_ceo: bool, +) -> bool { + org.owner == requester_uid + || ((org.id.eq_ignore_ascii_case("default") || org.owner.eq_ignore_ascii_case("server")) + && requester_is_default_org_ceo) +} + +fn resolve_member_uids(org: &HotOrgRecord, requester_uid: Option<&str>) -> Vec { + let mut member_uids = org.members.keys().cloned().collect::>(); + if let Some(uid) = requester_uid { + if !uid.is_empty() && !member_uids.iter().any(|member_uid| member_uid == uid) { + member_uids.push(uid.to_string()); + } + } + member_uids +} + +fn build_org_patch(org: &HotOrgRecord, fields: &[&str]) -> Result, String> { + let mut patch = HashMap::new(); + for field in fields { + patch.insert((*field).to_string(), current_org_field_value(org, field)?); + } + Ok(patch) +} + +fn current_org_field_value(org: &HotOrgRecord, field: &str) -> Result { + match field { + "id" => Ok(json!(org.id)), + "owner" => Ok(json!(org.owner)), + "name" => Ok(json!(org.name)), + "funds" => Ok(json!(org.funds)), + "reputation" => Ok(json!(org.reputation)), + "credit_lines" => serde_json::to_value(&org.credit_lines) + .map_err(|error| format!("Failed to serialize org credit lines: {}", error)), + "assets" => serde_json::to_value(&org.assets) + .map_err(|error| format!("Failed to serialize org assets: {}", error)), + "fleet" => serde_json::to_value(&org.fleet) + .map_err(|error| format!("Failed to serialize org fleet: {}", error)), + "members" => serde_json::to_value(&org.members) + .map_err(|error| format!("Failed to serialize org members: {}", error)), + _ => Err(format!("Unknown field: {}", field)), + } +} + +fn format_currency(amount: f64) -> String { + let rounded = amount.max(0.0).round() as i64; + let digits = rounded.to_string(); + let mut formatted = String::new(); + + for (index, character) in digits.chars().rev().enumerate() { + if index > 0 && index % 3 == 0 { + formatted.push(','); + } + formatted.push(character); + } + + formatted.chars().rev().collect() +} diff --git a/lib/services/src/store.rs b/lib/services/src/store.rs new file mode 100644 index 0000000..643b3c2 --- /dev/null +++ b/lib/services/src/store.rs @@ -0,0 +1,685 @@ +use forge_models::{ + Bank, BankCheckoutContext, BankMutationResult, EquipmentCategory, HotOrgRecord, Item, Locker, + OrgFleetEntry, StoreCheckoutContext, StoreCheckoutResult, StoreGrantedItem, + StoreGrantedVehicle, VGarage, VLocker, VehicleCategory, +}; +use forge_repositories::{ + BankHotRepository, BankRepository, LockerHotRepository, LockerRepository, OrgHotRepository, + OrgRepository, VGarageHotRepository, VGarageRepository, VLockerHotRepository, + VLockerRepository, +}; +use serde_json::json; +use std::collections::HashMap; + +use crate::{ + BankHotStateService, LockerHotStateService, OrgHotStateService, VGarageHotStateService, + VLockerHotStateService, +}; + +pub trait StoreBankBackend { + fn get_bank(&self, uid: &str) -> Result; + fn preview_checkout( + &self, + uid: &str, + amount: f64, + source: &str, + ) -> Result; + fn override_bank(&self, uid: &str, bank: &Bank) -> Result; +} + +pub trait StoreOrgBackend { + fn get_org(&self, org_id: &str) -> Result; + fn override_org(&self, org_id: &str, org: HotOrgRecord) -> Result; +} + +pub trait StoreLockerBackend { + fn get_locker(&self, uid: &str) -> Result; + fn override_locker(&self, uid: &str, items: HashMap) -> Result; +} + +pub trait StoreVLockerBackend { + fn fetch_locker(&self, uid: &str) -> Result; + fn override_locker(&self, uid: &str, locker: VLocker) -> Result; +} + +pub trait StoreVGarageBackend { + fn fetch_garage(&self, uid: &str) -> Result; + fn override_garage(&self, uid: &str, garage: VGarage) -> Result; +} + +impl StoreBankBackend for BankHotStateService { + fn get_bank(&self, uid: &str) -> Result { + BankHotStateService::get_bank(self, uid.to_string()) + } + + fn preview_checkout( + &self, + uid: &str, + amount: f64, + source: &str, + ) -> Result { + BankHotStateService::charge_checkout( + self, + uid.to_string(), + amount, + BankCheckoutContext { + source_field: source.to_string(), + commit: false, + }, + ) + } + + fn override_bank(&self, uid: &str, bank: &Bank) -> Result { + let json = serde_json::to_string(bank) + .map_err(|error| format!("Invalid bank override JSON: {}", error))?; + BankHotStateService::override_bank(self, uid.to_string(), json) + } +} + +impl StoreBankBackend for &BankHotStateService { + fn get_bank(&self, uid: &str) -> Result { + BankHotStateService::get_bank(self, uid.to_string()) + } + + fn preview_checkout( + &self, + uid: &str, + amount: f64, + source: &str, + ) -> Result { + BankHotStateService::charge_checkout( + self, + uid.to_string(), + amount, + BankCheckoutContext { + source_field: source.to_string(), + commit: false, + }, + ) + } + + fn override_bank(&self, uid: &str, bank: &Bank) -> Result { + let json = serde_json::to_string(bank) + .map_err(|error| format!("Invalid bank override JSON: {}", error))?; + BankHotStateService::override_bank(self, uid.to_string(), json) + } +} + +impl StoreOrgBackend for OrgHotStateService { + fn get_org(&self, org_id: &str) -> Result { + OrgHotStateService::get_org(self, org_id.to_string()) + } + + fn override_org(&self, org_id: &str, org: HotOrgRecord) -> Result { + OrgHotStateService::override_org(self, org_id.to_string(), org) + } +} + +impl StoreOrgBackend for &OrgHotStateService { + fn get_org(&self, org_id: &str) -> Result { + OrgHotStateService::get_org(self, org_id.to_string()) + } + + fn override_org(&self, org_id: &str, org: HotOrgRecord) -> Result { + OrgHotStateService::override_org(self, org_id.to_string(), org) + } +} + +impl StoreLockerBackend + for LockerHotStateService +{ + fn get_locker(&self, uid: &str) -> Result { + LockerHotStateService::get_locker(self, uid.to_string()) + } + + fn override_locker(&self, uid: &str, items: HashMap) -> Result { + LockerHotStateService::override_locker(self, uid.to_string(), items) + } +} + +impl StoreLockerBackend + for &LockerHotStateService +{ + fn get_locker(&self, uid: &str) -> Result { + LockerHotStateService::get_locker(self, uid.to_string()) + } + + fn override_locker(&self, uid: &str, items: HashMap) -> Result { + LockerHotStateService::override_locker(self, uid.to_string(), items) + } +} + +impl StoreVLockerBackend + for VLockerHotStateService +{ + fn fetch_locker(&self, uid: &str) -> Result { + VLockerHotStateService::fetch_locker(self, uid) + } + + fn override_locker(&self, uid: &str, locker: VLocker) -> Result { + VLockerHotStateService::override_locker(self, uid, locker) + } +} + +impl StoreVLockerBackend + for &VLockerHotStateService +{ + fn fetch_locker(&self, uid: &str) -> Result { + VLockerHotStateService::fetch_locker(self, uid) + } + + fn override_locker(&self, uid: &str, locker: VLocker) -> Result { + VLockerHotStateService::override_locker(self, uid, locker) + } +} + +impl StoreVGarageBackend + for VGarageHotStateService +{ + fn fetch_garage(&self, uid: &str) -> Result { + VGarageHotStateService::fetch_garage(self, uid) + } + + fn override_garage(&self, uid: &str, garage: VGarage) -> Result { + VGarageHotStateService::override_garage(self, uid, garage) + } +} + +impl StoreVGarageBackend + for &VGarageHotStateService +{ + fn fetch_garage(&self, uid: &str) -> Result { + VGarageHotStateService::fetch_garage(self, uid) + } + + fn override_garage(&self, uid: &str, garage: VGarage) -> Result { + VGarageHotStateService::override_garage(self, uid, garage) + } +} + +pub struct StoreService { + bank: B, + org: O, + locker: L, + vlocker: VL, + vgarage: VG, +} + +impl StoreService { + pub fn new(bank: B, org: O, locker: L, vlocker: VL, vgarage: VG) -> Self { + Self { + bank, + org, + locker, + vlocker, + vgarage, + } + } +} + +impl StoreService +where + B: StoreBankBackend, + O: StoreOrgBackend, + L: StoreLockerBackend, + VL: StoreVLockerBackend, + VG: StoreVGarageBackend, +{ + pub fn checkout(&self, context: StoreCheckoutContext) -> Result { + if context.requester_uid.trim().is_empty() { + return Err("A valid requester UID is required.".to_string()); + } + if context.items.is_empty() && context.vehicles.is_empty() { + return Err("Add at least one item before checkout.".to_string()); + } + + let charged_total = checkout_total(&context); + if charged_total <= 0.0 { + return Err("Checkout total must be greater than zero.".to_string()); + } + + let requester_uid = context.requester_uid.trim(); + let payment_method = context.payment_method.trim().to_ascii_lowercase(); + + let original_locker = self.locker.get_locker(requester_uid)?; + let original_vlocker = self.vlocker.fetch_locker(requester_uid)?; + let original_vgarage = self.vgarage.fetch_garage(requester_uid)?; + + let mut next_locker = original_locker.clone(); + let mut next_vlocker = original_vlocker.clone(); + let mut next_vgarage = original_vgarage.clone(); + + let mut locker_patch = HashMap::new(); + let mut va_patch = HashMap::new(); + let mut vgarage_patch = HashMap::new(); + let mut locker_granted = Vec::new(); + let mut vehicle_granted = Vec::new(); + let mut va_categories_changed: Vec<&str> = Vec::new(); + let mut vgarage_categories_changed: Vec<&str> = Vec::new(); + + for item_seed in &context.items { + if item_seed.classname.trim().is_empty() || item_seed.quantity == 0 { + return Err("Checkout contains an invalid item entry.".to_string()); + } + + let locker_category = resolve_locker_category(&item_seed.category)?; + let arsenal_category = resolve_arsenal_category(&item_seed.category)?; + + let existing_amount = next_locker + .items + .get(&item_seed.classname) + .map(|entry| entry.amount) + .unwrap_or(0); + let updated_item = Item { + category: locker_category.to_string(), + classname: item_seed.classname.clone(), + amount: existing_amount.saturating_add(item_seed.quantity), + }; + + next_locker + .items + .insert(item_seed.classname.clone(), updated_item.clone()); + locker_patch.insert( + item_seed.classname.clone(), + serde_json::to_value(&updated_item) + .map_err(|error| format!("Failed to serialize locker patch: {}", error))?, + ); + locker_granted.push(StoreGrantedItem { + classname: item_seed.classname.clone(), + category: locker_category.to_string(), + quantity: item_seed.quantity, + }); + + match arsenal_category { + EquipmentCategory::Items => { + push_unique(&mut next_vlocker.items, &item_seed.classname); + push_unique_str(&mut va_categories_changed, "items"); + } + EquipmentCategory::Weapons => { + push_unique(&mut next_vlocker.weapons, &item_seed.classname); + push_unique_str(&mut va_categories_changed, "weapons"); + } + EquipmentCategory::Magazines => { + push_unique(&mut next_vlocker.magazines, &item_seed.classname); + push_unique_str(&mut va_categories_changed, "magazines"); + } + EquipmentCategory::Backpacks => { + push_unique(&mut next_vlocker.backpacks, &item_seed.classname); + push_unique_str(&mut va_categories_changed, "backpacks"); + } + } + } + + if next_locker.items.len() > 25 { + return Err( + "Locker capacity would exceed 25 unique items. Clear space before checkout." + .to_string(), + ); + } + + for category in va_categories_changed { + match category { + "items" => { + va_patch.insert(category.to_string(), json!(next_vlocker.items)); + } + "weapons" => { + va_patch.insert(category.to_string(), json!(next_vlocker.weapons)); + } + "magazines" => { + va_patch.insert(category.to_string(), json!(next_vlocker.magazines)); + } + "backpacks" => { + va_patch.insert(category.to_string(), json!(next_vlocker.backpacks)); + } + _ => {} + } + } + + for vehicle_seed in &context.vehicles { + if vehicle_seed.classname.trim().is_empty() { + return Err("Vehicle checkout entry was missing a classname.".to_string()); + } + + let vehicle_category = resolve_vehicle_category(&vehicle_seed.category)?; + match vehicle_category { + VehicleCategory::Cars => { + push_unique(&mut next_vgarage.cars, &vehicle_seed.classname); + push_unique_str(&mut vgarage_categories_changed, "cars"); + } + VehicleCategory::Armor => { + push_unique(&mut next_vgarage.armor, &vehicle_seed.classname); + push_unique_str(&mut vgarage_categories_changed, "armor"); + } + VehicleCategory::Helis => { + push_unique(&mut next_vgarage.helis, &vehicle_seed.classname); + push_unique_str(&mut vgarage_categories_changed, "helis"); + } + VehicleCategory::Planes => { + push_unique(&mut next_vgarage.planes, &vehicle_seed.classname); + push_unique_str(&mut vgarage_categories_changed, "planes"); + } + VehicleCategory::Naval => { + push_unique(&mut next_vgarage.naval, &vehicle_seed.classname); + push_unique_str(&mut vgarage_categories_changed, "naval"); + } + VehicleCategory::Other => { + push_unique(&mut next_vgarage.other, &vehicle_seed.classname); + push_unique_str(&mut vgarage_categories_changed, "other"); + } + } + + vehicle_granted.push(StoreGrantedVehicle { + classname: vehicle_seed.classname.clone(), + category: vehicle_seed.category.clone(), + }); + } + + for category in vgarage_categories_changed { + match category { + "cars" => { + vgarage_patch.insert(category.to_string(), json!(next_vgarage.cars)); + } + "armor" => { + vgarage_patch.insert(category.to_string(), json!(next_vgarage.armor)); + } + "helis" => { + vgarage_patch.insert(category.to_string(), json!(next_vgarage.helis)); + } + "planes" => { + vgarage_patch.insert(category.to_string(), json!(next_vgarage.planes)); + } + "naval" => { + vgarage_patch.insert(category.to_string(), json!(next_vgarage.naval)); + } + "other" => { + vgarage_patch.insert(category.to_string(), json!(next_vgarage.other)); + } + _ => {} + } + } + + let mut bank_patch = HashMap::new(); + let mut final_bank = None; + let mut original_bank = None; + + let mut org_patch = HashMap::new(); + let mut org_target_uids = Vec::new(); + let mut final_org = None; + let mut original_org = None; + + match payment_method.as_str() { + "cash" | "bank" => { + original_bank = Some(self.bank.get_bank(requester_uid)?); + let preview = self.bank.preview_checkout( + requester_uid, + charged_total, + payment_method.as_str(), + )?; + bank_patch = preview.patch.clone(); + final_bank = Some(preview.account); + } + "org_funds" | "credit_line" => { + if context.org_id.trim().is_empty() { + return Err("A valid organization is required for this checkout.".to_string()); + } + + let mut org = self.org.get_org(&context.org_id)?; + original_org = Some(org.clone()); + + match payment_method.as_str() { + "org_funds" => { + if !can_manage_treasury( + &org, + requester_uid, + context.requester_is_default_org_ceo, + ) { + return Err( + "Only the organization leader or CEO can charge org funds." + .to_string(), + ); + } + if org.funds < charged_total { + return Err( + "Organization funds cannot cover this checkout.".to_string() + ); + } + org.funds -= charged_total; + org_patch.insert("funds".to_string(), json!(org.funds)); + } + "credit_line" => { + let credit_line = + org.credit_lines.get_mut(requester_uid).ok_or_else(|| { + "Assigned credit line cannot cover this checkout.".to_string() + })?; + if credit_line.amount < charged_total { + return Err( + "Assigned credit line cannot cover this checkout.".to_string() + ); + } + + credit_line.amount -= charged_total; + org_patch.insert("credit_lines".to_string(), json!(org.credit_lines)); + } + _ => unreachable!(), + } + + if payment_method == "org_funds" && !context.vehicles.is_empty() { + add_org_fleet_vehicles(&mut org, &context.vehicles); + org_patch.insert("fleet".to_string(), json!(org.fleet)); + } + + org_target_uids = resolve_member_uids(&org, Some(requester_uid)); + final_org = Some(org); + } + _ => return Err("Selected payment source is unsupported.".to_string()), + } + + let mut locker_saved = false; + let mut vlocker_saved = false; + let mut vgarage_saved = false; + let mut org_saved = false; + + let commit_result = (|| -> Result<(), String> { + if !locker_patch.is_empty() { + self.locker + .override_locker(requester_uid, next_locker.items.clone())?; + locker_saved = true; + } + + if !va_patch.is_empty() { + self.vlocker + .override_locker(requester_uid, next_vlocker.clone())?; + vlocker_saved = true; + } + + if !vgarage_patch.is_empty() { + self.vgarage + .override_garage(requester_uid, next_vgarage.clone())?; + vgarage_saved = true; + } + + if let Some(org) = final_org.clone() { + self.org.override_org(&context.org_id, org)?; + org_saved = true; + } + + if let Some(bank) = final_bank.as_ref() { + self.bank.override_bank(requester_uid, bank)?; + } + + Ok(()) + })(); + + if let Err(error) = commit_result { + if org_saved { + if let Some(org) = original_org { + let org_id = org.id.clone(); + let _ = self.org.override_org(&org_id, org); + } + } + if vgarage_saved { + let _ = self + .vgarage + .override_garage(requester_uid, original_vgarage); + } + if vlocker_saved { + let _ = self + .vlocker + .override_locker(requester_uid, original_vlocker); + } + if locker_saved { + let _ = self + .locker + .override_locker(requester_uid, original_locker.items); + } + if let Some(bank) = original_bank { + let _ = self.bank.override_bank(requester_uid, &bank); + } + return Err(error); + } + + Ok(StoreCheckoutResult { + charged_total, + payment_method, + message: format!( + "Checkout completed. {} charged, {} locker grant(s), {} vehicle unlock(s).", + format_currency(charged_total), + locker_granted.len(), + vehicle_granted.len() + ), + locker_granted, + vehicle_granted, + locker_patch, + va_patch, + vgarage_patch, + bank_patch, + org_patch, + org_target_uids, + }) + } +} + +fn checkout_total(context: &StoreCheckoutContext) -> f64 { + let item_total = context + .items + .iter() + .map(|entry| entry.price_value.max(0.0) * f64::from(entry.quantity)) + .sum::(); + let vehicle_total = context + .vehicles + .iter() + .map(|entry| entry.price_value.max(0.0)) + .sum::(); + + (item_total + vehicle_total).floor() +} + +fn resolve_locker_category(category: &str) -> Result<&'static str, String> { + match category.trim().to_ascii_lowercase().as_str() { + "item" | "attachment" => Ok("item"), + "weapon" => Ok("weapon"), + "magazine" => Ok("magazine"), + "backpack" => Ok("backpack"), + other => Err(format!("Store item category '{}' is unsupported.", other)), + } +} + +fn resolve_arsenal_category(category: &str) -> Result { + match category.trim().to_ascii_lowercase().as_str() { + "item" | "attachment" => Ok(EquipmentCategory::Items), + "weapon" => Ok(EquipmentCategory::Weapons), + "magazine" => Ok(EquipmentCategory::Magazines), + "backpack" => Ok(EquipmentCategory::Backpacks), + other => Err(format!("Store item category '{}' is unsupported.", other)), + } +} + +fn resolve_vehicle_category(category: &str) -> Result { + match category.trim().to_ascii_lowercase().as_str() { + "cars" => Ok(VehicleCategory::Cars), + "armor" => Ok(VehicleCategory::Armor), + "helis" | "heli" => Ok(VehicleCategory::Helis), + "planes" => Ok(VehicleCategory::Planes), + "naval" => Ok(VehicleCategory::Naval), + "other" => Ok(VehicleCategory::Other), + other => Err(format!("Vehicle category '{}' is unsupported.", other)), + } +} + +fn push_unique(values: &mut Vec, value: &str) { + if !values.iter().any(|entry| entry == value) { + values.push(value.to_string()); + } +} + +fn push_unique_str<'a>(values: &mut Vec<&'a str>, value: &'a str) { + if !values.contains(&value) { + values.push(value); + } +} + +fn can_manage_treasury( + org: &HotOrgRecord, + requester_uid: &str, + requester_is_default_org_ceo: bool, +) -> bool { + org.owner == requester_uid + || ((org.id.eq_ignore_ascii_case("default") || org.owner.eq_ignore_ascii_case("server")) + && requester_is_default_org_ceo) +} + +fn resolve_member_uids(org: &HotOrgRecord, requester_uid: Option<&str>) -> Vec { + let mut member_uids = org.members.keys().cloned().collect::>(); + if let Some(uid) = requester_uid { + if !uid.is_empty() && !member_uids.iter().any(|member_uid| member_uid == uid) { + member_uids.push(uid.to_string()); + } + } + member_uids +} + +fn add_org_fleet_vehicles( + org: &mut HotOrgRecord, + vehicles: &[forge_models::StoreCheckoutVehicleSeed], +) { + let mut fleet_index = org.fleet.len(); + for vehicle in vehicles { + if vehicle.classname.trim().is_empty() { + continue; + } + + let fleet_type = vehicle.category.trim().to_ascii_lowercase(); + let mut fleet_key = format!("{}_{}", vehicle.classname, fleet_index); + while org.fleet.contains_key(&fleet_key) { + fleet_index += 1; + fleet_key = format!("{}_{}", vehicle.classname, fleet_index); + } + + org.fleet.insert( + fleet_key, + OrgFleetEntry { + classname: vehicle.classname.clone(), + name: vehicle.classname.clone(), + fleet_type, + status: "Ready".to_string(), + damage: "0%".to_string(), + }, + ); + fleet_index += 1; + } +} + +fn format_currency(amount: f64) -> String { + let rounded = amount.max(0.0).round() as i64; + let digits = rounded.to_string(); + let mut formatted = String::new(); + + for (index, character) in digits.chars().rev().enumerate() { + if index > 0 && index % 3 == 0 { + formatted.push(','); + } + formatted.push(character); + } + + format!("${}", formatted.chars().rev().collect::()) +}