Merge development into master: shared Web UI runtime, bridge-driven UIs, and server-authoritative store flow #1
@ -1,3 +1,4 @@
|
||||
PREP(buildPortalPayload);
|
||||
PREP(handleUIEvents);
|
||||
PREP(initOrgClass);
|
||||
PREP(openUI);
|
||||
|
||||
@ -18,6 +18,32 @@ if (isNil QGVAR(OrgClass)) then { call FUNC(initOrgClass); };
|
||||
GVAR(OrgClass) call ["sync", [_data, _jip]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseCreateOrg), {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
private _control = uiNamespace getVariable [QGVAR(PendingBrowserControl), controlNull];
|
||||
uiNamespace setVariable [QGVAR(PendingBrowserControl), controlNull];
|
||||
|
||||
private _success = _payload getOrDefault ["success", false];
|
||||
if (!_success) exitWith {
|
||||
if (_control isNotEqualTo controlNull) then {
|
||||
private _json = toJSON (createHashMapFromArray [
|
||||
["message", _payload getOrDefault ["message", "Organization registration failed."]]
|
||||
]);
|
||||
|
||||
_control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.receiveCreateFailure(%1)", _json]];
|
||||
};
|
||||
};
|
||||
|
||||
private _orgData = _payload getOrDefault ["org", createHashMap];
|
||||
GVAR(OrgClass) call ["sync", [_orgData, true]];
|
||||
|
||||
if (_control isNotEqualTo controlNull) then {
|
||||
private _json = toJSON (call FUNC(buildPortalPayload));
|
||||
_control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.receiveCreateSuccess(%1)", _json]];
|
||||
};
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[{
|
||||
EGVAR(actor,ActorClass) get "isLoaded";
|
||||
}, {
|
||||
|
||||
129
arma/client/addons/org/functions/fnc_buildPortalPayload.sqf
Normal file
129
arma/client/addons/org/functions/fnc_buildPortalPayload.sqf
Normal file
@ -0,0 +1,129 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Builds the web portal payload from the synced org class.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Portal payload <HASHMAP>
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_org_fnc_buildPortalPayload;
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
private _orgData = GVAR(OrgClass) get "org";
|
||||
|
||||
private _name = _orgData getOrDefault ["name", "Unknown Organization"];
|
||||
private _id = _orgData getOrDefault ["id", ""];
|
||||
private _ownerUid = _orgData getOrDefault ["owner", ""];
|
||||
private _funds = _orgData getOrDefault ["funds", 0];
|
||||
private _reputation = _orgData getOrDefault ["reputation", 0];
|
||||
private _assetsRaw = _orgData getOrDefault ["assets", createHashMap];
|
||||
private _membersRaw = _orgData getOrDefault ["members", createHashMap];
|
||||
private _fleetRaw = _orgData getOrDefault ["fleet", createHashMap];
|
||||
private _headquarters = _orgData getOrDefault ["headquarters", "ArmA Verse"];
|
||||
private _type = _orgData getOrDefault ["type", "Organization"];
|
||||
private _status = _orgData getOrDefault ["status", "Operational"];
|
||||
private _isDefaultOrg = (_orgData getOrDefault ["default", false])
|
||||
|| {toLower _id isEqualTo "default"}
|
||||
|| {toLower _ownerUid isEqualTo "server"};
|
||||
|
||||
private _playerName = name player;
|
||||
private _playerUid = getPlayerUID 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 _playerUid) then { _sessionRole = "Member"; };
|
||||
|
||||
_membersList pushBack (createHashMapFromArray [["name", _memberName]]);
|
||||
} forEach _membersRaw;
|
||||
|
||||
if (_ownerName isEqualTo "" && { _ownerUid isEqualTo _playerUid }) then { _ownerName = _playerName; };
|
||||
if (_ownerName isEqualTo "" && { _ownerUid isNotEqualTo "" }) then { _ownerName = "Unknown Owner"; };
|
||||
if (_ownerUid isEqualTo _playerUid) then { _sessionRole = "Leader"; };
|
||||
|
||||
private _assetsList = [];
|
||||
{
|
||||
private _assetData = _y;
|
||||
_assetsList pushBack (createHashMapFromArray [
|
||||
["name", _assetData getOrDefault ["name", "Unknown Asset"]],
|
||||
["type", _assetData getOrDefault ["type", "items"]],
|
||||
["quantity", str (_assetData getOrDefault ["quantity", 0])]
|
||||
]);
|
||||
} 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 _roadmap = [
|
||||
createHashMapFromArray [
|
||||
["name", "Contracts Board"],
|
||||
["status", "Planned"],
|
||||
["detail", "Track payouts, assignments, and claim approvals."]
|
||||
],
|
||||
createHashMapFromArray [
|
||||
["name", "Diplomacy"],
|
||||
["status", "Future Review"],
|
||||
["detail", "Possible future module pending a full design and scope review."]
|
||||
],
|
||||
createHashMapFromArray [
|
||||
["name", "Logistics Queue"],
|
||||
["status", "Future Review"],
|
||||
["detail", "Possible future module pending a full design and scope review."]
|
||||
],
|
||||
createHashMapFromArray [
|
||||
["name", "Permissions"],
|
||||
["status", "Future Review"],
|
||||
["detail", "Possible future module pending a full design and scope review."]
|
||||
]
|
||||
];
|
||||
|
||||
createHashMapFromArray [
|
||||
["session", createHashMapFromArray [
|
||||
["actorName", _playerName],
|
||||
["actorUid", _playerUid],
|
||||
["role", _sessionRole],
|
||||
["ceo", _sessionIsCeo]
|
||||
]],
|
||||
["portalData", createHashMapFromArray [
|
||||
["org", createHashMapFromArray [
|
||||
["name", _name],
|
||||
["tag", _id],
|
||||
["type", _type],
|
||||
["status", _status],
|
||||
["headquarters", _headquarters],
|
||||
["owner", _ownerName],
|
||||
["ownerUid", _ownerUid],
|
||||
["isDefault", _isDefaultOrg]
|
||||
]],
|
||||
["funds", _funds],
|
||||
["reputation", _reputation],
|
||||
["members", _membersList],
|
||||
["fleet", _fleetList],
|
||||
["assets", _assetsList],
|
||||
["activity", []],
|
||||
["roadmap", _roadmap]
|
||||
]]
|
||||
]
|
||||
@ -30,159 +30,35 @@ private _fnc_execBridge = {
|
||||
_control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.%1(%2)", _fnName, _json]];
|
||||
};
|
||||
|
||||
private _fnc_buildPortalPayload = {
|
||||
private _orgData = GVAR(OrgClass) get "org";
|
||||
|
||||
private _name = _orgData getOrDefault ["name", "Unknown Organization"];
|
||||
private _id = _orgData getOrDefault ["id", ""];
|
||||
private _ownerUid = _orgData getOrDefault ["owner", ""];
|
||||
private _funds = _orgData getOrDefault ["funds", 0];
|
||||
private _reputation = _orgData getOrDefault ["reputation", 0];
|
||||
private _assetsRaw = _orgData getOrDefault ["assets", createHashMap];
|
||||
private _membersRaw = _orgData getOrDefault ["members", createHashMap];
|
||||
private _fleetRaw = _orgData getOrDefault ["fleet", createHashMap];
|
||||
private _headquarters = _orgData getOrDefault ["headquarters", "ArmA Verse"];
|
||||
private _type = _orgData getOrDefault ["type", "Organization"];
|
||||
private _status = _orgData getOrDefault ["status", "Operational"];
|
||||
private _isDefaultOrg = (_orgData getOrDefault ["default", false])
|
||||
|| {toLower _id isEqualTo "default"}
|
||||
|| {toLower _ownerUid isEqualTo "server"};
|
||||
|
||||
private _playerName = name player;
|
||||
private _playerUid = getPlayerUID 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 _playerUid) then {
|
||||
_sessionRole = "Member";
|
||||
};
|
||||
|
||||
_membersList pushBack (createHashMapFromArray [
|
||||
["name", _memberName]
|
||||
]);
|
||||
} forEach _membersRaw;
|
||||
|
||||
if (_ownerName isEqualTo "" && {_ownerUid isEqualTo _playerUid}) then {
|
||||
_ownerName = _playerName;
|
||||
};
|
||||
|
||||
if (_ownerName isEqualTo "" && {_ownerUid isNotEqualTo ""}) then {
|
||||
_ownerName = "Unknown Owner";
|
||||
};
|
||||
|
||||
if (_ownerUid isEqualTo _playerUid) then {
|
||||
_sessionRole = "Leader";
|
||||
};
|
||||
|
||||
private _assetsList = [];
|
||||
{
|
||||
private _assetData = _y;
|
||||
_assetsList pushBack (createHashMapFromArray [
|
||||
["name", _assetData getOrDefault ["name", "Unknown Asset"]],
|
||||
["type", _assetData getOrDefault ["type", "items"]],
|
||||
["quantity", str (_assetData getOrDefault ["quantity", 0])]
|
||||
]);
|
||||
} 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 _roadmap = [
|
||||
createHashMapFromArray [
|
||||
["name", "Contracts Board"],
|
||||
["status", "Planned"],
|
||||
["detail", "Track payouts, assignments, and claim approvals."]
|
||||
],
|
||||
createHashMapFromArray [
|
||||
["name", "Diplomacy"],
|
||||
["status", "Future Review"],
|
||||
["detail", "Possible future module pending a full design and scope review."]
|
||||
],
|
||||
createHashMapFromArray [
|
||||
["name", "Logistics Queue"],
|
||||
["status", "Future Review"],
|
||||
["detail", "Possible future module pending a full design and scope review."]
|
||||
],
|
||||
createHashMapFromArray [
|
||||
["name", "Permissions"],
|
||||
["status", "Future Review"],
|
||||
["detail", "Possible future module pending a full design and scope review."]
|
||||
]
|
||||
];
|
||||
|
||||
createHashMapFromArray [
|
||||
["session", createHashMapFromArray [
|
||||
["actorName", _playerName],
|
||||
["actorUid", _playerUid],
|
||||
["role", _sessionRole],
|
||||
["ceo", _sessionIsCeo]
|
||||
]],
|
||||
["portalData", createHashMapFromArray [
|
||||
["org", createHashMapFromArray [
|
||||
["name", _name],
|
||||
["tag", _id],
|
||||
["type", _type],
|
||||
["status", _status],
|
||||
["headquarters", _headquarters],
|
||||
["owner", _ownerName],
|
||||
["ownerUid", _ownerUid],
|
||||
["isDefault", _isDefaultOrg]
|
||||
]],
|
||||
["funds", _funds],
|
||||
["reputation", _reputation],
|
||||
["members", _membersList],
|
||||
["fleet", _fleetList],
|
||||
["assets", _assetsList],
|
||||
["activity", []],
|
||||
["roadmap", _roadmap]
|
||||
]]
|
||||
]
|
||||
};
|
||||
|
||||
diag_log format ["[FORGE:Client:Org] Handling UI event: %1 with data: %2", _event, _data];
|
||||
|
||||
switch (_event) do {
|
||||
case "org::close": { closeDialog 1; };
|
||||
case "org::login::request": {
|
||||
private _email = toLower (_data getOrDefault ["email", ""]);
|
||||
private _password = _data getOrDefault ["password", ""];
|
||||
private _orgData = GVAR(OrgClass) get "org";
|
||||
private _orgId = _orgData getOrDefault ["id", ""];
|
||||
private _orgName = _orgData getOrDefault ["name", ""];
|
||||
|
||||
if (_email isEqualTo "" || {_password isEqualTo ""}) exitWith {
|
||||
[_control, "receiveLoginFailure", createHashMapFromArray [
|
||||
["message", "Enter both email and password."]
|
||||
]] call _fnc_execBridge;
|
||||
};
|
||||
|
||||
if (_orgId isEqualTo "" && {_orgName isEqualTo ""}) exitWith {
|
||||
[_control, "receiveLoginFailure", createHashMapFromArray [
|
||||
["message", "No organization data is available for this player."]
|
||||
]] call _fnc_execBridge;
|
||||
};
|
||||
|
||||
[_control, "receiveLoginSuccess", call _fnc_buildPortalPayload] call _fnc_execBridge;
|
||||
private _payload = call FUNC(buildPortalPayload);
|
||||
[_control, "receiveLoginSuccess", _payload] call _fnc_execBridge;
|
||||
};
|
||||
case "org::create::request": {
|
||||
private _orgName = _data getOrDefault ["orgName", ""];
|
||||
|
||||
if (_orgName isEqualTo "") exitWith {
|
||||
[_control, "receiveCreateFailure", createHashMapFromArray [
|
||||
["message", "Enter an organization name."]
|
||||
]] call _fnc_execBridge;
|
||||
};
|
||||
|
||||
uiNamespace setVariable [QGVAR(PendingBrowserControl), _control];
|
||||
[SRPC(org,requestCreateOrg), [getPlayerUID player, _orgName]] call CFUNC(serverEvent);
|
||||
};
|
||||
case "org::ready": {
|
||||
[_control, "receive", createHashMapFromArray [
|
||||
|
||||
@ -30,6 +30,17 @@
|
||||
store.failLogin("Arma login bridge is unavailable.");
|
||||
}
|
||||
|
||||
function requestCreateOrg(registration) {
|
||||
store.startCreate();
|
||||
|
||||
const sent = sendEvent("org::create::request", registration);
|
||||
if (sent) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.failCreate("Arma registration bridge is unavailable.");
|
||||
}
|
||||
|
||||
function receive(eventOrPayload, data = {}) {
|
||||
const event =
|
||||
typeof eventOrPayload === "object" && eventOrPayload !== null
|
||||
@ -49,18 +60,33 @@
|
||||
store.failLogin(payloadData.message || "Authentication failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === "org::create::success") {
|
||||
store.completeCreate(payloadData);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === "org::create::failure") {
|
||||
store.failCreate(
|
||||
payloadData.message || "Organization registration failed.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RegistryApp.bridge = {
|
||||
requestLogin,
|
||||
requestCreateOrg,
|
||||
receive,
|
||||
sendEvent,
|
||||
};
|
||||
|
||||
window.OrgUIBridge = {
|
||||
requestLogin,
|
||||
requestCreateOrg,
|
||||
receive,
|
||||
receiveLoginSuccess: (data) => receive("org::login::success", data),
|
||||
receiveLoginFailure: (data) => receive("org::login::failure", data),
|
||||
receiveCreateSuccess: (data) => receive("org::create::success", data),
|
||||
receiveCreateFailure: (data) => receive("org::create::failure", data),
|
||||
};
|
||||
})();
|
||||
|
||||
107
arma/client/addons/org/ui/_site/components/AppShell.js
Normal file
107
arma/client/addons/org/ui/_site/components/AppShell.js
Normal file
@ -0,0 +1,107 @@
|
||||
(function () {
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
const store = RegistryApp.store;
|
||||
|
||||
RegistryApp.components = RegistryApp.components || {};
|
||||
|
||||
RegistryApp.components.App = function App() {
|
||||
const Navbar = window.SharedUI.componentFns.Navbar;
|
||||
const Header = window.SharedUI.componentFns.Header;
|
||||
const Footer = window.SharedUI.componentFns.Footer;
|
||||
const HomeView = RegistryApp.componentFns.HomeView;
|
||||
const RegistrationView = RegistryApp.componentFns.RegistrationView;
|
||||
const PortalApp =
|
||||
window.OrgPortal && window.OrgPortal.components
|
||||
? window.OrgPortal.components.App
|
||||
: null;
|
||||
|
||||
const view = store.getView();
|
||||
const viewLabel =
|
||||
view === "create"
|
||||
? "Organization Registration"
|
||||
: view === "portal"
|
||||
? "Organization Portal"
|
||||
: "Entry Hub";
|
||||
const actionLabel = view === "portal" ? "Sign Out" : "Close";
|
||||
const footerSections = [
|
||||
{
|
||||
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 closeRegistry() {
|
||||
if (
|
||||
typeof A3API !== "undefined" &&
|
||||
typeof A3API.SendAlert === "function"
|
||||
) {
|
||||
A3API.SendAlert(
|
||||
JSON.stringify({
|
||||
event: "org::close",
|
||||
data: {},
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
store.setView("home");
|
||||
}
|
||||
|
||||
if (view === "portal" && PortalApp) {
|
||||
return h(
|
||||
"div",
|
||||
null,
|
||||
Navbar({
|
||||
title: "Global Organization Network",
|
||||
viewLabel,
|
||||
actionLabel,
|
||||
onAction: closeRegistry,
|
||||
}),
|
||||
PortalApp(),
|
||||
);
|
||||
}
|
||||
|
||||
let mainContent;
|
||||
if (view === "home") {
|
||||
mainContent = HomeView();
|
||||
} else if (view === "create") {
|
||||
mainContent = RegistrationView();
|
||||
}
|
||||
|
||||
return h(
|
||||
"main",
|
||||
null,
|
||||
Navbar({
|
||||
title: "Global Organization Network",
|
||||
viewLabel,
|
||||
actionLabel,
|
||||
onAction: closeRegistry,
|
||||
}),
|
||||
h(
|
||||
"div",
|
||||
{ className: "container" },
|
||||
Header({
|
||||
title: "Global Organization Network",
|
||||
onTitleClick: () => store.setView("home"),
|
||||
}),
|
||||
mainContent,
|
||||
),
|
||||
Footer({ sections: footerSections }),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,40 +1,29 @@
|
||||
(function () {
|
||||
const SharedUI = (window.SharedUI = window.SharedUI || {});
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
|
||||
RegistryApp.componentFns = RegistryApp.componentFns || {};
|
||||
SharedUI.componentFns = SharedUI.componentFns || {};
|
||||
|
||||
RegistryApp.componentFns.Footer = function Footer() {
|
||||
SharedUI.componentFns.Footer = function Footer({ sections = [] }) {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "footer" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "wrapper" },
|
||||
...sections.map((section) =>
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h3", null, "Registry Resources"),
|
||||
h("h3", null, section.title),
|
||||
h(
|
||||
"ul",
|
||||
{ style: { listStyleType: "none", padding: 0 } },
|
||||
h("li", null, "Registration Guidelines"),
|
||||
h("li", null, "Tax & Fee Schedule"),
|
||||
h("li", null, "Legal Compliance"),
|
||||
h("li", null, "Trademark Database"),
|
||||
...(section.items || []).map((item) =>
|
||||
h("li", null, item),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h3", null, "Bureau Support"),
|
||||
h(
|
||||
"ul",
|
||||
{ style: { listStyleType: "none", padding: 0 } },
|
||||
h("li", null, "Office: Sector 7 Admin Block"),
|
||||
h("li", null, "Hours: 0800 - 1600 (GST)"),
|
||||
h("li", null, "Helpdesk: 555-01-REGISTRY"),
|
||||
h("li", null, "support@org-bureau.gov"),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
.split-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
text-align: left;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.app-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: left;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cancel-link {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.form-feedback {
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.92rem;
|
||||
|
||||
&.is-error {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.split-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,27 @@
|
||||
(function () {
|
||||
const SharedUI = (window.SharedUI = window.SharedUI || {});
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
const store = RegistryApp.store;
|
||||
|
||||
RegistryApp.componentFns = RegistryApp.componentFns || {};
|
||||
SharedUI.componentFns = SharedUI.componentFns || {};
|
||||
|
||||
RegistryApp.componentFns.Header = function Header({ title }) {
|
||||
SharedUI.componentFns.Header = function Header({
|
||||
title,
|
||||
subtitle = "Organization Registration & Management Portal",
|
||||
onTitleClick = null,
|
||||
}) {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "header" },
|
||||
h(
|
||||
"h1",
|
||||
{
|
||||
style: { cursor: "pointer" },
|
||||
onClick: () => store.setView("home"),
|
||||
style: { cursor: onTitleClick ? "pointer" : "default" },
|
||||
onClick: onTitleClick,
|
||||
},
|
||||
title,
|
||||
),
|
||||
h("p", null, "Organization Registration & Management Portal"),
|
||||
h("p", null, subtitle),
|
||||
);
|
||||
};
|
||||
})();
|
||||
|
||||
35
arma/client/addons/org/ui/_site/components/hero.js
Normal file
35
arma/client/addons/org/ui/_site/components/hero.js
Normal file
@ -0,0 +1,35 @@
|
||||
(function () {
|
||||
const SharedUI = (window.SharedUI = window.SharedUI || {});
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
|
||||
SharedUI.componentFns = SharedUI.componentFns || {};
|
||||
|
||||
SharedUI.componentFns.Hero = function Hero({
|
||||
className = "",
|
||||
kicker = "",
|
||||
title = "",
|
||||
subtitle = "",
|
||||
meta = "",
|
||||
}) {
|
||||
const finalClassName = [
|
||||
"card org-panel org-span-12 org-page-header",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: finalClassName },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-page-heading" },
|
||||
h("span", { className: "org-page-kicker" }, kicker),
|
||||
h("h1", { className: "org-page-title" }, title),
|
||||
h("p", { className: "org-page-subtitle" }, subtitle),
|
||||
h("span", { className: "org-page-meta" }, meta),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,12 +0,0 @@
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
(function () {
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
const store = RegistryApp.store;
|
||||
|
||||
RegistryApp.componentFns = RegistryApp.componentFns || {};
|
||||
|
||||
RegistryApp.componentFns.HomeView = function HomeView() {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "content" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "card" },
|
||||
h("h2", null, "Create Organization"),
|
||||
h(
|
||||
"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.",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{ onClick: () => store.setView("create") },
|
||||
"Register",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "card" },
|
||||
h("h2", null, "Organization Portal"),
|
||||
h(
|
||||
"p",
|
||||
null,
|
||||
"Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink.",
|
||||
),
|
||||
h("button", { onClick: () => store.setView("login") }, "Login"),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,48 +0,0 @@
|
||||
(function () {
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
const store = RegistryApp.store;
|
||||
|
||||
RegistryApp.components = RegistryApp.components || {};
|
||||
|
||||
RegistryApp.components.App = function App() {
|
||||
const Navbar = RegistryApp.componentFns.Navbar;
|
||||
const Header = RegistryApp.componentFns.Header;
|
||||
const HomeView = RegistryApp.componentFns.HomeView;
|
||||
const LoginForm = RegistryApp.componentFns.LoginForm;
|
||||
const CreateOrgForm = RegistryApp.componentFns.CreateOrgForm;
|
||||
const Footer = RegistryApp.componentFns.Footer;
|
||||
const PortalApp =
|
||||
window.OrgPortal && window.OrgPortal.components
|
||||
? window.OrgPortal.components.App
|
||||
: null;
|
||||
|
||||
const view = store.getView();
|
||||
|
||||
if (view === "portal" && PortalApp) {
|
||||
return h("div", null, Navbar(), PortalApp());
|
||||
}
|
||||
|
||||
let mainContent;
|
||||
if (view === "home") {
|
||||
mainContent = HomeView();
|
||||
} else if (view === "login") {
|
||||
mainContent = LoginForm();
|
||||
} else if (view === "create") {
|
||||
mainContent = CreateOrgForm();
|
||||
}
|
||||
|
||||
return h(
|
||||
"main",
|
||||
null,
|
||||
Navbar(),
|
||||
h(
|
||||
"div",
|
||||
{ className: "container" },
|
||||
Header({ title: "Global Organization Network" }),
|
||||
mainContent,
|
||||
),
|
||||
Footer(),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,96 +0,0 @@
|
||||
(function () {
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
const store = RegistryApp.store;
|
||||
|
||||
RegistryApp.componentFns = RegistryApp.componentFns || {};
|
||||
|
||||
RegistryApp.componentFns.LoginForm = function LoginForm() {
|
||||
const bridge = RegistryApp.bridge;
|
||||
const isAuthenticating = store.getIsAuthenticating();
|
||||
const loginError = store.getLoginError();
|
||||
|
||||
const handleLogin = () => {
|
||||
const data = {
|
||||
email: String(
|
||||
document.getElementById("org-login-email")?.value || "",
|
||||
),
|
||||
password: String(
|
||||
document.getElementById("org-login-password")?.value || "",
|
||||
),
|
||||
};
|
||||
|
||||
if (!bridge) {
|
||||
store.failLogin("Login bridge is not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
bridge.requestLogin(data);
|
||||
};
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
className: "card",
|
||||
style: { maxWidth: "400px", margin: "0 auto" },
|
||||
},
|
||||
h("h2", null, "Organization Login"),
|
||||
h(
|
||||
"div",
|
||||
{ className: "app-form" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("label", null, "Email"),
|
||||
h("input", {
|
||||
id: "org-login-email",
|
||||
type: "text",
|
||||
placeholder: "admin@spearnet.mil",
|
||||
}),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("label", null, "Password"),
|
||||
h("input", {
|
||||
id: "org-login-password",
|
||||
type: "password",
|
||||
placeholder: "********",
|
||||
disabled: isAuthenticating,
|
||||
}),
|
||||
),
|
||||
loginError
|
||||
? h(
|
||||
"div",
|
||||
{ className: "form-feedback is-error" },
|
||||
loginError,
|
||||
)
|
||||
: null,
|
||||
h(
|
||||
"div",
|
||||
{ className: "form-actions" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
style: { width: "100%" },
|
||||
onClick: handleLogin,
|
||||
disabled: isAuthenticating,
|
||||
},
|
||||
isAuthenticating
|
||||
? "Authenticating..."
|
||||
: "Access Authenticator",
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
className: "cancel-link",
|
||||
onClick: () => store.setView("home"),
|
||||
},
|
||||
"Cancel / Return to Main",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
190
arma/client/addons/org/ui/_site/components/modal.js
Normal file
190
arma/client/addons/org/ui/_site/components/modal.js
Normal file
@ -0,0 +1,190 @@
|
||||
(function () {
|
||||
const SharedUI = (window.SharedUI = window.SharedUI || {});
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h, ensureScopedStyle } = RegistryApp.runtime;
|
||||
const scopeAttr = "data-ui-modal";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const modalCss = `
|
||||
${scopeSelector} {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgb(15 23 42 / 0.38);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-card {
|
||||
width: min(100%, 30rem);
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-title {
|
||||
margin: 0;
|
||||
color: var(--primary-hover);
|
||||
font-size: 1.45rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-close {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 0;
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-main);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-close:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
color: var(--text-main);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-form label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-form input,
|
||||
${scopeSelector} .app-modal-form select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-form input:focus,
|
||||
${scopeSelector} .app-modal-form select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12);
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-form input:disabled,
|
||||
${scopeSelector} .app-modal-form select:disabled {
|
||||
background: #f1f5f9;
|
||||
color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-actions button + button,
|
||||
${scopeSelector} .app-modal-danger-actions button + button {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-danger {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: var(--radius);
|
||||
background: #fff1f2;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-danger p {
|
||||
margin: 0;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-danger-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .app-modal-head,
|
||||
${scopeSelector} .app-modal-danger {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
SharedUI.componentFns = SharedUI.componentFns || {};
|
||||
|
||||
SharedUI.componentFns.Modal = function Modal({
|
||||
title = "",
|
||||
body = null,
|
||||
onClose = null,
|
||||
}) {
|
||||
ensureScopedStyle("shared-modal", modalCss);
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
className: "app-modal-backdrop",
|
||||
[scopeAttr]: "",
|
||||
onClick: (e) => {
|
||||
if (e.target === e.currentTarget && onClose) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
},
|
||||
h(
|
||||
"div",
|
||||
{ className: "card app-modal-card" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "app-modal-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h2", { className: "app-modal-title" }, title),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "app-modal-close",
|
||||
onClick: onClose,
|
||||
"aria-label": "Close dialog",
|
||||
},
|
||||
"x",
|
||||
),
|
||||
),
|
||||
body,
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,79 +0,0 @@
|
||||
.app-navbar {
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.app-navbar-inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 2rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app-navbar-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.app-navbar-kicker {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-navbar-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-hover);
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.app-navbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.app-navbar-view {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-close-btn {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
color: var(--primary-hover);
|
||||
border-color: var(--primary);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.app-navbar-inner {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.app-navbar-actions {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
@ -1,54 +1,113 @@
|
||||
(function () {
|
||||
const SharedUI = (window.SharedUI = window.SharedUI || {});
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
const store = RegistryApp.store;
|
||||
|
||||
RegistryApp.componentFns = RegistryApp.componentFns || {};
|
||||
|
||||
function closeRegistry() {
|
||||
if (
|
||||
typeof A3API !== "undefined" &&
|
||||
typeof A3API.SendAlert === "function"
|
||||
) {
|
||||
A3API.SendAlert(
|
||||
JSON.stringify({
|
||||
event: "org::close",
|
||||
data: {},
|
||||
}),
|
||||
);
|
||||
return;
|
||||
const { h, ensureScopedStyle } = RegistryApp.runtime;
|
||||
const scopeAttr = "data-ui-navbar";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const navbarCss = `
|
||||
${scopeSelector} {
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
store.setView("home");
|
||||
${scopeSelector} .app-navbar-inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 2rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
RegistryApp.componentFns.Navbar = function Navbar() {
|
||||
const view = store.getView();
|
||||
const viewLabel =
|
||||
view === "login"
|
||||
? "Organization Login"
|
||||
: view === "create"
|
||||
? "Organization Registration"
|
||||
: view === "portal"
|
||||
? "Organization Portal"
|
||||
: "Entry Hub";
|
||||
const actionLabel = view === "portal" ? "Sign Out" : "Close";
|
||||
${scopeSelector} .app-navbar-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-navbar-kicker {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-navbar-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-hover);
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-navbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-navbar-view {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-close-btn {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-close-btn:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
color: var(--primary-hover);
|
||||
border-color: var(--primary);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .app-navbar-inner {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-navbar-actions {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
SharedUI.componentFns = SharedUI.componentFns || {};
|
||||
|
||||
SharedUI.componentFns.Navbar = function Navbar({
|
||||
kicker = "ORBIS",
|
||||
title = "",
|
||||
viewLabel = "",
|
||||
actionLabel = "",
|
||||
onAction = null,
|
||||
}) {
|
||||
ensureScopedStyle("shared-navbar", navbarCss);
|
||||
|
||||
return h(
|
||||
"nav",
|
||||
{ className: "app-navbar" },
|
||||
{ className: "app-navbar", [scopeAttr]: "" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "app-navbar-inner" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "app-navbar-brand" },
|
||||
h("span", { className: "app-navbar-kicker" }, "ORBIS"),
|
||||
h(
|
||||
"span",
|
||||
{ className: "app-navbar-title" },
|
||||
"Global Organization Network",
|
||||
),
|
||||
h("span", { className: "app-navbar-kicker" }, kicker),
|
||||
h("span", { className: "app-navbar-title" }, title),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
@ -59,7 +118,7 @@
|
||||
{
|
||||
type: "button",
|
||||
className: "app-close-btn",
|
||||
onClick: closeRegistry,
|
||||
onClick: onAction,
|
||||
},
|
||||
actionLabel,
|
||||
),
|
||||
|
||||
83
arma/client/addons/org/ui/_site/components/panelCard.js
Normal file
83
arma/client/addons/org/ui/_site/components/panelCard.js
Normal file
@ -0,0 +1,83 @@
|
||||
(function () {
|
||||
const SharedUI = (window.SharedUI = window.SharedUI || {});
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h, ensureScopedStyle } = RegistryApp.runtime;
|
||||
const scopeAttr = "data-ui-panel-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const panelCardCss = `
|
||||
${scopeSelector} .org-panel-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-eyebrow {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-panel-title {
|
||||
margin: 0;
|
||||
color: var(--primary-hover);
|
||||
font-size: 1.45rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-panel-subtitle {
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-panel-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
SharedUI.componentFns = SharedUI.componentFns || {};
|
||||
|
||||
SharedUI.componentFns.PanelCard = function PanelCard({
|
||||
className = "",
|
||||
eyebrow = "",
|
||||
title = "",
|
||||
subtitle = "",
|
||||
headerExtras = null,
|
||||
body = null,
|
||||
rootProps = {},
|
||||
}) {
|
||||
const finalClassName = ["card org-panel", className]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
ensureScopedStyle("shared-panel-card", panelCardCss);
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: finalClassName, [scopeAttr]: "", ...rootProps },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-panel-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
eyebrow
|
||||
? h("div", { className: "org-eyebrow" }, eyebrow)
|
||||
: null,
|
||||
h("h2", { className: "org-panel-title" }, title),
|
||||
subtitle
|
||||
? h("p", { className: "org-panel-subtitle" }, subtitle)
|
||||
: null,
|
||||
),
|
||||
headerExtras,
|
||||
),
|
||||
body,
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,79 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const scopeAttr = "data-ui-activity-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const activityCardCss = `
|
||||
${scopeSelector} .org-activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 0.85rem;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 0.35rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #94a3b8 #e2e8f0;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-activity-row {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid #94a3b8;
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-activity-row:nth-child(even) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
border-left-color: #64748b;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-activity-row p {
|
||||
margin: 0;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-activity-time {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.35rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.ActivityCard = function ActivityCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
ensureScopedStyle("portal-activity-card", activityCardCss);
|
||||
|
||||
return PanelCard({
|
||||
className: "org-scroll-panel org-span-6",
|
||||
title: "Command Feed",
|
||||
subtitle: "Recent organization-level actions and updates.",
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
{ className: "org-activity-list" },
|
||||
...portalData.activity.map((item) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-activity-row" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "org-activity-time" },
|
||||
item.time,
|
||||
),
|
||||
h("p", null, item.text),
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,94 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const actions = OrgPortal.actions;
|
||||
const scopeAttr = "data-ui-assets-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const assetsCardCss = `
|
||||
${scopeSelector} .org-simple-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 0.85rem;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 0.35rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #94a3b8 #e2e8f0;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-row:nth-child(even) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-name {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-simple-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.AssetsCard = function AssetsCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
const SimpleStat = OrgPortal.componentFns.SimpleStat;
|
||||
ensureScopedStyle("portal-assets-card", assetsCardCss);
|
||||
|
||||
return PanelCard({
|
||||
className: "org-scroll-panel org-span-7",
|
||||
title: "Assets",
|
||||
subtitle: "Inventory supplies and equipment with quantity totals.",
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
{ className: "org-simple-list" },
|
||||
...portalData.assets.map((asset) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-simple-row" },
|
||||
h(
|
||||
"strong",
|
||||
{ className: "org-simple-name" },
|
||||
asset.name,
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-simple-meta" },
|
||||
SimpleStat(
|
||||
"Type",
|
||||
actions.formatAssetType(asset.type),
|
||||
),
|
||||
SimpleStat("Quantity", asset.quantity),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,70 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const permissions = OrgPortal.permissions;
|
||||
const actions = OrgPortal.actions;
|
||||
const scopeAttr = "data-ui-danger-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const dangerCardCss = `
|
||||
${scopeSelector} {
|
||||
border-color: #fecaca;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-danger-copy {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-danger-copy strong,
|
||||
${scopeSelector} .org-danger-copy p {
|
||||
display: block;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-danger-copy p {
|
||||
margin: 0.4rem 0 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.DangerCard = function DangerCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
ensureScopedStyle("portal-danger-card", dangerCardCss);
|
||||
|
||||
if (!permissions.canDisbandOrg()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return PanelCard({
|
||||
className: "org-span-12 org-danger-panel",
|
||||
title: "Organization Controls",
|
||||
subtitle:
|
||||
"Leader-only actions for membership and permanent organization removal.",
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-danger-copy" },
|
||||
h("strong", null, "Disband organization"),
|
||||
h(
|
||||
"p",
|
||||
null,
|
||||
"This removes the organization and revokes access to the portal for all members.",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-danger-btn",
|
||||
onClick: () => actions.openModal("disband"),
|
||||
},
|
||||
"Disband Organization",
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
101
arma/client/addons/org/ui/_site/components/portal/fleetCard.js
Normal file
101
arma/client/addons/org/ui/_site/components/portal/fleetCard.js
Normal file
@ -0,0 +1,101 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const actions = OrgPortal.actions;
|
||||
const scopeAttr = "data-ui-fleet-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const fleetCardCss = `
|
||||
${scopeSelector} .org-simple-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 0.85rem;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 0.35rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #94a3b8 #e2e8f0;
|
||||
}
|
||||
|
||||
${scopeSelector} {
|
||||
min-height: 32.5rem;
|
||||
max-height: 32.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-row:nth-child(even) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-name {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-simple-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.FleetCard = function FleetCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
const SimpleStat = OrgPortal.componentFns.SimpleStat;
|
||||
ensureScopedStyle("portal-fleet-card", fleetCardCss);
|
||||
|
||||
return PanelCard({
|
||||
className: "org-scroll-panel org-span-7",
|
||||
title: "Fleet",
|
||||
subtitle:
|
||||
"Individual vehicles with type, status, and overall damage.",
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
{ className: "org-simple-list" },
|
||||
...portalData.fleet.map((unit) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-simple-row" },
|
||||
h(
|
||||
"strong",
|
||||
{ className: "org-simple-name" },
|
||||
unit.name,
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-simple-meta" },
|
||||
SimpleStat(
|
||||
"Type",
|
||||
actions.formatVehicleType(unit.type),
|
||||
),
|
||||
SimpleStat("Status", unit.status),
|
||||
SimpleStat("Damage", unit.damage),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
105
arma/client/addons/org/ui/_site/components/portal/futureCard.js
Normal file
105
arma/client/addons/org/ui/_site/components/portal/futureCard.js
Normal file
@ -0,0 +1,105 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const scopeAttr = "data-ui-future-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const futureCardCss = `
|
||||
${scopeSelector} .org-roadmap-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 0.35rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #94a3b8 #e2e8f0;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-roadmap-card {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.7rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-roadmap-card:nth-child(4n + 2),
|
||||
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
|
||||
border-color: rgb(100 116 139 / 0.4);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-roadmap-card p {
|
||||
margin: 0;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-list-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
background: #e2e8f0;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-roadmap-card:nth-child(4n + 2) .org-list-tag,
|
||||
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {
|
||||
background: #cbd5e1;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-roadmap-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) {
|
||||
background: #f8fafc;
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {
|
||||
background: #e2e8f0;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.FutureCard = function FutureCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
ensureScopedStyle("portal-future-card", futureCardCss);
|
||||
|
||||
return PanelCard({
|
||||
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: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
{ className: "org-roadmap-grid" },
|
||||
...portalData.roadmap.map((item) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-roadmap-card" },
|
||||
h("span", { className: "org-list-tag" }, item.status),
|
||||
h("strong", null, item.name),
|
||||
h("p", null, item.detail),
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
@ -1,42 +1,83 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const store = OrgPortal.store;
|
||||
const permissions = OrgPortal.permissions;
|
||||
const actions = OrgPortal.actions;
|
||||
const scopeAttr = "data-ui-members-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const membersCardCss = `
|
||||
${scopeSelector} .org-name-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 0.85rem;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 0.35rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #94a3b8 #e2e8f0;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-name-row:nth-child(even) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-name-row button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-name-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-name-row button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.MembersCard = function MembersCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
const members = store.getMembers();
|
||||
const allowMemberManagement = permissions.canManageMembers();
|
||||
ensureScopedStyle("portal-members-card", membersCardCss);
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: "card org-panel org-scroll-panel org-span-5" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-panel-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h2", { className: "org-panel-title" }, "Members"),
|
||||
h(
|
||||
"p",
|
||||
{ className: "org-panel-subtitle" },
|
||||
"Current roster listing with member removal controls.",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
return PanelCard({
|
||||
className: "org-scroll-panel org-span-5",
|
||||
title: "Members",
|
||||
subtitle:
|
||||
"Current roster listing. The organization owner cannot be removed.",
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
{ className: "org-name-list" },
|
||||
...members.map((member) =>
|
||||
h(
|
||||
...members.map((member) => {
|
||||
const canRemoveMember =
|
||||
allowMemberManagement &&
|
||||
!actions.isOwnerMember(member.name);
|
||||
|
||||
return h(
|
||||
"article",
|
||||
{ className: "org-name-row" },
|
||||
h("strong", null, member.name),
|
||||
allowMemberManagement
|
||||
canRemoveMember
|
||||
? h(
|
||||
"button",
|
||||
{
|
||||
@ -67,9 +108,9 @@
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,77 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const scopeAttr = "data-ui-metric-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const metricCardCss = `
|
||||
${scopeSelector} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
padding: 1rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||
}
|
||||
|
||||
${scopeSelector}:nth-child(4n + 2),
|
||||
${scopeSelector}:nth-child(4n + 3) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%);
|
||||
border-color: rgb(100 116 139 / 0.35);
|
||||
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-metric-label {
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-metric-value {
|
||||
font-size: 1.8rem;
|
||||
color: var(--primary-hover);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
${scopeSelector}:nth-child(4n + 2) .org-metric-value,
|
||||
${scopeSelector}:nth-child(4n + 3) .org-metric-value {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-metric-note {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector}:nth-child(4n + 3) {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||
border-color: var(--border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
${scopeSelector}:nth-child(4n + 3) .org-metric-value {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.MetricCard = function MetricCard(
|
||||
label,
|
||||
value,
|
||||
note,
|
||||
) {
|
||||
ensureScopedStyle("portal-metric-card", metricCardCss);
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "org-metric-card", [scopeAttr]: "" },
|
||||
h("span", { className: "org-metric-label" }, label),
|
||||
h("strong", { className: "org-metric-value" }, value),
|
||||
h("span", { className: "org-metric-note" }, note),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -8,6 +8,7 @@
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.ModalLayer = function ModalLayer() {
|
||||
const Modal = window.SharedUI.componentFns.Modal;
|
||||
const modal = store.getModal();
|
||||
if (!modal) {
|
||||
return null;
|
||||
@ -24,7 +25,7 @@
|
||||
title = "Run Payroll";
|
||||
body = h(
|
||||
"div",
|
||||
{ className: "org-modal-form" },
|
||||
{ className: "app-modal-form" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
@ -39,7 +40,7 @@
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-modal-actions" },
|
||||
{ className: "app-modal-actions" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
@ -75,7 +76,7 @@
|
||||
title = "Send Funds";
|
||||
body = h(
|
||||
"div",
|
||||
{ className: "org-modal-form" },
|
||||
{ className: "app-modal-form" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
@ -104,7 +105,7 @@
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-modal-actions" },
|
||||
{ className: "app-modal-actions" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
@ -146,7 +147,7 @@
|
||||
title = "Assign Credit Line";
|
||||
body = h(
|
||||
"div",
|
||||
{ className: "org-modal-form" },
|
||||
{ className: "app-modal-form" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
@ -172,7 +173,7 @@
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-modal-actions" },
|
||||
{ className: "app-modal-actions" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
@ -214,7 +215,7 @@
|
||||
title = "Disband Organization";
|
||||
body = h(
|
||||
"div",
|
||||
{ className: "org-danger-confirm" },
|
||||
{ className: "app-modal-danger" },
|
||||
h(
|
||||
"p",
|
||||
null,
|
||||
@ -224,7 +225,7 @@
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-danger-actions" },
|
||||
{ className: "app-modal-danger-actions" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
@ -247,40 +248,10 @@
|
||||
);
|
||||
}
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
className: "org-modal-backdrop",
|
||||
onClick: (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
actions.closeModal();
|
||||
}
|
||||
},
|
||||
},
|
||||
h(
|
||||
"div",
|
||||
{ className: "card org-modal-card" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-modal-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h2", { className: "org-panel-title" }, title),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-modal-close",
|
||||
onClick: () => actions.closeModal(),
|
||||
"aria-label": "Close dialog",
|
||||
},
|
||||
"x",
|
||||
),
|
||||
),
|
||||
return Modal({
|
||||
title,
|
||||
body,
|
||||
),
|
||||
);
|
||||
onClose: () => actions.closeModal(),
|
||||
});
|
||||
};
|
||||
})();
|
||||
@ -1,35 +1,90 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const store = OrgPortal.store;
|
||||
const actions = OrgPortal.actions;
|
||||
const scopeAttr = "data-ui-overview-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const overviewCardCss = `
|
||||
${scopeSelector} .org-hero-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-summary {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-meta-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-meta-item:nth-child(even) {
|
||||
background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-meta-label {
|
||||
font-size: 0.76rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-meta-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-hero-grid,
|
||||
${scopeSelector} .org-meta-row,
|
||||
${scopeSelector} .org-metric-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.OverviewCard = function OverviewCard() {
|
||||
const MetricCard = OrgPortal.componentFns.MetricCard;
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
const readiness = actions.getAssetReadiness();
|
||||
const headquarters = portalData.org.headquarters || "ArmA Verse";
|
||||
ensureScopedStyle("portal-overview-card", overviewCardCss);
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: "card org-panel org-span-12" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-panel-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("div", { className: "org-eyebrow" }, portalData.org.tag),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "org-panel-title" },
|
||||
"Organization Overview",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
return PanelCard({
|
||||
className: "org-span-12",
|
||||
eyebrow: portalData.org.tag,
|
||||
title: "Organization Overview",
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
{ className: "org-hero-grid" },
|
||||
h(
|
||||
@ -115,6 +170,6 @@
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,39 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const scopeAttr = "data-ui-simple-stat";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const simpleStatCss = `
|
||||
${scopeSelector} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-label {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-value {
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.SimpleStat = function SimpleStat(label, value) {
|
||||
ensureScopedStyle("portal-simple-stat", simpleStatCss);
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "org-simple-stat", [scopeAttr]: "" },
|
||||
h("span", { className: "org-simple-label" }, label),
|
||||
h("strong", { className: "org-simple-value" }, value),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,430 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle, createSignal } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const store = OrgPortal.store;
|
||||
const permissions = OrgPortal.permissions;
|
||||
const actions = OrgPortal.actions;
|
||||
const scopeAttr = "data-ui-treasury-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const [getTreasuryTab, setTreasuryTab] = createSignal("overview");
|
||||
const [getTreasuryMenuOpen, setTreasuryMenuOpen] = createSignal(false);
|
||||
const treasuryCardCss = `
|
||||
${scopeSelector} .org-treasury-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-btn {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border);
|
||||
background: #f8fafc;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-btn:hover {
|
||||
color: var(--primary-hover);
|
||||
border-color: rgb(148 163 184 / 0.65);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-btn svg {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.6rem);
|
||||
right: 0;
|
||||
min-width: 10.5rem;
|
||||
padding: 0.45rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
box-shadow: 0 12px 28px rgb(15 23 42 / 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-option + .org-menu-option {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-option {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
background: transparent;
|
||||
color: var(--text-main);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-option:hover {
|
||||
background: #f8fafc;
|
||||
border-color: rgb(148 163 184 / 0.35);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-option.is-active {
|
||||
background: rgb(226 232 240 / 0.7);
|
||||
color: var(--primary-hover);
|
||||
border-color: rgb(148 163 184 / 0.35);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-finance-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-finance-meta > div {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-meta-label {
|
||||
font-size: 0.76rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-action-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-action-grid button + button {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-action-grid button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-access-note {
|
||||
margin: 0 0 1rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-summary strong {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-summary span:last-child {
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-lines-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-line-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-line-row:nth-child(even) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-line-member {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-line-label {
|
||||
font-size: 0.76rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-line-empty {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-finance-meta {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-line-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.TreasuryCard = function TreasuryCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
const creditLines = store.getCreditLines();
|
||||
const allowTreasuryActions = permissions.canManageTreasury();
|
||||
const activeTab = getTreasuryTab();
|
||||
const isMenuOpen = getTreasuryMenuOpen();
|
||||
const activeCreditLabel =
|
||||
creditLines.length === 1
|
||||
? "1 active credit line"
|
||||
: `${creditLines.length} active credit lines`;
|
||||
ensureScopedStyle("portal-treasury-card", treasuryCardCss);
|
||||
|
||||
return PanelCard({
|
||||
className: "org-span-5",
|
||||
title: "Treasury",
|
||||
subtitle: "Organization funds, reputation, and member payouts.",
|
||||
headerExtras: h(
|
||||
"div",
|
||||
{ className: "org-treasury-menu" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-menu-btn",
|
||||
title: "Treasury views",
|
||||
"aria-label": "Treasury views",
|
||||
onClick: () => setTreasuryMenuOpen((open) => !open),
|
||||
},
|
||||
h(
|
||||
"svg",
|
||||
{
|
||||
viewBox: "0 0 24 24",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round",
|
||||
"aria-hidden": "true",
|
||||
},
|
||||
h("line", { x1: "4", y1: "7", x2: "20", y2: "7" }),
|
||||
h("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
||||
h("line", { x1: "4", y1: "17", x2: "20", y2: "17" }),
|
||||
),
|
||||
),
|
||||
isMenuOpen
|
||||
? h(
|
||||
"div",
|
||||
{ className: "org-menu-dropdown" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className:
|
||||
activeTab === "overview"
|
||||
? "org-menu-option is-active"
|
||||
: "org-menu-option",
|
||||
onClick: () => {
|
||||
setTreasuryTab("overview");
|
||||
setTreasuryMenuOpen(false);
|
||||
},
|
||||
},
|
||||
"Overview",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className:
|
||||
activeTab === "credit"
|
||||
? "org-menu-option is-active"
|
||||
: "org-menu-option",
|
||||
onClick: () => {
|
||||
setTreasuryTab("credit");
|
||||
setTreasuryMenuOpen(false);
|
||||
},
|
||||
},
|
||||
"Credit Lines",
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
null,
|
||||
activeTab === "credit"
|
||||
? creditLines.length > 0
|
||||
? h(
|
||||
"div",
|
||||
{ className: "org-credit-lines-list" },
|
||||
...creditLines.map((line) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-credit-line-row" },
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
"org-credit-line-member",
|
||||
},
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
className:
|
||||
"org-credit-line-label",
|
||||
},
|
||||
"Member",
|
||||
),
|
||||
h("strong", null, line.member),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
"org-credit-line-member",
|
||||
},
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
className:
|
||||
"org-credit-line-label",
|
||||
},
|
||||
"Amount",
|
||||
),
|
||||
h(
|
||||
"strong",
|
||||
null,
|
||||
actions.formatCurrency(
|
||||
line.amount,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: h(
|
||||
"div",
|
||||
{ className: "org-credit-line-empty" },
|
||||
"No active credit lines.",
|
||||
)
|
||||
: h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-finance-meta" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"span",
|
||||
{ className: "org-meta-label" },
|
||||
"Funds",
|
||||
),
|
||||
h(
|
||||
"strong",
|
||||
null,
|
||||
actions.formatCurrency(store.getFunds()),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"span",
|
||||
{ className: "org-meta-label" },
|
||||
"Reputation",
|
||||
),
|
||||
h("strong", null, `${portalData.reputation}`),
|
||||
),
|
||||
),
|
||||
allowTreasuryActions
|
||||
? h(
|
||||
"div",
|
||||
{ className: "org-action-grid" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
onClick: () =>
|
||||
actions.openModal("payroll"),
|
||||
},
|
||||
"Run Payroll",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-secondary-btn",
|
||||
onClick: () =>
|
||||
actions.openModal("transfer"),
|
||||
},
|
||||
"Send Funds",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-secondary-btn",
|
||||
onClick: () =>
|
||||
actions.openModal("credit"),
|
||||
},
|
||||
"Credit Line",
|
||||
),
|
||||
)
|
||||
: h(
|
||||
"p",
|
||||
{ className: "org-access-note" },
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-credit-summary" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "org-meta-label" },
|
||||
"Credit Line Status",
|
||||
),
|
||||
h("strong", null, activeCreditLabel),
|
||||
h(
|
||||
"span",
|
||||
null,
|
||||
creditLines.length > 0
|
||||
? "Open the Credit Lines tab to review assigned members and amounts."
|
||||
: "Assign a credit line to create the first approved member limit.",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
@ -6,55 +6,41 @@
|
||||
<title>ORBIS - Global Organization Network</title>
|
||||
<script>
|
||||
const addonRoot = "forge\\forge_client\\addons\\org\\ui\\_site\\";
|
||||
const styleFiles = [
|
||||
"base.css",
|
||||
"components\\navbar.css",
|
||||
"components\\homeView.css",
|
||||
"components\\forms.css",
|
||||
"portal\\components\\controls.css",
|
||||
"portal\\components\\layout.css",
|
||||
"portal\\components\\portalHeader.css",
|
||||
"portal\\components\\overviewCard.css",
|
||||
"portal\\components\\metricCard.css",
|
||||
"portal\\components\\simpleList.css",
|
||||
"portal\\components\\simpleStat.css",
|
||||
"portal\\components\\treasuryCard.css",
|
||||
"portal\\components\\activityCard.css",
|
||||
"portal\\components\\futureCard.css",
|
||||
"portal\\components\\dangerCard.css",
|
||||
"portal\\components\\modalLayer.css",
|
||||
];
|
||||
const styleFiles = ["base.css", "controls.css", "hero.css"];
|
||||
const scriptFiles = [
|
||||
"runtime.js",
|
||||
"logic\\registryStore.js",
|
||||
"logic\\portalStore.js",
|
||||
"logic\\portalPermissions.js",
|
||||
"logic\\portalActions.js",
|
||||
"state.js",
|
||||
"bridge.js",
|
||||
"portal\\runtime.js",
|
||||
"portal\\data.js",
|
||||
"portal\\store.js",
|
||||
"portal\\permissions.js",
|
||||
"portal\\actions.js",
|
||||
"portal\\components\\metricCard.js",
|
||||
"portal\\components\\simpleStat.js",
|
||||
"portal\\components\\portalHeader.js",
|
||||
"portal\\components\\overviewCard.js",
|
||||
"portal\\components\\fleetCard.js",
|
||||
"portal\\components\\treasuryCard.js",
|
||||
"portal\\components\\assetsCard.js",
|
||||
"portal\\components\\membersCard.js",
|
||||
"portal\\components\\activityCard.js",
|
||||
"portal\\components\\futureCard.js",
|
||||
"portal\\components\\dangerCard.js",
|
||||
"portal\\components\\modalLayer.js",
|
||||
"portal\\components\\disbandedView.js",
|
||||
"portal\\components\\footer.js",
|
||||
"portal\\components\\index.js",
|
||||
"components\\navbar.js",
|
||||
"components\\header.js",
|
||||
"components\\loginForm.js",
|
||||
"components\\createOrgForm.js",
|
||||
"components\\homeView.js",
|
||||
"components\\hero.js",
|
||||
"components\\footer.js",
|
||||
"components\\index.js",
|
||||
"components\\modal.js",
|
||||
"components\\panelCard.js",
|
||||
"components\\portal\\metricCard.js",
|
||||
"components\\portal\\simpleStat.js",
|
||||
"components\\portal\\overviewCard.js",
|
||||
"components\\portal\\fleetCard.js",
|
||||
"components\\portal\\treasuryCard.js",
|
||||
"components\\portal\\assetsCard.js",
|
||||
"components\\portal\\membersCard.js",
|
||||
"components\\portal\\activityCard.js",
|
||||
"components\\portal\\futureCard.js",
|
||||
"components\\portal\\dangerCard.js",
|
||||
"components\\portal\\modalLayer.js",
|
||||
"views\\DisbandedView.js",
|
||||
"views\\PortalView.js",
|
||||
"views\\RegistrationView.js",
|
||||
"views\\HomeView.js",
|
||||
"components\\AppShell.js",
|
||||
"bootstrap.js",
|
||||
];
|
||||
|
||||
|
||||
318
arma/client/addons/org/ui/_site/logic/portalActions.js
Normal file
318
arma/client/addons/org/ui/_site/logic/portalActions.js
Normal file
@ -0,0 +1,318 @@
|
||||
(function () {
|
||||
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
|
||||
|
||||
SharedLogic.createPortalActions = function createPortalActions({
|
||||
portalData,
|
||||
store,
|
||||
permissions,
|
||||
registryStore,
|
||||
}) {
|
||||
class OrgPortalActions {
|
||||
constructor() {
|
||||
this.treasuryNoticeTimer = null;
|
||||
}
|
||||
|
||||
formatCurrency(value) {
|
||||
return "$" + value.toLocaleString();
|
||||
}
|
||||
|
||||
formatVehicleType(type) {
|
||||
if (!type) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return type.charAt(0).toUpperCase() + type.slice(1);
|
||||
}
|
||||
|
||||
formatAssetType(type) {
|
||||
if (!type) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return type.charAt(0).toUpperCase() + type.slice(1);
|
||||
}
|
||||
|
||||
formatDisplayName(value) {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return String(value)
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map((part) => {
|
||||
if (!part) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return (
|
||||
part.charAt(0).toUpperCase() +
|
||||
part.slice(1).toLowerCase()
|
||||
);
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
getAssetReadiness() {
|
||||
if (portalData.fleet.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const total = portalData.fleet.reduce(
|
||||
(sum, unit) => sum + (100 - parseInt(unit.damage, 10)),
|
||||
0,
|
||||
);
|
||||
return Math.round(total / portalData.fleet.length);
|
||||
}
|
||||
|
||||
showTreasuryNotice(type, text) {
|
||||
store.setTreasuryNotice({ type, text });
|
||||
|
||||
if (this.treasuryNoticeTimer) {
|
||||
clearTimeout(this.treasuryNoticeTimer);
|
||||
}
|
||||
|
||||
this.treasuryNoticeTimer = setTimeout(() => {
|
||||
store.setTreasuryNotice({ type: "", text: "" });
|
||||
this.treasuryNoticeTimer = null;
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
parseAmount(value) {
|
||||
const amount = Number(value);
|
||||
return Number.isFinite(amount) ? Math.round(amount) : 0;
|
||||
}
|
||||
|
||||
getInputValue(id) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.value : "";
|
||||
}
|
||||
|
||||
isOwnerMember(memberName) {
|
||||
return (
|
||||
String(memberName || "")
|
||||
.trim()
|
||||
.toLowerCase() ===
|
||||
String(portalData.org.owner || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
closePortal() {
|
||||
if (
|
||||
typeof A3API !== "undefined" &&
|
||||
typeof A3API.SendAlert === "function"
|
||||
) {
|
||||
A3API.SendAlert(
|
||||
JSON.stringify({
|
||||
event: "org::close",
|
||||
data: {},
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (registryStore) {
|
||||
registryStore.setView("home");
|
||||
}
|
||||
}
|
||||
|
||||
openModal(type) {
|
||||
if (
|
||||
(type === "payroll" ||
|
||||
type === "transfer" ||
|
||||
type === "credit") &&
|
||||
!permissions.canManageTreasury()
|
||||
) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "disband" && !permissions.canDisbandOrg()) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.setModal({ type });
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
store.setModal(null);
|
||||
}
|
||||
|
||||
removeMember(memberName) {
|
||||
if (!permissions.canManageMembers()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isOwnerMember(memberName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setMembers((currentMembers) =>
|
||||
currentMembers.filter(
|
||||
(member) => member.name !== memberName,
|
||||
),
|
||||
);
|
||||
store.setCreditLines((currentLines) =>
|
||||
currentLines.filter((line) => line.member !== memberName),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
disbandOrganization() {
|
||||
if (!permissions.canDisbandOrg()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setOrgDisbanded(true);
|
||||
this.closeModal();
|
||||
return true;
|
||||
}
|
||||
|
||||
runPayroll(amountPerMember) {
|
||||
if (!permissions.canManageTreasury()) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const members = store.getMembers();
|
||||
const funds = store.getFunds();
|
||||
|
||||
if (members.length === 0) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"No members available for payroll.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amountPerMember <= 0) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Enter a valid payroll amount.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const total = amountPerMember * members.length;
|
||||
if (total > funds) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Insufficient org funds for payroll.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setFunds(funds - total);
|
||||
this.showTreasuryNotice(
|
||||
"success",
|
||||
`Payroll sent to ${members.length} members for ${this.formatCurrency(total)}.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
sendFundsToMember(memberName, amount) {
|
||||
if (!permissions.canManageTreasury()) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const funds = store.getFunds();
|
||||
|
||||
if (!memberName) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Select a member to receive funds.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Enter a valid transfer amount.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount > funds) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Insufficient org funds for this transfer.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setFunds(funds - amount);
|
||||
this.showTreasuryNotice(
|
||||
"success",
|
||||
`${this.formatCurrency(amount)} sent to ${memberName}.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
grantCreditLine(memberName, amount) {
|
||||
if (!permissions.canManageTreasury()) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!memberName) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Select a member for the credit line.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Enter a valid credit line amount.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setCreditLines((currentLines) => {
|
||||
const existingIndex = currentLines.findIndex(
|
||||
(line) => line.member === memberName,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
return [
|
||||
...currentLines,
|
||||
{ member: memberName, amount },
|
||||
];
|
||||
}
|
||||
|
||||
const updatedLines = [...currentLines];
|
||||
updatedLines[existingIndex] = {
|
||||
member: memberName,
|
||||
amount,
|
||||
};
|
||||
return updatedLines;
|
||||
});
|
||||
|
||||
this.showTreasuryNotice(
|
||||
"success",
|
||||
`Credit line of ${this.formatCurrency(amount)} assigned to ${memberName}.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return new OrgPortalActions();
|
||||
};
|
||||
})();
|
||||
75
arma/client/addons/org/ui/_site/logic/portalPermissions.js
Normal file
75
arma/client/addons/org/ui/_site/logic/portalPermissions.js
Normal file
@ -0,0 +1,75 @@
|
||||
(function () {
|
||||
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
|
||||
|
||||
SharedLogic.createPortalPermissions = function createPortalPermissions({
|
||||
portalData,
|
||||
session,
|
||||
}) {
|
||||
class OrgPortalPermissions {
|
||||
getNormalizedRole() {
|
||||
return String(session.role || "")
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
isDefaultOrg() {
|
||||
return (
|
||||
portalData.org.isDefault === true ||
|
||||
String(portalData.org.tag || "")
|
||||
.trim()
|
||||
.toUpperCase() === "DEFAULT"
|
||||
);
|
||||
}
|
||||
|
||||
isOrgOwner() {
|
||||
const ownerUid = String(
|
||||
portalData.org.ownerUid || portalData.org.owner || "",
|
||||
)
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const actorUid = String(session.actorUid || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
if (ownerUid && actorUid) {
|
||||
return actorUid === ownerUid;
|
||||
}
|
||||
|
||||
return (
|
||||
String(session.actorName || "")
|
||||
.trim()
|
||||
.toLowerCase() ===
|
||||
String(portalData.org.owner || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
isSessionCeo() {
|
||||
return session.ceo === true;
|
||||
}
|
||||
|
||||
isOrgLeaderOrCeo() {
|
||||
return (
|
||||
this.isOrgOwner() ||
|
||||
this.getNormalizedRole() === "LEADER" ||
|
||||
(this.isDefaultOrg() && this.isSessionCeo())
|
||||
);
|
||||
}
|
||||
|
||||
canManageMembers() {
|
||||
return this.isOrgLeaderOrCeo();
|
||||
}
|
||||
|
||||
canManageTreasury() {
|
||||
return this.isOrgLeaderOrCeo();
|
||||
}
|
||||
|
||||
canDisbandOrg() {
|
||||
return this.isOrgLeaderOrCeo();
|
||||
}
|
||||
}
|
||||
|
||||
return new OrgPortalPermissions();
|
||||
};
|
||||
})();
|
||||
38
arma/client/addons/org/ui/_site/logic/portalStore.js
Normal file
38
arma/client/addons/org/ui/_site/logic/portalStore.js
Normal file
@ -0,0 +1,38 @@
|
||||
(function () {
|
||||
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
|
||||
|
||||
SharedLogic.createPortalStore = function createPortalStore({
|
||||
createSignal,
|
||||
portalData,
|
||||
}) {
|
||||
class OrgPortalStore {
|
||||
constructor() {
|
||||
[this.getFunds, this.setFunds] = createSignal(portalData.funds);
|
||||
[this.getMembers, this.setMembers] = createSignal([
|
||||
...portalData.members,
|
||||
]);
|
||||
[this.getCreditLines, this.setCreditLines] = createSignal([]);
|
||||
[this.getTreasuryNotice, this.setTreasuryNotice] = createSignal(
|
||||
{
|
||||
type: "",
|
||||
text: "",
|
||||
},
|
||||
);
|
||||
[this.getModal, this.setModal] = createSignal(null);
|
||||
[this.getOrgDisbanded, this.setOrgDisbanded] =
|
||||
createSignal(false);
|
||||
}
|
||||
|
||||
hydrateFromPayload(payload) {
|
||||
this.setFunds(payload.portalData.funds || 0);
|
||||
this.setMembers([...(payload.portalData.members || [])]);
|
||||
this.setCreditLines([]);
|
||||
this.setTreasuryNotice({ type: "", text: "" });
|
||||
this.setModal(null);
|
||||
this.setOrgDisbanded(false);
|
||||
}
|
||||
}
|
||||
|
||||
return new OrgPortalStore();
|
||||
};
|
||||
})();
|
||||
69
arma/client/addons/org/ui/_site/logic/registryStore.js
Normal file
69
arma/client/addons/org/ui/_site/logic/registryStore.js
Normal file
@ -0,0 +1,69 @@
|
||||
(function () {
|
||||
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
|
||||
|
||||
SharedLogic.createRegistryStore = function createRegistryStore({
|
||||
createSignal,
|
||||
onHydratePortal,
|
||||
}) {
|
||||
class RegistryStore {
|
||||
constructor() {
|
||||
[this.getView, this.setView] = createSignal("home");
|
||||
[this.getIsAuthenticating, this.setIsAuthenticating] =
|
||||
createSignal(false);
|
||||
[this.getLoginError, this.setLoginError] = createSignal("");
|
||||
[this.getIsCreating, this.setIsCreating] = createSignal(false);
|
||||
[this.getCreateError, this.setCreateError] = createSignal("");
|
||||
}
|
||||
|
||||
startLogin() {
|
||||
this.setLoginError("");
|
||||
this.setIsAuthenticating(true);
|
||||
}
|
||||
|
||||
startCreate() {
|
||||
this.setCreateError("");
|
||||
this.setIsCreating(true);
|
||||
}
|
||||
|
||||
failLogin(message) {
|
||||
this.setIsAuthenticating(false);
|
||||
this.setLoginError(message || "Authentication failed.");
|
||||
}
|
||||
|
||||
failCreate(message) {
|
||||
this.setIsCreating(false);
|
||||
this.setCreateError(
|
||||
message || "Organization registration failed.",
|
||||
);
|
||||
}
|
||||
|
||||
hydratePortal(payload) {
|
||||
return Boolean(onHydratePortal && onHydratePortal(payload));
|
||||
}
|
||||
|
||||
completeLogin(payload) {
|
||||
if (!this.hydratePortal(payload)) {
|
||||
this.failLogin("Login response was missing portal data.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setLoginError("");
|
||||
this.setIsAuthenticating(false);
|
||||
}
|
||||
|
||||
completeCreate(payload) {
|
||||
if (!this.hydratePortal(payload)) {
|
||||
this.failCreate(
|
||||
"Organization registration response was missing portal data.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setCreateError("");
|
||||
this.setIsCreating(false);
|
||||
}
|
||||
}
|
||||
|
||||
return new RegistryStore();
|
||||
};
|
||||
})();
|
||||
@ -4,289 +4,12 @@
|
||||
const store = OrgPortal.store;
|
||||
const permissions = OrgPortal.permissions;
|
||||
const registryStore = window.RegistryApp.store;
|
||||
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
|
||||
|
||||
class OrgPortalActions {
|
||||
constructor() {
|
||||
this.treasuryNoticeTimer = null;
|
||||
}
|
||||
|
||||
formatCurrency(value) {
|
||||
return "$" + value.toLocaleString();
|
||||
}
|
||||
|
||||
formatVehicleType(type) {
|
||||
if (!type) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return type.charAt(0).toUpperCase() + type.slice(1);
|
||||
}
|
||||
|
||||
formatAssetType(type) {
|
||||
if (!type) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return type.charAt(0).toUpperCase() + type.slice(1);
|
||||
}
|
||||
|
||||
formatDisplayName(value) {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return String(value)
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map((part) => {
|
||||
if (!part) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return (
|
||||
part.charAt(0).toUpperCase() +
|
||||
part.slice(1).toLowerCase()
|
||||
);
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
getAssetReadiness() {
|
||||
if (portalData.fleet.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const total = portalData.fleet.reduce(
|
||||
(sum, unit) => sum + (100 - parseInt(unit.damage, 10)),
|
||||
0,
|
||||
);
|
||||
return Math.round(total / portalData.fleet.length);
|
||||
}
|
||||
|
||||
showTreasuryNotice(type, text) {
|
||||
store.setTreasuryNotice({ type, text });
|
||||
|
||||
if (this.treasuryNoticeTimer) {
|
||||
clearTimeout(this.treasuryNoticeTimer);
|
||||
}
|
||||
|
||||
this.treasuryNoticeTimer = setTimeout(() => {
|
||||
store.setTreasuryNotice({ type: "", text: "" });
|
||||
this.treasuryNoticeTimer = null;
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
parseAmount(value) {
|
||||
const amount = Number(value);
|
||||
return Number.isFinite(amount) ? Math.round(amount) : 0;
|
||||
}
|
||||
|
||||
getInputValue(id) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.value : "";
|
||||
}
|
||||
|
||||
closePortal() {
|
||||
if (
|
||||
typeof A3API !== "undefined" &&
|
||||
typeof A3API.SendAlert === "function"
|
||||
) {
|
||||
A3API.SendAlert(
|
||||
JSON.stringify({
|
||||
event: "org::close",
|
||||
data: {},
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (registryStore) {
|
||||
registryStore.setView("home");
|
||||
}
|
||||
}
|
||||
|
||||
openModal(type) {
|
||||
if (
|
||||
(type === "payroll" ||
|
||||
type === "transfer" ||
|
||||
type === "credit") &&
|
||||
!permissions.canManageTreasury()
|
||||
) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "disband" && !permissions.canDisbandOrg()) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.setModal({ type });
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
store.setModal(null);
|
||||
}
|
||||
|
||||
removeMember(memberName) {
|
||||
if (!permissions.canManageMembers()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setMembers((currentMembers) =>
|
||||
currentMembers.filter((member) => member.name !== memberName),
|
||||
);
|
||||
store.setCreditLines((currentLines) =>
|
||||
currentLines.filter((line) => line.member !== memberName),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
disbandOrganization() {
|
||||
if (!permissions.canDisbandOrg()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setOrgDisbanded(true);
|
||||
this.closeModal();
|
||||
return true;
|
||||
}
|
||||
|
||||
runPayroll(amountPerMember) {
|
||||
if (!permissions.canManageTreasury()) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const members = store.getMembers();
|
||||
const funds = store.getFunds();
|
||||
|
||||
if (members.length === 0) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"No members available for payroll.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amountPerMember <= 0) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Enter a valid payroll amount.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const total = amountPerMember * members.length;
|
||||
if (total > funds) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Insufficient org funds for payroll.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setFunds(funds - total);
|
||||
this.showTreasuryNotice(
|
||||
"success",
|
||||
`Payroll sent to ${members.length} members for ${this.formatCurrency(total)}.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
sendFundsToMember(memberName, amount) {
|
||||
if (!permissions.canManageTreasury()) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const funds = store.getFunds();
|
||||
|
||||
if (!memberName) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Select a member to receive funds.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Enter a valid transfer amount.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount > funds) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Insufficient org funds for this transfer.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setFunds(funds - amount);
|
||||
this.showTreasuryNotice(
|
||||
"success",
|
||||
`${this.formatCurrency(amount)} sent to ${memberName}.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
grantCreditLine(memberName, amount) {
|
||||
if (!permissions.canManageTreasury()) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!memberName) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Select a member for the credit line.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Enter a valid credit line amount.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setCreditLines((currentLines) => {
|
||||
const existingIndex = currentLines.findIndex(
|
||||
(line) => line.member === memberName,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
return [...currentLines, { member: memberName, amount }];
|
||||
}
|
||||
|
||||
const updatedLines = [...currentLines];
|
||||
updatedLines[existingIndex] = { member: memberName, amount };
|
||||
return updatedLines;
|
||||
OrgPortal.actions = SharedLogic.createPortalActions({
|
||||
portalData,
|
||||
store,
|
||||
permissions,
|
||||
registryStore,
|
||||
});
|
||||
|
||||
this.showTreasuryNotice(
|
||||
"success",
|
||||
`Credit line of ${this.formatCurrency(amount)} assigned to ${memberName}.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
OrgPortal.actions = new OrgPortalActions();
|
||||
})();
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
.org-activity-row {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid #94a3b8;
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(248 250 252) 0%,
|
||||
rgb(241 245 249) 100%
|
||||
);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
border-left-color: #64748b;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--text-main);
|
||||
}
|
||||
}
|
||||
|
||||
.org-activity-time {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.35rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.ActivityCard = function ActivityCard() {
|
||||
return h(
|
||||
"section",
|
||||
{ className: "card org-panel org-scroll-panel org-span-6" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-panel-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h2", { className: "org-panel-title" }, "Command Feed"),
|
||||
h(
|
||||
"p",
|
||||
{ className: "org-panel-subtitle" },
|
||||
"Recent organization-level actions and updates.",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-activity-list" },
|
||||
...portalData.activity.map((item) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-activity-row" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "org-activity-time" },
|
||||
item.time,
|
||||
),
|
||||
h("p", null, item.text),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,55 +0,0 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const actions = OrgPortal.actions;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.AssetsCard = function AssetsCard() {
|
||||
const SimpleStat = OrgPortal.componentFns.SimpleStat;
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: "card org-panel org-scroll-panel org-span-7" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-panel-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h2", { className: "org-panel-title" }, "Assets"),
|
||||
h(
|
||||
"p",
|
||||
{ className: "org-panel-subtitle" },
|
||||
"Inventory supplies and equipment with quantity totals.",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-simple-list" },
|
||||
...portalData.assets.map((asset) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-simple-row" },
|
||||
h(
|
||||
"strong",
|
||||
{ className: "org-simple-name" },
|
||||
asset.name,
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-simple-meta" },
|
||||
SimpleStat(
|
||||
"Type",
|
||||
actions.formatAssetType(asset.type),
|
||||
),
|
||||
SimpleStat("Quantity", asset.quantity),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,22 +0,0 @@
|
||||
.org-danger-panel {
|
||||
border-color: #fecaca;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%);
|
||||
}
|
||||
|
||||
.org-danger-copy {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
strong,
|
||||
p {
|
||||
display: block;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.4rem 0 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.org-empty-state {
|
||||
text-align: left;
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const permissions = OrgPortal.permissions;
|
||||
const actions = OrgPortal.actions;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.DangerCard = function DangerCard() {
|
||||
if (!permissions.canDisbandOrg()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: "card org-panel org-span-12 org-danger-panel" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-panel-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"h2",
|
||||
{ className: "org-panel-title" },
|
||||
"Organization Controls",
|
||||
),
|
||||
h(
|
||||
"p",
|
||||
{ className: "org-panel-subtitle" },
|
||||
"Leader-only actions for membership and permanent organization removal.",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-danger-copy" },
|
||||
h("strong", null, "Disband organization"),
|
||||
h(
|
||||
"p",
|
||||
null,
|
||||
"This removes the organization and revokes access to the portal for all members.",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-danger-btn",
|
||||
onClick: () => actions.openModal("disband"),
|
||||
},
|
||||
"Disband Organization",
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,47 +0,0 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const registryStore = window.RegistryApp.store;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.DisbandedView = function DisbandedView() {
|
||||
return h(
|
||||
"section",
|
||||
{ className: "card org-panel org-span-12 org-empty-state" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-panel-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-eyebrow" },
|
||||
"Organization Removed",
|
||||
),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "org-panel-title" },
|
||||
portalData.org.name,
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"p",
|
||||
{ className: "org-summary" },
|
||||
"This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview.",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-secondary-btn",
|
||||
onClick: () => registryStore.setView("home"),
|
||||
},
|
||||
"Return to Registry",
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,56 +0,0 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const actions = OrgPortal.actions;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.FleetCard = function FleetCard() {
|
||||
const SimpleStat = OrgPortal.componentFns.SimpleStat;
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: "card org-panel org-scroll-panel org-span-7" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-panel-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h2", { className: "org-panel-title" }, "Fleet"),
|
||||
h(
|
||||
"p",
|
||||
{ className: "org-panel-subtitle" },
|
||||
"Individual vehicles with type, status, and overall damage.",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-simple-list" },
|
||||
...portalData.fleet.map((unit) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-simple-row" },
|
||||
h(
|
||||
"strong",
|
||||
{ className: "org-simple-name" },
|
||||
unit.name,
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-simple-meta" },
|
||||
SimpleStat(
|
||||
"Type",
|
||||
actions.formatVehicleType(unit.type),
|
||||
),
|
||||
SimpleStat("Status", unit.status),
|
||||
SimpleStat("Damage", unit.damage),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,43 +0,0 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.Footer = function Footer() {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "footer" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "wrapper" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h3", null, "Organization Controls"),
|
||||
h(
|
||||
"ul",
|
||||
{ style: { listStyleType: "none", padding: 0 } },
|
||||
h("li", null, "Roster Management"),
|
||||
h("li", null, "Fleet Assignment"),
|
||||
h("li", null, "Treasury Permissions"),
|
||||
h("li", null, "Asset Registry"),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h3", null, "Planned Extensions"),
|
||||
h(
|
||||
"ul",
|
||||
{ style: { listStyleType: "none", padding: 0 } },
|
||||
h("li", null, "Contracts Board"),
|
||||
h("li", null, "Diplomacy Layer"),
|
||||
h("li", null, "Procurement Queue"),
|
||||
h("li", null, "Reputation History"),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,74 +0,0 @@
|
||||
.org-roadmap-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 0.35rem;
|
||||
}
|
||||
|
||||
.org-roadmap-card {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.7rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
|
||||
&:nth-child(4n + 2),
|
||||
&:nth-child(4n + 3) {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(248 250 252) 0%,
|
||||
rgb(241 245 249) 100%
|
||||
);
|
||||
border-color: rgb(100 116 139 / 0.4);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--text-main);
|
||||
}
|
||||
}
|
||||
|
||||
.org-list-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
background: #e2e8f0;
|
||||
color: var(--primary-hover);
|
||||
|
||||
.org-roadmap-card:nth-child(4n + 2) &,
|
||||
.org-roadmap-card:nth-child(4n + 3) & {
|
||||
background: #cbd5e1;
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.org-roadmap-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.org-roadmap-card {
|
||||
&:nth-child(4n + 3) {
|
||||
background: #f8fafc;
|
||||
border-color: var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
.org-list-tag {
|
||||
.org-roadmap-card:nth-child(4n + 3) & {
|
||||
background: #e2e8f0;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.FutureCard = function FutureCard() {
|
||||
return h(
|
||||
"section",
|
||||
{ className: "card org-panel org-scroll-panel org-span-6" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-panel-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"h2",
|
||||
{ className: "org-panel-title" },
|
||||
"Expansion Slots",
|
||||
),
|
||||
h(
|
||||
"p",
|
||||
{ className: "org-panel-subtitle" },
|
||||
"Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-roadmap-grid" },
|
||||
...portalData.roadmap.map((item) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-roadmap-card" },
|
||||
h("span", { className: "org-list-tag" }, item.status),
|
||||
h("strong", null, item.name),
|
||||
h("p", null, item.detail),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,65 +0,0 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const store = OrgPortal.store;
|
||||
|
||||
OrgPortal.components = OrgPortal.components || {};
|
||||
|
||||
OrgPortal.components.App = function App() {
|
||||
const PortalHeader = OrgPortal.componentFns.PortalHeader;
|
||||
const OverviewCard = OrgPortal.componentFns.OverviewCard;
|
||||
const FleetCard = OrgPortal.componentFns.FleetCard;
|
||||
const TreasuryCard = OrgPortal.componentFns.TreasuryCard;
|
||||
const MembersCard = OrgPortal.componentFns.MembersCard;
|
||||
const AssetsCard = OrgPortal.componentFns.AssetsCard;
|
||||
const ActivityCard = OrgPortal.componentFns.ActivityCard;
|
||||
const FutureCard = OrgPortal.componentFns.FutureCard;
|
||||
const DangerCard = OrgPortal.componentFns.DangerCard;
|
||||
const ModalLayer = OrgPortal.componentFns.ModalLayer;
|
||||
const DisbandedView = OrgPortal.componentFns.DisbandedView;
|
||||
const Footer = OrgPortal.componentFns.Footer;
|
||||
|
||||
if (store.getOrgDisbanded()) {
|
||||
return h(
|
||||
"main",
|
||||
null,
|
||||
h(
|
||||
"div",
|
||||
{ className: "container" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-dashboard-grid" },
|
||||
PortalHeader(),
|
||||
DisbandedView(),
|
||||
),
|
||||
),
|
||||
ModalLayer(),
|
||||
Footer(),
|
||||
);
|
||||
}
|
||||
|
||||
return h(
|
||||
"main",
|
||||
null,
|
||||
h(
|
||||
"div",
|
||||
{ className: "container" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-dashboard-grid" },
|
||||
PortalHeader(),
|
||||
OverviewCard(),
|
||||
FleetCard(),
|
||||
TreasuryCard(),
|
||||
MembersCard(),
|
||||
AssetsCard(),
|
||||
ActivityCard(),
|
||||
FutureCard(),
|
||||
DangerCard(),
|
||||
),
|
||||
),
|
||||
ModalLayer(),
|
||||
Footer(),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,81 +0,0 @@
|
||||
.org-dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.org-panel {
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.org-scroll-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 31rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.org-span-12 {
|
||||
grid-column: span 12;
|
||||
}
|
||||
|
||||
.org-span-7 {
|
||||
grid-column: span 7;
|
||||
}
|
||||
|
||||
.org-span-6 {
|
||||
grid-column: span 6;
|
||||
}
|
||||
|
||||
.org-span-5 {
|
||||
grid-column: span 5;
|
||||
}
|
||||
|
||||
.org-panel-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
&.org-panel-head-stack {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.org-eyebrow {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.org-panel-title {
|
||||
margin: 0;
|
||||
color: var(--primary-hover);
|
||||
font-size: 1.45rem;
|
||||
}
|
||||
|
||||
.org-panel-subtitle {
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.org-span-12,
|
||||
.org-span-7,
|
||||
.org-span-6,
|
||||
.org-span-5 {
|
||||
grid-column: span 12;
|
||||
}
|
||||
|
||||
.org-panel-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
.org-metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.org-metric-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
padding: 1rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||
|
||||
&:nth-child(4n + 2),
|
||||
&:nth-child(4n + 3) {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(248 250 252) 0%,
|
||||
rgb(226 232 240) 100%
|
||||
);
|
||||
border-color: rgb(100 116 139 / 0.35);
|
||||
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.org-metric-label {
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.org-metric-value {
|
||||
font-size: 1.8rem;
|
||||
color: var(--primary-hover);
|
||||
line-height: 1.1;
|
||||
|
||||
.org-metric-card:nth-child(4n + 2) &,
|
||||
.org-metric-card:nth-child(4n + 3) & {
|
||||
color: #334155;
|
||||
}
|
||||
}
|
||||
|
||||
.org-metric-note {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.org-metric-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.org-metric-card {
|
||||
&:nth-child(4n + 3) {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||
border-color: var(--border);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.org-metric-value {
|
||||
.org-metric-card:nth-child(4n + 3) & {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.MetricCard = function MetricCard(
|
||||
label,
|
||||
value,
|
||||
note,
|
||||
) {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "org-metric-card" },
|
||||
h("span", { className: "org-metric-label" }, label),
|
||||
h("strong", { className: "org-metric-value" }, value),
|
||||
h("span", { className: "org-metric-note" }, note),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,131 +0,0 @@
|
||||
.org-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgb(15 23 42 / 0.38);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.org-modal-card {
|
||||
width: min(100%, 30rem);
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.org-modal-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.org-modal-close {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 0;
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-main);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
color: var(--text-main);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.org-modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
box-shadow 0.2s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #f1f5f9;
|
||||
color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.org-modal-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
button + button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.org-danger-confirm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: var(--radius);
|
||||
background: #fff1f2;
|
||||
align-items: flex-start;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--text-main);
|
||||
}
|
||||
}
|
||||
|
||||
.org-danger-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
|
||||
button + button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.org-modal-head,
|
||||
.org-danger-confirm {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
.org-hero-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.org-summary {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.org-meta-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.org-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(241 245 249) 0%,
|
||||
rgb(226 232 240) 100%
|
||||
);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
.org-meta-label {
|
||||
font-size: 0.76rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.org-meta-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.org-hero-grid,
|
||||
.org-meta-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const { portalData, session } = OrgPortal.data;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.PortalHeader = function PortalHeader() {
|
||||
return h(
|
||||
"section",
|
||||
{ className: "card org-panel org-span-12 org-page-header" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-page-heading" },
|
||||
h("span", { className: "org-page-kicker" }, portalData.org.tag),
|
||||
h("h1", { className: "org-page-title" }, portalData.org.name),
|
||||
h(
|
||||
"p",
|
||||
{ className: "org-page-subtitle" },
|
||||
"Player organization command portal",
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "org-page-meta" },
|
||||
`${session.actorName} - ${session.role}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,67 +0,0 @@
|
||||
.org-simple-list,
|
||||
.org-name-list,
|
||||
.org-activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 0.85rem;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 0.35rem;
|
||||
}
|
||||
|
||||
.org-simple-list,
|
||||
.org-name-list,
|
||||
.org-activity-list,
|
||||
.org-roadmap-grid {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #94a3b8 #e2e8f0;
|
||||
}
|
||||
|
||||
.org-simple-row,
|
||||
.org-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(248 250 252) 0%,
|
||||
rgb(241 245 249) 100%
|
||||
);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
.org-simple-name {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.org-simple-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.org-name-row {
|
||||
justify-content: flex-start;
|
||||
|
||||
button {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.org-simple-row,
|
||||
.org-name-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
.org-simple-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.org-simple-label {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.org-simple-value {
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.SimpleStat = function SimpleStat(label, value) {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "org-simple-stat" },
|
||||
h("span", { className: "org-simple-label" }, label),
|
||||
h("strong", { className: "org-simple-value" }, value),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,99 +0,0 @@
|
||||
.org-finance-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
> div {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.org-action-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
button + button {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.org-treasury-notice {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.92rem;
|
||||
|
||||
&.is-success {
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #bbf7d0;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
&.is-error {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
}
|
||||
|
||||
.org-credit-lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.org-access-note {
|
||||
margin: 0 0 1rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.org-credit-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.org-credit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(248 250 252) 0%,
|
||||
rgb(241 245 249) 100%
|
||||
);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.org-finance-meta {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.org-credit-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
@ -1,126 +0,0 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const store = OrgPortal.store;
|
||||
const permissions = OrgPortal.permissions;
|
||||
const actions = OrgPortal.actions;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.TreasuryCard = function TreasuryCard() {
|
||||
const notice = store.getTreasuryNotice();
|
||||
const creditLines = store.getCreditLines();
|
||||
const allowTreasuryActions = permissions.canManageTreasury();
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: "card org-panel org-span-5" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-panel-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h2", { className: "org-panel-title" }, "Treasury"),
|
||||
h(
|
||||
"p",
|
||||
{ className: "org-panel-subtitle" },
|
||||
"Organization funds, reputation, and member payouts.",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-finance-meta" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "org-meta-label" }, "Funds"),
|
||||
h("strong", null, actions.formatCurrency(store.getFunds())),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "org-meta-label" }, "Reputation"),
|
||||
h("strong", null, `${portalData.reputation}`),
|
||||
),
|
||||
),
|
||||
allowTreasuryActions
|
||||
? h(
|
||||
"div",
|
||||
{ className: "org-action-grid" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
onClick: () => actions.openModal("payroll"),
|
||||
},
|
||||
"Run Payroll",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-secondary-btn",
|
||||
onClick: () => actions.openModal("transfer"),
|
||||
},
|
||||
"Send Funds",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-secondary-btn",
|
||||
onClick: () => actions.openModal("credit"),
|
||||
},
|
||||
"Credit Line",
|
||||
),
|
||||
)
|
||||
: h(
|
||||
"p",
|
||||
{ className: "org-access-note" },
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
),
|
||||
notice.text
|
||||
? h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
notice.type === "error"
|
||||
? "org-treasury-notice is-error"
|
||||
: "org-treasury-notice is-success",
|
||||
},
|
||||
notice.text,
|
||||
)
|
||||
: null,
|
||||
creditLines.length > 0
|
||||
? h(
|
||||
"div",
|
||||
{ className: "org-credit-lines" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "org-meta-label" },
|
||||
"Active Credit Lines",
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-credit-list" },
|
||||
...creditLines.map((line) =>
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-credit-row" },
|
||||
h("span", null, line.member),
|
||||
h(
|
||||
"strong",
|
||||
null,
|
||||
actions.formatCurrency(line.amount),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,71 +1,10 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { portalData, session } = OrgPortal.data;
|
||||
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
|
||||
|
||||
class OrgPortalPermissions {
|
||||
getNormalizedRole() {
|
||||
return String(session.role || "")
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
isDefaultOrg() {
|
||||
return (
|
||||
portalData.org.isDefault === true ||
|
||||
String(portalData.org.tag || "")
|
||||
.trim()
|
||||
.toUpperCase() === "DEFAULT"
|
||||
);
|
||||
}
|
||||
|
||||
isOrgOwner() {
|
||||
const ownerUid = String(
|
||||
portalData.org.ownerUid || portalData.org.owner || "",
|
||||
)
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const actorUid = String(session.actorUid || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
if (ownerUid && actorUid) {
|
||||
return actorUid === ownerUid;
|
||||
}
|
||||
|
||||
return (
|
||||
String(session.actorName || "")
|
||||
.trim()
|
||||
.toLowerCase() ===
|
||||
String(portalData.org.owner || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
isSessionCeo() {
|
||||
return session.ceo === true;
|
||||
}
|
||||
|
||||
isOrgLeaderOrCeo() {
|
||||
return (
|
||||
this.isOrgOwner() ||
|
||||
this.getNormalizedRole() === "LEADER" ||
|
||||
(this.isDefaultOrg() && this.isSessionCeo())
|
||||
);
|
||||
}
|
||||
|
||||
canManageMembers() {
|
||||
return this.isOrgLeaderOrCeo();
|
||||
}
|
||||
|
||||
canManageTreasury() {
|
||||
return this.isOrgLeaderOrCeo();
|
||||
}
|
||||
|
||||
canDisbandOrg() {
|
||||
return this.isOrgLeaderOrCeo();
|
||||
}
|
||||
}
|
||||
|
||||
OrgPortal.permissions = new OrgPortalPermissions();
|
||||
OrgPortal.permissions = SharedLogic.createPortalPermissions({
|
||||
portalData,
|
||||
session,
|
||||
});
|
||||
})();
|
||||
|
||||
@ -1,98 +0,0 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const SVG_TAGS = new Set([
|
||||
"svg",
|
||||
"path",
|
||||
"circle",
|
||||
"rect",
|
||||
"line",
|
||||
"polyline",
|
||||
"polygon",
|
||||
"g",
|
||||
"defs",
|
||||
"use",
|
||||
"text",
|
||||
"tspan",
|
||||
"clipPath",
|
||||
"mask",
|
||||
]);
|
||||
|
||||
function h(tag, props = {}, ...children) {
|
||||
const isSvg = SVG_TAGS.has(tag);
|
||||
const el = isSvg
|
||||
? document.createElementNS(SVG_NS, tag)
|
||||
: document.createElement(tag);
|
||||
|
||||
if (props) {
|
||||
Object.entries(props).forEach(([key, value]) => {
|
||||
if (key.startsWith("on") && typeof value === "function") {
|
||||
el.addEventListener(key.substring(2).toLowerCase(), value);
|
||||
} else if (key === "className") {
|
||||
if (isSvg) {
|
||||
el.setAttribute("class", value);
|
||||
} else {
|
||||
el.className = value;
|
||||
}
|
||||
} else if (key === "style" && typeof value === "object") {
|
||||
Object.assign(el.style, value);
|
||||
} else if (typeof value === "boolean") {
|
||||
if (value) {
|
||||
el.setAttribute(key, "");
|
||||
} else {
|
||||
el.removeAttribute(key);
|
||||
}
|
||||
} else if (value === null || value === undefined) {
|
||||
el.removeAttribute(key);
|
||||
} else {
|
||||
el.setAttribute(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
children.forEach((child) => {
|
||||
if (typeof child === "string" || typeof child === "number") {
|
||||
el.appendChild(document.createTextNode(child));
|
||||
} else if (child instanceof Node) {
|
||||
el.appendChild(child);
|
||||
} else if (Array.isArray(child)) {
|
||||
child.forEach((c) => el.appendChild(c));
|
||||
}
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
let rootContainer = null;
|
||||
let rootComponent = null;
|
||||
|
||||
function render(component, container) {
|
||||
rootContainer = container;
|
||||
rootComponent = component;
|
||||
rerender();
|
||||
}
|
||||
|
||||
function rerender() {
|
||||
rootContainer.innerHTML = "";
|
||||
rootContainer.appendChild(rootComponent());
|
||||
}
|
||||
|
||||
function createSignal(initialValue) {
|
||||
let value = initialValue;
|
||||
|
||||
const getValue = () => value;
|
||||
const setValue = (newValue) => {
|
||||
value = typeof newValue === "function" ? newValue(value) : newValue;
|
||||
rerender();
|
||||
};
|
||||
|
||||
return [getValue, setValue];
|
||||
}
|
||||
|
||||
OrgPortal.runtime = {
|
||||
h,
|
||||
render,
|
||||
createSignal,
|
||||
};
|
||||
})();
|
||||
@ -2,31 +2,10 @@
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { createSignal } = window.RegistryApp.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
|
||||
|
||||
class OrgPortalStore {
|
||||
constructor() {
|
||||
[this.getFunds, this.setFunds] = createSignal(portalData.funds);
|
||||
[this.getMembers, this.setMembers] = createSignal([
|
||||
...portalData.members,
|
||||
]);
|
||||
[this.getCreditLines, this.setCreditLines] = createSignal([]);
|
||||
[this.getTreasuryNotice, this.setTreasuryNotice] = createSignal({
|
||||
type: "",
|
||||
text: "",
|
||||
OrgPortal.store = SharedLogic.createPortalStore({
|
||||
createSignal,
|
||||
portalData,
|
||||
});
|
||||
[this.getModal, this.setModal] = createSignal(null);
|
||||
[this.getOrgDisbanded, this.setOrgDisbanded] = createSignal(false);
|
||||
}
|
||||
|
||||
hydrateFromPayload(payload) {
|
||||
this.setFunds(payload.portalData.funds || 0);
|
||||
this.setMembers([...(payload.portalData.members || [])]);
|
||||
this.setCreditLines([]);
|
||||
this.setTreasuryNotice({ type: "", text: "" });
|
||||
this.setModal(null);
|
||||
this.setOrgDisbanded(false);
|
||||
}
|
||||
}
|
||||
|
||||
OrgPortal.store = new OrgPortalStore();
|
||||
})();
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
(function () {
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const SVG_TAGS = new Set([
|
||||
@ -66,6 +67,7 @@
|
||||
|
||||
let rootContainer = null;
|
||||
let rootComponent = null;
|
||||
const injectedStyles = new Set();
|
||||
|
||||
function render(component, container) {
|
||||
rootContainer = container;
|
||||
@ -74,10 +76,26 @@
|
||||
}
|
||||
|
||||
function rerender() {
|
||||
if (!rootContainer || !rootComponent) {
|
||||
return;
|
||||
}
|
||||
|
||||
rootContainer.innerHTML = "";
|
||||
rootContainer.appendChild(rootComponent());
|
||||
}
|
||||
|
||||
function ensureScopedStyle(id, cssText) {
|
||||
if (!id || !cssText || injectedStyles.has(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute("data-ui-style", id);
|
||||
style.textContent = cssText;
|
||||
document.head.appendChild(style);
|
||||
injectedStyles.add(id);
|
||||
}
|
||||
|
||||
function createSignal(initialValue) {
|
||||
let value = initialValue;
|
||||
|
||||
@ -90,9 +108,14 @@
|
||||
return [getValue, setValue];
|
||||
}
|
||||
|
||||
RegistryApp.runtime = {
|
||||
const runtime = {
|
||||
h,
|
||||
render,
|
||||
createSignal,
|
||||
ensureScopedStyle,
|
||||
};
|
||||
|
||||
RegistryApp.runtime = runtime;
|
||||
OrgPortal.runtime = runtime;
|
||||
window.AppRuntime = runtime;
|
||||
})();
|
||||
|
||||
@ -1,43 +1,23 @@
|
||||
(function () {
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { createSignal } = RegistryApp.runtime;
|
||||
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
|
||||
|
||||
class RegistryStore {
|
||||
constructor() {
|
||||
[this.getView, this.setView] = createSignal("home");
|
||||
[this.getIsAuthenticating, this.setIsAuthenticating] =
|
||||
createSignal(false);
|
||||
[this.getLoginError, this.setLoginError] = createSignal("");
|
||||
}
|
||||
|
||||
startLogin() {
|
||||
this.setLoginError("");
|
||||
this.setIsAuthenticating(true);
|
||||
}
|
||||
|
||||
failLogin(message) {
|
||||
this.setIsAuthenticating(false);
|
||||
this.setLoginError(message || "Authentication failed.");
|
||||
}
|
||||
|
||||
completeLogin(payload) {
|
||||
RegistryApp.store = SharedLogic.createRegistryStore({
|
||||
createSignal,
|
||||
onHydratePortal(payload) {
|
||||
const OrgPortal = window.OrgPortal;
|
||||
const portalData = payload?.portalData;
|
||||
const session = payload?.session;
|
||||
|
||||
if (!OrgPortal || !portalData || !session) {
|
||||
this.failLogin("Login response was missing portal data.");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
OrgPortal.data.applyLoginPayload(payload);
|
||||
OrgPortal.store.hydrateFromPayload(payload);
|
||||
|
||||
this.setLoginError("");
|
||||
this.setIsAuthenticating(false);
|
||||
this.setView("portal");
|
||||
}
|
||||
}
|
||||
|
||||
RegistryApp.store = new RegistryStore();
|
||||
RegistryApp.store.setView("portal");
|
||||
return true;
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
36
arma/client/addons/org/ui/_site/views/DisbandedView.js
Normal file
36
arma/client/addons/org/ui/_site/views/DisbandedView.js
Normal file
@ -0,0 +1,36 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const registryStore = window.RegistryApp.store;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.DisbandedView = function DisbandedView() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
|
||||
return PanelCard({
|
||||
className: "org-span-12 org-empty-state",
|
||||
eyebrow: "Organization Removed",
|
||||
title: portalData.org.name,
|
||||
body: h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"p",
|
||||
{ className: "org-summary" },
|
||||
"This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview.",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-secondary-btn",
|
||||
onClick: () => registryStore.setView("home"),
|
||||
},
|
||||
"Return to Registry",
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
89
arma/client/addons/org/ui/_site/views/HomeView.js
Normal file
89
arma/client/addons/org/ui/_site/views/HomeView.js
Normal file
@ -0,0 +1,89 @@
|
||||
(function () {
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h, ensureScopedStyle } = RegistryApp.runtime;
|
||||
const store = RegistryApp.store;
|
||||
const bridge = RegistryApp.bridge;
|
||||
const scopeAttr = "data-ui-home-view";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const homeViewCss = `
|
||||
${scopeSelector} {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .home-feedback {
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.92rem;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
RegistryApp.componentFns = RegistryApp.componentFns || {};
|
||||
|
||||
RegistryApp.componentFns.HomeView = function HomeView() {
|
||||
const isAuthenticating = store.getIsAuthenticating();
|
||||
const loginError = store.getLoginError();
|
||||
ensureScopedStyle("main-home-view", homeViewCss);
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "content", [scopeAttr]: "" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "card" },
|
||||
h("h2", null, "Create Organization"),
|
||||
h(
|
||||
"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.",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{ onClick: () => store.setView("create") },
|
||||
"Register",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "card" },
|
||||
h("h2", null, "Organization Portal"),
|
||||
h(
|
||||
"p",
|
||||
null,
|
||||
"Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink.",
|
||||
),
|
||||
loginError
|
||||
? h("div", { className: "home-feedback" }, loginError)
|
||||
: null,
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
disabled: isAuthenticating,
|
||||
onClick: () => {
|
||||
if (!bridge) {
|
||||
store.failLogin(
|
||||
"Login bridge is not available.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
bridge.requestLogin({});
|
||||
},
|
||||
},
|
||||
isAuthenticating ? "Opening Portal..." : "Login",
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
206
arma/client/addons/org/ui/_site/views/PortalView.js
Normal file
206
arma/client/addons/org/ui/_site/views/PortalView.js
Normal file
@ -0,0 +1,206 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const { portalData, session } = OrgPortal.data;
|
||||
const store = OrgPortal.store;
|
||||
const portalViewScope = "[data-ui-portal-view]";
|
||||
|
||||
ensureScopedStyle(
|
||||
"portal-view",
|
||||
`
|
||||
${portalViewScope} .org-toast-stack {
|
||||
position: fixed;
|
||||
top: 1.5rem;
|
||||
right: 2rem;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
${portalViewScope} .org-toast {
|
||||
max-width: 24rem;
|
||||
padding: 0.9rem 1rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
box-shadow: 0 12px 28px rgb(15 23 42 / 0.14);
|
||||
font-size: 0.92rem;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
${portalViewScope} .org-toast.is-success {
|
||||
background: #ecfdf5;
|
||||
border-color: #bbf7d0;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
${portalViewScope} .org-toast.is-error {
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
${portalViewScope} .org-dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
${portalViewScope} .org-panel {
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
${portalViewScope} .org-scroll-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 31rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
${portalViewScope} .org-span-12 {
|
||||
grid-column: span 12;
|
||||
}
|
||||
|
||||
${portalViewScope} .org-span-7 {
|
||||
grid-column: span 7;
|
||||
}
|
||||
|
||||
${portalViewScope} .org-span-6 {
|
||||
grid-column: span 6;
|
||||
}
|
||||
|
||||
${portalViewScope} .org-span-5 {
|
||||
grid-column: span 5;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${portalViewScope} .org-toast-stack {
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
${portalViewScope} .org-toast {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
${portalViewScope} .org-span-12,
|
||||
${portalViewScope} .org-span-7,
|
||||
${portalViewScope} .org-span-6,
|
||||
${portalViewScope} .org-span-5 {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
OrgPortal.components = OrgPortal.components || {};
|
||||
|
||||
OrgPortal.components.App = function App() {
|
||||
const Hero = window.SharedUI.componentFns.Hero;
|
||||
const Footer = window.SharedUI.componentFns.Footer;
|
||||
const OverviewCard = OrgPortal.componentFns.OverviewCard;
|
||||
const FleetCard = OrgPortal.componentFns.FleetCard;
|
||||
const TreasuryCard = OrgPortal.componentFns.TreasuryCard;
|
||||
const MembersCard = OrgPortal.componentFns.MembersCard;
|
||||
const AssetsCard = OrgPortal.componentFns.AssetsCard;
|
||||
const ActivityCard = OrgPortal.componentFns.ActivityCard;
|
||||
const FutureCard = OrgPortal.componentFns.FutureCard;
|
||||
const DangerCard = OrgPortal.componentFns.DangerCard;
|
||||
const ModalLayer = OrgPortal.componentFns.ModalLayer;
|
||||
const DisbandedView = OrgPortal.componentFns.DisbandedView;
|
||||
const treasuryNotice = store.getTreasuryNotice();
|
||||
const footerSections = [
|
||||
{
|
||||
title: "Organization Controls",
|
||||
items: [
|
||||
"Roster Management",
|
||||
"Fleet Assignment",
|
||||
"Treasury Permissions",
|
||||
"Asset Registry",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Planned Extensions",
|
||||
items: [
|
||||
"Contracts Board",
|
||||
"Diplomacy Layer",
|
||||
"Procurement Queue",
|
||||
"Reputation History",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
if (store.getOrgDisbanded()) {
|
||||
return h(
|
||||
"main",
|
||||
{ "data-ui-portal-view": "" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "container" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-dashboard-grid" },
|
||||
Hero({
|
||||
kicker: portalData.org.tag,
|
||||
title: portalData.org.name,
|
||||
subtitle: "Player organization command portal",
|
||||
meta: `${session.actorName} - ${session.role}`,
|
||||
}),
|
||||
DisbandedView(),
|
||||
),
|
||||
),
|
||||
ModalLayer(),
|
||||
Footer({ sections: footerSections }),
|
||||
);
|
||||
}
|
||||
|
||||
return h(
|
||||
"main",
|
||||
{ "data-ui-portal-view": "" },
|
||||
treasuryNotice.text
|
||||
? h(
|
||||
"div",
|
||||
{ className: "org-toast-stack" },
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
treasuryNotice.type === "error"
|
||||
? "org-toast is-error"
|
||||
: "org-toast is-success",
|
||||
},
|
||||
treasuryNotice.text,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
h(
|
||||
"div",
|
||||
{ className: "container" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-dashboard-grid" },
|
||||
Hero({
|
||||
kicker: portalData.org.tag,
|
||||
title: portalData.org.name,
|
||||
subtitle: "Player organization command portal",
|
||||
meta: `${session.actorName} - ${session.role}`,
|
||||
}),
|
||||
OverviewCard(),
|
||||
FleetCard(),
|
||||
TreasuryCard(),
|
||||
MembersCard(),
|
||||
AssetsCard(),
|
||||
ActivityCard(),
|
||||
FutureCard(),
|
||||
DangerCard(),
|
||||
),
|
||||
),
|
||||
ModalLayer(),
|
||||
Footer({ sections: footerSections }),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,26 +1,175 @@
|
||||
(function () {
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
const { h, ensureScopedStyle } = RegistryApp.runtime;
|
||||
const store = RegistryApp.store;
|
||||
const bridge = RegistryApp.bridge;
|
||||
const scopeAttr = "data-ui-registration-view";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const registrationViewCss = `
|
||||
${scopeSelector} {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
${scopeSelector} .info-panel {
|
||||
text-align: left;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .create-feature-list {
|
||||
text-align: left;
|
||||
margin-top: 1.5rem;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
${scopeSelector} .create-feature-item {
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .create-feature-icon {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
${scopeSelector} .price-tag {
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-app);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
${scopeSelector} .price-label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .price-value {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
${scopeSelector} .form-panel {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-form label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-form input,
|
||||
${scopeSelector} .app-form select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-form input:focus,
|
||||
${scopeSelector} .app-form select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1);
|
||||
}
|
||||
|
||||
${scopeSelector} .form-actions {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
${scopeSelector} .submit-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
${scopeSelector} .cancel-link {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
${scopeSelector} .cancel-link:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
${scopeSelector} .form-feedback {
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .form-feedback.is-error {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
RegistryApp.componentFns = RegistryApp.componentFns || {};
|
||||
|
||||
RegistryApp.componentFns.CreateOrgForm = function CreateOrgForm() {
|
||||
RegistryApp.componentFns.RegistrationView = function RegistrationView() {
|
||||
const isCreating = store.getIsCreating();
|
||||
const createError = store.getCreateError();
|
||||
ensureScopedStyle("main-registration-view", registrationViewCss);
|
||||
|
||||
const handleCreate = () => {
|
||||
const data = {
|
||||
orgName: String(
|
||||
document.getElementById("org-create-name")?.value || "",
|
||||
),
|
||||
).trim(),
|
||||
type: String(
|
||||
document.getElementById("org-create-type")?.value || "",
|
||||
),
|
||||
};
|
||||
console.log("Org Registration:", data);
|
||||
|
||||
if (!bridge || typeof bridge.requestCreateOrg !== "function") {
|
||||
store.failCreate("Registration bridge is not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
bridge.requestCreateOrg(data);
|
||||
};
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "split-container" },
|
||||
{ className: "split-container", [scopeAttr]: "" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "info-panel" },
|
||||
@ -32,24 +181,10 @@
|
||||
),
|
||||
h(
|
||||
"ul",
|
||||
{
|
||||
style: {
|
||||
textAlign: "left",
|
||||
marginTop: "1.5rem",
|
||||
listStyleType: "none",
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
{ className: "create-feature-list" },
|
||||
h(
|
||||
"li",
|
||||
{
|
||||
style: {
|
||||
marginBottom: "0.5rem",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
},
|
||||
},
|
||||
{ className: "create-feature-item" },
|
||||
h(
|
||||
"svg",
|
||||
{
|
||||
@ -59,11 +194,7 @@
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round",
|
||||
style: {
|
||||
width: "1.2rem",
|
||||
height: "1.2rem",
|
||||
flexShrink: "0",
|
||||
},
|
||||
className: "create-feature-icon",
|
||||
},
|
||||
h("path", { d: "M20 6L9 17l-5-5" }),
|
||||
),
|
||||
@ -71,14 +202,7 @@
|
||||
),
|
||||
h(
|
||||
"li",
|
||||
{
|
||||
style: {
|
||||
marginBottom: "0.5rem",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
},
|
||||
},
|
||||
{ className: "create-feature-item" },
|
||||
h(
|
||||
"svg",
|
||||
{
|
||||
@ -88,11 +212,7 @@
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round",
|
||||
style: {
|
||||
width: "1.2rem",
|
||||
height: "1.2rem",
|
||||
flexShrink: "0",
|
||||
},
|
||||
className: "create-feature-icon",
|
||||
},
|
||||
h("path", { d: "M20 6L9 17l-5-5" }),
|
||||
),
|
||||
@ -100,14 +220,7 @@
|
||||
),
|
||||
h(
|
||||
"li",
|
||||
{
|
||||
style: {
|
||||
marginBottom: "0.5rem",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
},
|
||||
},
|
||||
{ className: "create-feature-item" },
|
||||
h(
|
||||
"svg",
|
||||
{
|
||||
@ -117,11 +230,7 @@
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round",
|
||||
style: {
|
||||
width: "1.2rem",
|
||||
height: "1.2rem",
|
||||
flexShrink: "0",
|
||||
},
|
||||
className: "create-feature-icon",
|
||||
},
|
||||
h("path", { d: "M20 6L9 17l-5-5" }),
|
||||
),
|
||||
@ -129,14 +238,7 @@
|
||||
),
|
||||
h(
|
||||
"li",
|
||||
{
|
||||
style: {
|
||||
marginBottom: "0.5rem",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
},
|
||||
},
|
||||
{ className: "create-feature-item" },
|
||||
h(
|
||||
"svg",
|
||||
{
|
||||
@ -146,11 +248,7 @@
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round",
|
||||
style: {
|
||||
width: "1.2rem",
|
||||
height: "1.2rem",
|
||||
flexShrink: "0",
|
||||
},
|
||||
className: "create-feature-icon",
|
||||
},
|
||||
h("path", { d: "M20 6L9 17l-5-5" }),
|
||||
),
|
||||
@ -159,44 +257,14 @@
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className: "price-tag",
|
||||
style: {
|
||||
marginTop: "2rem",
|
||||
padding: "1rem",
|
||||
background: "var(--bg-app)",
|
||||
borderRadius: "var(--radius)",
|
||||
border: "1px solid var(--border)",
|
||||
},
|
||||
},
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
style: {
|
||||
display: "block",
|
||||
fontSize: "0.9rem",
|
||||
color: "var(--text-muted)",
|
||||
},
|
||||
},
|
||||
"Registration Fee",
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
style: {
|
||||
display: "block",
|
||||
fontSize: "2rem",
|
||||
fontWeight: "700",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
},
|
||||
"$50,000",
|
||||
),
|
||||
{ className: "price-tag" },
|
||||
h("span", { className: "price-label" }, "Registration Fee"),
|
||||
h("span", { className: "price-value" }, "$50,000"),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "form-panel card", style: { margin: 0 } },
|
||||
{ className: "form-panel card" },
|
||||
h("h2", null, "Organization Registration"),
|
||||
h(
|
||||
"div",
|
||||
@ -239,14 +307,24 @@
|
||||
h(
|
||||
"div",
|
||||
{ className: "form-actions" },
|
||||
createError
|
||||
? h(
|
||||
"div",
|
||||
{ className: "form-feedback is-error" },
|
||||
createError,
|
||||
)
|
||||
: null,
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
style: { width: "100%" },
|
||||
className: "submit-btn",
|
||||
disabled: isCreating,
|
||||
onClick: handleCreate,
|
||||
},
|
||||
"Submit Registration",
|
||||
isCreating
|
||||
? "Submitting Registration..."
|
||||
: "Submit Registration",
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
@ -14,6 +14,29 @@ PREP_RECOMPILE_END;
|
||||
GVAR(OrgStore) call ["init", [_uid]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestCreateOrg), {
|
||||
params [["_uid", "", [""]], ["_orgName", "", [""]]];
|
||||
|
||||
if (_uid isEqualTo "" || { _orgName isEqualTo "" }) exitWith {
|
||||
diag_log "[FORGE:Server:Org] Empty/Invalid UID or Organization Name!"
|
||||
};
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
private _result = GVAR(OrgStore) call ["register", [_uid, _orgName]];
|
||||
|
||||
if (_result getOrDefault ["success", false]) then {
|
||||
private _org = _result getOrDefault ["org", createHashMap];
|
||||
private _orgID = _org getOrDefault ["id", ""];
|
||||
|
||||
if (_orgID isNotEqualTo "") then {
|
||||
private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, true]];
|
||||
[CRPC(actor,responseSyncActor), [_actorPatch], _player] call CFUNC(targetEvent);
|
||||
};
|
||||
};
|
||||
|
||||
[CRPC(org,responseCreateOrg), [_result], _player] call CFUNC(targetEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestGetOrg), {
|
||||
params [["_uid", "", [""]], ["_field", "", [""]]];
|
||||
|
||||
|
||||
@ -82,7 +82,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
if !(_isSuccess) exitWith {
|
||||
["ERROR", "Failed to check for default org!"] call EFUNC(common,log);
|
||||
|
||||
private _fallbackDefaultOrg = createHashMapFromArray [
|
||||
private _defaultOrg = createHashMapFromArray [
|
||||
["id", "default"],
|
||||
["owner", "server"],
|
||||
["name", "Forge Dynamics"],
|
||||
@ -90,9 +90,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
["reputation", 0],
|
||||
["members", createHashMap]
|
||||
];
|
||||
GVAR(Registry) set ["default", _fallbackDefaultOrg];
|
||||
GVAR(Registry) set ["default", _defaultOrg];
|
||||
|
||||
_fallbackDefaultOrg
|
||||
_defaultOrg
|
||||
};
|
||||
|
||||
private _finalOrg = createHashMap;
|
||||
@ -112,6 +112,109 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
|
||||
GVAR(Registry) set ["default", _finalOrg];
|
||||
}],
|
||||
["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 _finalMembers = +_members;
|
||||
_finalMembers set [_uid, createHashMapFromArray [["uid", _uid], ["name", _memberName]]];
|
||||
_org set ["members", _finalMembers];
|
||||
|
||||
_org
|
||||
}],
|
||||
["register", compileFinal {
|
||||
params [["_uid", "", [""]], ["_orgName", "", [""]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", ""],
|
||||
["org", createHashMap]
|
||||
];
|
||||
|
||||
if (_uid isEqualTo "" || { _orgName isEqualTo "" }) exitWith {
|
||||
_result set ["message", "A valid player and organization name are required."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
|
||||
private _existingOrgID = _actor getOrDefault ["organization", ""];
|
||||
|
||||
if (_existingOrgID isNotEqualTo "") exitWith {
|
||||
_result set ["message", "Player already belongs to an organization."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _orgID = _actor getOrDefault ["phone_number", ""];
|
||||
if (_orgID isEqualTo "") exitWith {
|
||||
_result set ["message", "Player phone number was not available for organization registration."];
|
||||
_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],
|
||||
["members", createHashMap]
|
||||
];
|
||||
|
||||
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]];
|
||||
_result
|
||||
};
|
||||
|
||||
if (_createResult isNotEqualTo "") then {
|
||||
_org = _self call ["toHashMap", [_createResult]];
|
||||
};
|
||||
|
||||
_org set ["members", createHashMap];
|
||||
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
|
||||
|
||||
["org:members:remove", ["default", _uid]] call EFUNC(extension,extCall);
|
||||
|
||||
private _defaultOrg = GVAR(Registry) getOrDefault ["default", createHashMap];
|
||||
if (_defaultOrg isNotEqualTo createHashMap) then {
|
||||
private _defaultMembers = _defaultOrg getOrDefault ["members", createHashMap];
|
||||
_defaultMembers deleteAt _uid;
|
||||
_defaultOrg set ["members", _defaultMembers];
|
||||
GVAR(Registry) set ["default", _defaultOrg, true];
|
||||
};
|
||||
|
||||
GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", _orgID]]];
|
||||
GVAR(Registry) set [_orgID, _org, true];
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["org", _org];
|
||||
_result
|
||||
}],
|
||||
["init", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
@ -121,24 +224,26 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
if (_orgID isEqualTo "") then { _orgID = "default" };
|
||||
|
||||
private _cached = GVAR(Registry) getOrDefault [_orgID, nil];
|
||||
if !(isNil { _cached }) exitWith { [CRPC(org,responseInitOrg), [_cached], _player] call CFUNC(targetEvent); _cached };
|
||||
if !(isNil { _cached }) exitWith {
|
||||
private _cachedOwner = _cached getOrDefault ["owner", ""];
|
||||
if (_orgID isEqualTo "default" || { _cachedOwner isEqualTo _uid }) then {
|
||||
_cached = _self call ["verifyMember", [_cached, _orgID, _uid, _player, _actor]];
|
||||
};
|
||||
GVAR(Registry) set [_orgID, _cached, true];
|
||||
[CRPC(org,responseInitOrg), [_cached], _player] call CFUNC(targetEvent);
|
||||
|
||||
_cached
|
||||
};
|
||||
|
||||
["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
|
||||
if !(_isSuccess) exitWith {
|
||||
["ERROR", format ["Failed to check for org %1! Using fallback org.", _orgID]] call EFUNC(common,log);
|
||||
|
||||
private _fallbackOrg = GVAR(Registry) getOrDefault ["default", createHashMapFromArray [
|
||||
["id", "default"],
|
||||
["owner", "server"],
|
||||
["name", "Forge Dynamics"],
|
||||
["funds", 200000],
|
||||
["reputation", 0],
|
||||
["members", createHashMap]
|
||||
]];
|
||||
|
||||
private _fallbackOrg = GVAR(Registry) getOrDefault ["default", createHashMap];
|
||||
private _entry = createHashMapFromArray [["orgID", _orgID]];
|
||||
GVAR(IndexRegistry) set [_uid, _entry];
|
||||
|
||||
if (_orgID isEqualTo "default") then { _fallbackOrg = _self call ["verifyMember", [_fallbackOrg, _orgID, _uid, _player, _actor]]; };
|
||||
GVAR(Registry) set [_orgID, _fallbackOrg, true];
|
||||
[CRPC(org,responseInitOrg), [_fallbackOrg], _player] call CFUNC(targetEvent);
|
||||
|
||||
@ -154,6 +259,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
["INFO", format ["Found org for %1", _orgID]] call EFUNC(common,log);
|
||||
} else {
|
||||
["WARNING", format ["No existing org found for %1, using default org.", _uid]] call EFUNC(common,log);
|
||||
_finalOrg = GVAR(Registry) getOrDefault ["default", createHashMap];
|
||||
_orgID = "default";
|
||||
};
|
||||
|
||||
private _entry = createHashMapFromArray [["orgID", _orgID]];
|
||||
@ -177,6 +284,10 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
_finalOrg set ["members", _finalMembers];
|
||||
// _finalOrg set ["assets", _finalAssets];
|
||||
|
||||
private _finalOwner = _finalOrg getOrDefault ["owner", ""];
|
||||
if (_orgID isEqualTo "default" || { _finalOwner isEqualTo _uid }) then {
|
||||
_finalOrg = _self call ["verifyMember", [_finalOrg, _orgID, _uid, _player, _actor]];
|
||||
};
|
||||
GVAR(Registry) set [_orgID, _finalOrg, true];
|
||||
[CRPC(org,responseInitOrg), [_finalOrg], _player] call CFUNC(targetEvent);
|
||||
|
||||
|
||||
107
arma/ui/apps/components/AppShell.js
Normal file
107
arma/ui/apps/components/AppShell.js
Normal file
@ -0,0 +1,107 @@
|
||||
(function () {
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
const store = RegistryApp.store;
|
||||
|
||||
RegistryApp.components = RegistryApp.components || {};
|
||||
|
||||
RegistryApp.components.App = function App() {
|
||||
const Navbar = window.SharedUI.componentFns.Navbar;
|
||||
const Header = window.SharedUI.componentFns.Header;
|
||||
const Footer = window.SharedUI.componentFns.Footer;
|
||||
const HomeView = RegistryApp.componentFns.HomeView;
|
||||
const RegistrationView = RegistryApp.componentFns.RegistrationView;
|
||||
const PortalApp =
|
||||
window.OrgPortal && window.OrgPortal.components
|
||||
? window.OrgPortal.components.App
|
||||
: null;
|
||||
|
||||
const view = store.getView();
|
||||
const viewLabel =
|
||||
view === "create"
|
||||
? "Organization Registration"
|
||||
: view === "portal"
|
||||
? "Organization Portal"
|
||||
: "Entry Hub";
|
||||
const actionLabel = view === "portal" ? "Sign Out" : "Close";
|
||||
const footerSections = [
|
||||
{
|
||||
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 closeRegistry() {
|
||||
if (
|
||||
typeof A3API !== "undefined" &&
|
||||
typeof A3API.SendAlert === "function"
|
||||
) {
|
||||
A3API.SendAlert(
|
||||
JSON.stringify({
|
||||
event: "org::close",
|
||||
data: {},
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
store.setView("home");
|
||||
}
|
||||
|
||||
if (view === "portal" && PortalApp) {
|
||||
return h(
|
||||
"div",
|
||||
null,
|
||||
Navbar({
|
||||
title: "Global Organization Network",
|
||||
viewLabel,
|
||||
actionLabel,
|
||||
onAction: closeRegistry,
|
||||
}),
|
||||
PortalApp(),
|
||||
);
|
||||
}
|
||||
|
||||
let mainContent;
|
||||
if (view === "home") {
|
||||
mainContent = HomeView();
|
||||
} else if (view === "create") {
|
||||
mainContent = RegistrationView();
|
||||
}
|
||||
|
||||
return h(
|
||||
"main",
|
||||
null,
|
||||
Navbar({
|
||||
title: "Global Organization Network",
|
||||
viewLabel,
|
||||
actionLabel,
|
||||
onAction: closeRegistry,
|
||||
}),
|
||||
h(
|
||||
"div",
|
||||
{ className: "container" },
|
||||
Header({
|
||||
title: "Global Organization Network",
|
||||
onTitleClick: () => store.setView("home"),
|
||||
}),
|
||||
mainContent,
|
||||
),
|
||||
Footer({ sections: footerSections }),
|
||||
);
|
||||
};
|
||||
})();
|
||||
32
arma/ui/apps/components/footer.js
Normal file
32
arma/ui/apps/components/footer.js
Normal file
@ -0,0 +1,32 @@
|
||||
(function () {
|
||||
const SharedUI = (window.SharedUI = window.SharedUI || {});
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
|
||||
SharedUI.componentFns = SharedUI.componentFns || {};
|
||||
|
||||
SharedUI.componentFns.Footer = function Footer({ sections = [] }) {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "footer" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "wrapper" },
|
||||
...sections.map((section) =>
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h3", null, section.title),
|
||||
h(
|
||||
"ul",
|
||||
{ style: { listStyleType: "none", padding: 0 } },
|
||||
...(section.items || []).map((item) =>
|
||||
h("li", null, item),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
27
arma/ui/apps/components/header.js
Normal file
27
arma/ui/apps/components/header.js
Normal file
@ -0,0 +1,27 @@
|
||||
(function () {
|
||||
const SharedUI = (window.SharedUI = window.SharedUI || {});
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
|
||||
SharedUI.componentFns = SharedUI.componentFns || {};
|
||||
|
||||
SharedUI.componentFns.Header = function Header({
|
||||
title,
|
||||
subtitle = "Organization Registration & Management Portal",
|
||||
onTitleClick = null,
|
||||
}) {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "header" },
|
||||
h(
|
||||
"h1",
|
||||
{
|
||||
style: { cursor: onTitleClick ? "pointer" : "default" },
|
||||
onClick: onTitleClick,
|
||||
},
|
||||
title,
|
||||
),
|
||||
h("p", null, subtitle),
|
||||
);
|
||||
};
|
||||
})();
|
||||
35
arma/ui/apps/components/hero.js
Normal file
35
arma/ui/apps/components/hero.js
Normal file
@ -0,0 +1,35 @@
|
||||
(function () {
|
||||
const SharedUI = (window.SharedUI = window.SharedUI || {});
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
|
||||
SharedUI.componentFns = SharedUI.componentFns || {};
|
||||
|
||||
SharedUI.componentFns.Hero = function Hero({
|
||||
className = "",
|
||||
kicker = "",
|
||||
title = "",
|
||||
subtitle = "",
|
||||
meta = "",
|
||||
}) {
|
||||
const finalClassName = [
|
||||
"card org-panel org-span-12 org-page-header",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: finalClassName },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-page-heading" },
|
||||
h("span", { className: "org-page-kicker" }, kicker),
|
||||
h("h1", { className: "org-page-title" }, title),
|
||||
h("p", { className: "org-page-subtitle" }, subtitle),
|
||||
h("span", { className: "org-page-meta" }, meta),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
190
arma/ui/apps/components/modal.js
Normal file
190
arma/ui/apps/components/modal.js
Normal file
@ -0,0 +1,190 @@
|
||||
(function () {
|
||||
const SharedUI = (window.SharedUI = window.SharedUI || {});
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h, ensureScopedStyle } = RegistryApp.runtime;
|
||||
const scopeAttr = "data-ui-modal";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const modalCss = `
|
||||
${scopeSelector} {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgb(15 23 42 / 0.38);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-card {
|
||||
width: min(100%, 30rem);
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-title {
|
||||
margin: 0;
|
||||
color: var(--primary-hover);
|
||||
font-size: 1.45rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-close {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 0;
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-main);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-close:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
color: var(--text-main);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-form label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-form input,
|
||||
${scopeSelector} .app-modal-form select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-form input:focus,
|
||||
${scopeSelector} .app-modal-form select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12);
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-form input:disabled,
|
||||
${scopeSelector} .app-modal-form select:disabled {
|
||||
background: #f1f5f9;
|
||||
color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-actions button + button,
|
||||
${scopeSelector} .app-modal-danger-actions button + button {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-danger {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: var(--radius);
|
||||
background: #fff1f2;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-danger p {
|
||||
margin: 0;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
${scopeSelector} .app-modal-danger-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .app-modal-head,
|
||||
${scopeSelector} .app-modal-danger {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
SharedUI.componentFns = SharedUI.componentFns || {};
|
||||
|
||||
SharedUI.componentFns.Modal = function Modal({
|
||||
title = "",
|
||||
body = null,
|
||||
onClose = null,
|
||||
}) {
|
||||
ensureScopedStyle("shared-modal", modalCss);
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
className: "app-modal-backdrop",
|
||||
[scopeAttr]: "",
|
||||
onClick: (e) => {
|
||||
if (e.target === e.currentTarget && onClose) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
},
|
||||
h(
|
||||
"div",
|
||||
{ className: "card app-modal-card" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "app-modal-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h2", { className: "app-modal-title" }, title),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "app-modal-close",
|
||||
onClick: onClose,
|
||||
"aria-label": "Close dialog",
|
||||
},
|
||||
"x",
|
||||
),
|
||||
),
|
||||
body,
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
129
arma/ui/apps/components/navbar.js
Normal file
129
arma/ui/apps/components/navbar.js
Normal file
@ -0,0 +1,129 @@
|
||||
(function () {
|
||||
const SharedUI = (window.SharedUI = window.SharedUI || {});
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h, ensureScopedStyle } = RegistryApp.runtime;
|
||||
const scopeAttr = "data-ui-navbar";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const navbarCss = `
|
||||
${scopeSelector} {
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
${scopeSelector} .app-navbar-inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 2rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-navbar-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-navbar-kicker {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-navbar-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-hover);
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-navbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-navbar-view {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-close-btn {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-close-btn:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
color: var(--primary-hover);
|
||||
border-color: var(--primary);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .app-navbar-inner {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .app-navbar-actions {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
SharedUI.componentFns = SharedUI.componentFns || {};
|
||||
|
||||
SharedUI.componentFns.Navbar = function Navbar({
|
||||
kicker = "ORBIS",
|
||||
title = "",
|
||||
viewLabel = "",
|
||||
actionLabel = "",
|
||||
onAction = null,
|
||||
}) {
|
||||
ensureScopedStyle("shared-navbar", navbarCss);
|
||||
|
||||
return h(
|
||||
"nav",
|
||||
{ className: "app-navbar", [scopeAttr]: "" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "app-navbar-inner" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "app-navbar-brand" },
|
||||
h("span", { className: "app-navbar-kicker" }, kicker),
|
||||
h("span", { className: "app-navbar-title" }, title),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "app-navbar-actions" },
|
||||
h("span", { className: "app-navbar-view" }, viewLabel),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "app-close-btn",
|
||||
onClick: onAction,
|
||||
},
|
||||
actionLabel,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
83
arma/ui/apps/components/panelCard.js
Normal file
83
arma/ui/apps/components/panelCard.js
Normal file
@ -0,0 +1,83 @@
|
||||
(function () {
|
||||
const SharedUI = (window.SharedUI = window.SharedUI || {});
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h, ensureScopedStyle } = RegistryApp.runtime;
|
||||
const scopeAttr = "data-ui-panel-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const panelCardCss = `
|
||||
${scopeSelector} .org-panel-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-eyebrow {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-panel-title {
|
||||
margin: 0;
|
||||
color: var(--primary-hover);
|
||||
font-size: 1.45rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-panel-subtitle {
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-panel-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
SharedUI.componentFns = SharedUI.componentFns || {};
|
||||
|
||||
SharedUI.componentFns.PanelCard = function PanelCard({
|
||||
className = "",
|
||||
eyebrow = "",
|
||||
title = "",
|
||||
subtitle = "",
|
||||
headerExtras = null,
|
||||
body = null,
|
||||
rootProps = {},
|
||||
}) {
|
||||
const finalClassName = ["card org-panel", className]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
ensureScopedStyle("shared-panel-card", panelCardCss);
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: finalClassName, [scopeAttr]: "", ...rootProps },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-panel-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
eyebrow
|
||||
? h("div", { className: "org-eyebrow" }, eyebrow)
|
||||
: null,
|
||||
h("h2", { className: "org-panel-title" }, title),
|
||||
subtitle
|
||||
? h("p", { className: "org-panel-subtitle" }, subtitle)
|
||||
: null,
|
||||
),
|
||||
headerExtras,
|
||||
),
|
||||
body,
|
||||
);
|
||||
};
|
||||
})();
|
||||
79
arma/ui/apps/components/portal/activityCard.js
Normal file
79
arma/ui/apps/components/portal/activityCard.js
Normal file
@ -0,0 +1,79 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const scopeAttr = "data-ui-activity-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const activityCardCss = `
|
||||
${scopeSelector} .org-activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 0.85rem;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 0.35rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #94a3b8 #e2e8f0;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-activity-row {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid #94a3b8;
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-activity-row:nth-child(even) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
border-left-color: #64748b;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-activity-row p {
|
||||
margin: 0;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-activity-time {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.35rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.ActivityCard = function ActivityCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
ensureScopedStyle("portal-activity-card", activityCardCss);
|
||||
|
||||
return PanelCard({
|
||||
className: "org-scroll-panel org-span-6",
|
||||
title: "Command Feed",
|
||||
subtitle: "Recent organization-level actions and updates.",
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
{ className: "org-activity-list" },
|
||||
...portalData.activity.map((item) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-activity-row" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "org-activity-time" },
|
||||
item.time,
|
||||
),
|
||||
h("p", null, item.text),
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
94
arma/ui/apps/components/portal/assetsCard.js
Normal file
94
arma/ui/apps/components/portal/assetsCard.js
Normal file
@ -0,0 +1,94 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const actions = OrgPortal.actions;
|
||||
const scopeAttr = "data-ui-assets-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const assetsCardCss = `
|
||||
${scopeSelector} .org-simple-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 0.85rem;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 0.35rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #94a3b8 #e2e8f0;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-row:nth-child(even) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-name {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-simple-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.AssetsCard = function AssetsCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
const SimpleStat = OrgPortal.componentFns.SimpleStat;
|
||||
ensureScopedStyle("portal-assets-card", assetsCardCss);
|
||||
|
||||
return PanelCard({
|
||||
className: "org-scroll-panel org-span-7",
|
||||
title: "Assets",
|
||||
subtitle: "Inventory supplies and equipment with quantity totals.",
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
{ className: "org-simple-list" },
|
||||
...portalData.assets.map((asset) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-simple-row" },
|
||||
h(
|
||||
"strong",
|
||||
{ className: "org-simple-name" },
|
||||
asset.name,
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-simple-meta" },
|
||||
SimpleStat(
|
||||
"Type",
|
||||
actions.formatAssetType(asset.type),
|
||||
),
|
||||
SimpleStat("Quantity", asset.quantity),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
70
arma/ui/apps/components/portal/dangerCard.js
Normal file
70
arma/ui/apps/components/portal/dangerCard.js
Normal file
@ -0,0 +1,70 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const permissions = OrgPortal.permissions;
|
||||
const actions = OrgPortal.actions;
|
||||
const scopeAttr = "data-ui-danger-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const dangerCardCss = `
|
||||
${scopeSelector} {
|
||||
border-color: #fecaca;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-danger-copy {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-danger-copy strong,
|
||||
${scopeSelector} .org-danger-copy p {
|
||||
display: block;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-danger-copy p {
|
||||
margin: 0.4rem 0 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.DangerCard = function DangerCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
ensureScopedStyle("portal-danger-card", dangerCardCss);
|
||||
|
||||
if (!permissions.canDisbandOrg()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return PanelCard({
|
||||
className: "org-span-12 org-danger-panel",
|
||||
title: "Organization Controls",
|
||||
subtitle:
|
||||
"Leader-only actions for membership and permanent organization removal.",
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-danger-copy" },
|
||||
h("strong", null, "Disband organization"),
|
||||
h(
|
||||
"p",
|
||||
null,
|
||||
"This removes the organization and revokes access to the portal for all members.",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-danger-btn",
|
||||
onClick: () => actions.openModal("disband"),
|
||||
},
|
||||
"Disband Organization",
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
101
arma/ui/apps/components/portal/fleetCard.js
Normal file
101
arma/ui/apps/components/portal/fleetCard.js
Normal file
@ -0,0 +1,101 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const actions = OrgPortal.actions;
|
||||
const scopeAttr = "data-ui-fleet-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const fleetCardCss = `
|
||||
${scopeSelector} .org-simple-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 0.85rem;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 0.35rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #94a3b8 #e2e8f0;
|
||||
}
|
||||
|
||||
${scopeSelector} {
|
||||
min-height: 32.5rem;
|
||||
max-height: 32.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-row:nth-child(even) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-name {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-simple-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.FleetCard = function FleetCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
const SimpleStat = OrgPortal.componentFns.SimpleStat;
|
||||
ensureScopedStyle("portal-fleet-card", fleetCardCss);
|
||||
|
||||
return PanelCard({
|
||||
className: "org-scroll-panel org-span-7",
|
||||
title: "Fleet",
|
||||
subtitle:
|
||||
"Individual vehicles with type, status, and overall damage.",
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
{ className: "org-simple-list" },
|
||||
...portalData.fleet.map((unit) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-simple-row" },
|
||||
h(
|
||||
"strong",
|
||||
{ className: "org-simple-name" },
|
||||
unit.name,
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-simple-meta" },
|
||||
SimpleStat(
|
||||
"Type",
|
||||
actions.formatVehicleType(unit.type),
|
||||
),
|
||||
SimpleStat("Status", unit.status),
|
||||
SimpleStat("Damage", unit.damage),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
105
arma/ui/apps/components/portal/futureCard.js
Normal file
105
arma/ui/apps/components/portal/futureCard.js
Normal file
@ -0,0 +1,105 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const scopeAttr = "data-ui-future-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const futureCardCss = `
|
||||
${scopeSelector} .org-roadmap-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 0.35rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #94a3b8 #e2e8f0;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-roadmap-card {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.7rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-roadmap-card:nth-child(4n + 2),
|
||||
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
|
||||
border-color: rgb(100 116 139 / 0.4);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-roadmap-card p {
|
||||
margin: 0;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-list-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
background: #e2e8f0;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-roadmap-card:nth-child(4n + 2) .org-list-tag,
|
||||
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {
|
||||
background: #cbd5e1;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-roadmap-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) {
|
||||
background: #f8fafc;
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {
|
||||
background: #e2e8f0;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.FutureCard = function FutureCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
ensureScopedStyle("portal-future-card", futureCardCss);
|
||||
|
||||
return PanelCard({
|
||||
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: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
{ className: "org-roadmap-grid" },
|
||||
...portalData.roadmap.map((item) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-roadmap-card" },
|
||||
h("span", { className: "org-list-tag" }, item.status),
|
||||
h("strong", null, item.name),
|
||||
h("p", null, item.detail),
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
@ -1,42 +1,83 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const store = OrgPortal.store;
|
||||
const permissions = OrgPortal.permissions;
|
||||
const actions = OrgPortal.actions;
|
||||
const scopeAttr = "data-ui-members-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const membersCardCss = `
|
||||
${scopeSelector} .org-name-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 0.85rem;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 0.35rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #94a3b8 #e2e8f0;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-name-row:nth-child(even) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-name-row button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-name-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-name-row button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.MembersCard = function MembersCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
const members = store.getMembers();
|
||||
const allowMemberManagement = permissions.canManageMembers();
|
||||
ensureScopedStyle("portal-members-card", membersCardCss);
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: "card org-panel org-scroll-panel org-span-5" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-panel-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h2", { className: "org-panel-title" }, "Members"),
|
||||
h(
|
||||
"p",
|
||||
{ className: "org-panel-subtitle" },
|
||||
"Current roster listing with member removal controls.",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
return PanelCard({
|
||||
className: "org-scroll-panel org-span-5",
|
||||
title: "Members",
|
||||
subtitle:
|
||||
"Current roster listing. The organization owner cannot be removed.",
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
{ className: "org-name-list" },
|
||||
...members.map((member) =>
|
||||
h(
|
||||
...members.map((member) => {
|
||||
const canRemoveMember =
|
||||
allowMemberManagement &&
|
||||
!actions.isOwnerMember(member.name);
|
||||
|
||||
return h(
|
||||
"article",
|
||||
{ className: "org-name-row" },
|
||||
h("strong", null, member.name),
|
||||
allowMemberManagement
|
||||
canRemoveMember
|
||||
? h(
|
||||
"button",
|
||||
{
|
||||
@ -67,9 +108,9 @@
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
77
arma/ui/apps/components/portal/metricCard.js
Normal file
77
arma/ui/apps/components/portal/metricCard.js
Normal file
@ -0,0 +1,77 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const scopeAttr = "data-ui-metric-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const metricCardCss = `
|
||||
${scopeSelector} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
padding: 1rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||
}
|
||||
|
||||
${scopeSelector}:nth-child(4n + 2),
|
||||
${scopeSelector}:nth-child(4n + 3) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%);
|
||||
border-color: rgb(100 116 139 / 0.35);
|
||||
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-metric-label {
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-metric-value {
|
||||
font-size: 1.8rem;
|
||||
color: var(--primary-hover);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
${scopeSelector}:nth-child(4n + 2) .org-metric-value,
|
||||
${scopeSelector}:nth-child(4n + 3) .org-metric-value {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-metric-note {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector}:nth-child(4n + 3) {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||
border-color: var(--border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
${scopeSelector}:nth-child(4n + 3) .org-metric-value {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.MetricCard = function MetricCard(
|
||||
label,
|
||||
value,
|
||||
note,
|
||||
) {
|
||||
ensureScopedStyle("portal-metric-card", metricCardCss);
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "org-metric-card", [scopeAttr]: "" },
|
||||
h("span", { className: "org-metric-label" }, label),
|
||||
h("strong", { className: "org-metric-value" }, value),
|
||||
h("span", { className: "org-metric-note" }, note),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -8,6 +8,7 @@
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.ModalLayer = function ModalLayer() {
|
||||
const Modal = window.SharedUI.componentFns.Modal;
|
||||
const modal = store.getModal();
|
||||
if (!modal) {
|
||||
return null;
|
||||
@ -24,7 +25,7 @@
|
||||
title = "Run Payroll";
|
||||
body = h(
|
||||
"div",
|
||||
{ className: "org-modal-form" },
|
||||
{ className: "app-modal-form" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
@ -39,7 +40,7 @@
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-modal-actions" },
|
||||
{ className: "app-modal-actions" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
@ -75,7 +76,7 @@
|
||||
title = "Send Funds";
|
||||
body = h(
|
||||
"div",
|
||||
{ className: "org-modal-form" },
|
||||
{ className: "app-modal-form" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
@ -104,7 +105,7 @@
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-modal-actions" },
|
||||
{ className: "app-modal-actions" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
@ -146,7 +147,7 @@
|
||||
title = "Assign Credit Line";
|
||||
body = h(
|
||||
"div",
|
||||
{ className: "org-modal-form" },
|
||||
{ className: "app-modal-form" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
@ -172,7 +173,7 @@
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-modal-actions" },
|
||||
{ className: "app-modal-actions" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
@ -214,7 +215,7 @@
|
||||
title = "Disband Organization";
|
||||
body = h(
|
||||
"div",
|
||||
{ className: "org-danger-confirm" },
|
||||
{ className: "app-modal-danger" },
|
||||
h(
|
||||
"p",
|
||||
null,
|
||||
@ -224,7 +225,7 @@
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-danger-actions" },
|
||||
{ className: "app-modal-danger-actions" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
@ -247,40 +248,10 @@
|
||||
);
|
||||
}
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
className: "org-modal-backdrop",
|
||||
onClick: (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
actions.closeModal();
|
||||
}
|
||||
},
|
||||
},
|
||||
h(
|
||||
"div",
|
||||
{ className: "card org-modal-card" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-modal-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h2", { className: "org-panel-title" }, title),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-modal-close",
|
||||
onClick: () => actions.closeModal(),
|
||||
"aria-label": "Close dialog",
|
||||
},
|
||||
"x",
|
||||
),
|
||||
),
|
||||
return Modal({
|
||||
title,
|
||||
body,
|
||||
),
|
||||
);
|
||||
onClose: () => actions.closeModal(),
|
||||
});
|
||||
};
|
||||
})();
|
||||
@ -1,35 +1,90 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h } = OrgPortal.runtime;
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const store = OrgPortal.store;
|
||||
const actions = OrgPortal.actions;
|
||||
const scopeAttr = "data-ui-overview-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const overviewCardCss = `
|
||||
${scopeSelector} .org-hero-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-summary {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-meta-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-meta-item:nth-child(even) {
|
||||
background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-meta-label {
|
||||
font-size: 0.76rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-meta-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-hero-grid,
|
||||
${scopeSelector} .org-meta-row,
|
||||
${scopeSelector} .org-metric-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.OverviewCard = function OverviewCard() {
|
||||
const MetricCard = OrgPortal.componentFns.MetricCard;
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
const readiness = actions.getAssetReadiness();
|
||||
const headquarters = portalData.org.headquarters || "ArmA Verse";
|
||||
ensureScopedStyle("portal-overview-card", overviewCardCss);
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: "card org-panel org-span-12" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-panel-head" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("div", { className: "org-eyebrow" }, portalData.org.tag),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "org-panel-title" },
|
||||
"Organization Overview",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
return PanelCard({
|
||||
className: "org-span-12",
|
||||
eyebrow: portalData.org.tag,
|
||||
title: "Organization Overview",
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
{ className: "org-hero-grid" },
|
||||
h(
|
||||
@ -115,6 +170,6 @@
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
})();
|
||||
39
arma/ui/apps/components/portal/simpleStat.js
Normal file
39
arma/ui/apps/components/portal/simpleStat.js
Normal file
@ -0,0 +1,39 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle } = OrgPortal.runtime;
|
||||
const scopeAttr = "data-ui-simple-stat";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const simpleStatCss = `
|
||||
${scopeSelector} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-label {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-simple-value {
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.SimpleStat = function SimpleStat(label, value) {
|
||||
ensureScopedStyle("portal-simple-stat", simpleStatCss);
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "org-simple-stat", [scopeAttr]: "" },
|
||||
h("span", { className: "org-simple-label" }, label),
|
||||
h("strong", { className: "org-simple-value" }, value),
|
||||
);
|
||||
};
|
||||
})();
|
||||
430
arma/ui/apps/components/portal/treasuryCard.js
Normal file
430
arma/ui/apps/components/portal/treasuryCard.js
Normal file
@ -0,0 +1,430 @@
|
||||
(function () {
|
||||
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
|
||||
const { h, ensureScopedStyle, createSignal } = OrgPortal.runtime;
|
||||
const { portalData } = OrgPortal.data;
|
||||
const store = OrgPortal.store;
|
||||
const permissions = OrgPortal.permissions;
|
||||
const actions = OrgPortal.actions;
|
||||
const scopeAttr = "data-ui-treasury-card";
|
||||
const scopeSelector = `[${scopeAttr}]`;
|
||||
const [getTreasuryTab, setTreasuryTab] = createSignal("overview");
|
||||
const [getTreasuryMenuOpen, setTreasuryMenuOpen] = createSignal(false);
|
||||
const treasuryCardCss = `
|
||||
${scopeSelector} .org-treasury-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-btn {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border);
|
||||
background: #f8fafc;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-btn:hover {
|
||||
color: var(--primary-hover);
|
||||
border-color: rgb(148 163 184 / 0.65);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-btn svg {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.6rem);
|
||||
right: 0;
|
||||
min-width: 10.5rem;
|
||||
padding: 0.45rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
box-shadow: 0 12px 28px rgb(15 23 42 / 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-option + .org-menu-option {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-option {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
background: transparent;
|
||||
color: var(--text-main);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-option:hover {
|
||||
background: #f8fafc;
|
||||
border-color: rgb(148 163 184 / 0.35);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-menu-option.is-active {
|
||||
background: rgb(226 232 240 / 0.7);
|
||||
color: var(--primary-hover);
|
||||
border-color: rgb(148 163 184 / 0.35);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-finance-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-finance-meta > div {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-meta-label {
|
||||
font-size: 0.76rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-action-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-action-grid button + button {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-action-grid button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-access-note {
|
||||
margin: 0 0 1rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-summary strong {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-summary span:last-child {
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-lines-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-line-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-line-row:nth-child(even) {
|
||||
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
|
||||
border-color: rgb(148 163 184 / 0.45);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-line-member {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-line-label {
|
||||
font-size: 0.76rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-line-empty {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
${scopeSelector} .org-finance-meta {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
${scopeSelector} .org-credit-line-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
OrgPortal.componentFns = OrgPortal.componentFns || {};
|
||||
|
||||
OrgPortal.componentFns.TreasuryCard = function TreasuryCard() {
|
||||
const PanelCard = window.SharedUI.componentFns.PanelCard;
|
||||
const creditLines = store.getCreditLines();
|
||||
const allowTreasuryActions = permissions.canManageTreasury();
|
||||
const activeTab = getTreasuryTab();
|
||||
const isMenuOpen = getTreasuryMenuOpen();
|
||||
const activeCreditLabel =
|
||||
creditLines.length === 1
|
||||
? "1 active credit line"
|
||||
: `${creditLines.length} active credit lines`;
|
||||
ensureScopedStyle("portal-treasury-card", treasuryCardCss);
|
||||
|
||||
return PanelCard({
|
||||
className: "org-span-5",
|
||||
title: "Treasury",
|
||||
subtitle: "Organization funds, reputation, and member payouts.",
|
||||
headerExtras: h(
|
||||
"div",
|
||||
{ className: "org-treasury-menu" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-menu-btn",
|
||||
title: "Treasury views",
|
||||
"aria-label": "Treasury views",
|
||||
onClick: () => setTreasuryMenuOpen((open) => !open),
|
||||
},
|
||||
h(
|
||||
"svg",
|
||||
{
|
||||
viewBox: "0 0 24 24",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round",
|
||||
"aria-hidden": "true",
|
||||
},
|
||||
h("line", { x1: "4", y1: "7", x2: "20", y2: "7" }),
|
||||
h("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
||||
h("line", { x1: "4", y1: "17", x2: "20", y2: "17" }),
|
||||
),
|
||||
),
|
||||
isMenuOpen
|
||||
? h(
|
||||
"div",
|
||||
{ className: "org-menu-dropdown" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className:
|
||||
activeTab === "overview"
|
||||
? "org-menu-option is-active"
|
||||
: "org-menu-option",
|
||||
onClick: () => {
|
||||
setTreasuryTab("overview");
|
||||
setTreasuryMenuOpen(false);
|
||||
},
|
||||
},
|
||||
"Overview",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className:
|
||||
activeTab === "credit"
|
||||
? "org-menu-option is-active"
|
||||
: "org-menu-option",
|
||||
onClick: () => {
|
||||
setTreasuryTab("credit");
|
||||
setTreasuryMenuOpen(false);
|
||||
},
|
||||
},
|
||||
"Credit Lines",
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
rootProps: { [scopeAttr]: "" },
|
||||
body: h(
|
||||
"div",
|
||||
null,
|
||||
activeTab === "credit"
|
||||
? creditLines.length > 0
|
||||
? h(
|
||||
"div",
|
||||
{ className: "org-credit-lines-list" },
|
||||
...creditLines.map((line) =>
|
||||
h(
|
||||
"article",
|
||||
{ className: "org-credit-line-row" },
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
"org-credit-line-member",
|
||||
},
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
className:
|
||||
"org-credit-line-label",
|
||||
},
|
||||
"Member",
|
||||
),
|
||||
h("strong", null, line.member),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
"org-credit-line-member",
|
||||
},
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
className:
|
||||
"org-credit-line-label",
|
||||
},
|
||||
"Amount",
|
||||
),
|
||||
h(
|
||||
"strong",
|
||||
null,
|
||||
actions.formatCurrency(
|
||||
line.amount,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: h(
|
||||
"div",
|
||||
{ className: "org-credit-line-empty" },
|
||||
"No active credit lines.",
|
||||
)
|
||||
: h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-finance-meta" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"span",
|
||||
{ className: "org-meta-label" },
|
||||
"Funds",
|
||||
),
|
||||
h(
|
||||
"strong",
|
||||
null,
|
||||
actions.formatCurrency(store.getFunds()),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"span",
|
||||
{ className: "org-meta-label" },
|
||||
"Reputation",
|
||||
),
|
||||
h("strong", null, `${portalData.reputation}`),
|
||||
),
|
||||
),
|
||||
allowTreasuryActions
|
||||
? h(
|
||||
"div",
|
||||
{ className: "org-action-grid" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
onClick: () =>
|
||||
actions.openModal("payroll"),
|
||||
},
|
||||
"Run Payroll",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-secondary-btn",
|
||||
onClick: () =>
|
||||
actions.openModal("transfer"),
|
||||
},
|
||||
"Send Funds",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "org-secondary-btn",
|
||||
onClick: () =>
|
||||
actions.openModal("credit"),
|
||||
},
|
||||
"Credit Line",
|
||||
),
|
||||
)
|
||||
: h(
|
||||
"p",
|
||||
{ className: "org-access-note" },
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "org-credit-summary" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "org-meta-label" },
|
||||
"Credit Line Status",
|
||||
),
|
||||
h("strong", null, activeCreditLabel),
|
||||
h(
|
||||
"span",
|
||||
null,
|
||||
creditLines.length > 0
|
||||
? "Open the Credit Lines tab to review assigned members and amounts."
|
||||
: "Assign a credit line to create the first approved member limit.",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
})();
|
||||
318
arma/ui/apps/logic/portalActions.js
Normal file
318
arma/ui/apps/logic/portalActions.js
Normal file
@ -0,0 +1,318 @@
|
||||
(function () {
|
||||
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
|
||||
|
||||
SharedLogic.createPortalActions = function createPortalActions({
|
||||
portalData,
|
||||
store,
|
||||
permissions,
|
||||
registryStore,
|
||||
}) {
|
||||
class OrgPortalActions {
|
||||
constructor() {
|
||||
this.treasuryNoticeTimer = null;
|
||||
}
|
||||
|
||||
formatCurrency(value) {
|
||||
return "$" + value.toLocaleString();
|
||||
}
|
||||
|
||||
formatVehicleType(type) {
|
||||
if (!type) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return type.charAt(0).toUpperCase() + type.slice(1);
|
||||
}
|
||||
|
||||
formatAssetType(type) {
|
||||
if (!type) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return type.charAt(0).toUpperCase() + type.slice(1);
|
||||
}
|
||||
|
||||
formatDisplayName(value) {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return String(value)
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map((part) => {
|
||||
if (!part) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return (
|
||||
part.charAt(0).toUpperCase() +
|
||||
part.slice(1).toLowerCase()
|
||||
);
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
getAssetReadiness() {
|
||||
if (portalData.fleet.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const total = portalData.fleet.reduce(
|
||||
(sum, unit) => sum + (100 - parseInt(unit.damage, 10)),
|
||||
0,
|
||||
);
|
||||
return Math.round(total / portalData.fleet.length);
|
||||
}
|
||||
|
||||
showTreasuryNotice(type, text) {
|
||||
store.setTreasuryNotice({ type, text });
|
||||
|
||||
if (this.treasuryNoticeTimer) {
|
||||
clearTimeout(this.treasuryNoticeTimer);
|
||||
}
|
||||
|
||||
this.treasuryNoticeTimer = setTimeout(() => {
|
||||
store.setTreasuryNotice({ type: "", text: "" });
|
||||
this.treasuryNoticeTimer = null;
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
parseAmount(value) {
|
||||
const amount = Number(value);
|
||||
return Number.isFinite(amount) ? Math.round(amount) : 0;
|
||||
}
|
||||
|
||||
getInputValue(id) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.value : "";
|
||||
}
|
||||
|
||||
isOwnerMember(memberName) {
|
||||
return (
|
||||
String(memberName || "")
|
||||
.trim()
|
||||
.toLowerCase() ===
|
||||
String(portalData.org.owner || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
closePortal() {
|
||||
if (
|
||||
typeof A3API !== "undefined" &&
|
||||
typeof A3API.SendAlert === "function"
|
||||
) {
|
||||
A3API.SendAlert(
|
||||
JSON.stringify({
|
||||
event: "org::close",
|
||||
data: {},
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (registryStore) {
|
||||
registryStore.setView("home");
|
||||
}
|
||||
}
|
||||
|
||||
openModal(type) {
|
||||
if (
|
||||
(type === "payroll" ||
|
||||
type === "transfer" ||
|
||||
type === "credit") &&
|
||||
!permissions.canManageTreasury()
|
||||
) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "disband" && !permissions.canDisbandOrg()) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.setModal({ type });
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
store.setModal(null);
|
||||
}
|
||||
|
||||
removeMember(memberName) {
|
||||
if (!permissions.canManageMembers()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isOwnerMember(memberName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setMembers((currentMembers) =>
|
||||
currentMembers.filter(
|
||||
(member) => member.name !== memberName,
|
||||
),
|
||||
);
|
||||
store.setCreditLines((currentLines) =>
|
||||
currentLines.filter((line) => line.member !== memberName),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
disbandOrganization() {
|
||||
if (!permissions.canDisbandOrg()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setOrgDisbanded(true);
|
||||
this.closeModal();
|
||||
return true;
|
||||
}
|
||||
|
||||
runPayroll(amountPerMember) {
|
||||
if (!permissions.canManageTreasury()) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const members = store.getMembers();
|
||||
const funds = store.getFunds();
|
||||
|
||||
if (members.length === 0) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"No members available for payroll.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amountPerMember <= 0) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Enter a valid payroll amount.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const total = amountPerMember * members.length;
|
||||
if (total > funds) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Insufficient org funds for payroll.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setFunds(funds - total);
|
||||
this.showTreasuryNotice(
|
||||
"success",
|
||||
`Payroll sent to ${members.length} members for ${this.formatCurrency(total)}.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
sendFundsToMember(memberName, amount) {
|
||||
if (!permissions.canManageTreasury()) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const funds = store.getFunds();
|
||||
|
||||
if (!memberName) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Select a member to receive funds.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Enter a valid transfer amount.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount > funds) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Insufficient org funds for this transfer.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setFunds(funds - amount);
|
||||
this.showTreasuryNotice(
|
||||
"success",
|
||||
`${this.formatCurrency(amount)} sent to ${memberName}.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
grantCreditLine(memberName, amount) {
|
||||
if (!permissions.canManageTreasury()) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Only the organization leader or CEO can manage treasury actions.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!memberName) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Select a member for the credit line.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
this.showTreasuryNotice(
|
||||
"error",
|
||||
"Enter a valid credit line amount.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
store.setCreditLines((currentLines) => {
|
||||
const existingIndex = currentLines.findIndex(
|
||||
(line) => line.member === memberName,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
return [
|
||||
...currentLines,
|
||||
{ member: memberName, amount },
|
||||
];
|
||||
}
|
||||
|
||||
const updatedLines = [...currentLines];
|
||||
updatedLines[existingIndex] = {
|
||||
member: memberName,
|
||||
amount,
|
||||
};
|
||||
return updatedLines;
|
||||
});
|
||||
|
||||
this.showTreasuryNotice(
|
||||
"success",
|
||||
`Credit line of ${this.formatCurrency(amount)} assigned to ${memberName}.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return new OrgPortalActions();
|
||||
};
|
||||
})();
|
||||
75
arma/ui/apps/logic/portalPermissions.js
Normal file
75
arma/ui/apps/logic/portalPermissions.js
Normal file
@ -0,0 +1,75 @@
|
||||
(function () {
|
||||
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
|
||||
|
||||
SharedLogic.createPortalPermissions = function createPortalPermissions({
|
||||
portalData,
|
||||
session,
|
||||
}) {
|
||||
class OrgPortalPermissions {
|
||||
getNormalizedRole() {
|
||||
return String(session.role || "")
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
isDefaultOrg() {
|
||||
return (
|
||||
portalData.org.isDefault === true ||
|
||||
String(portalData.org.tag || "")
|
||||
.trim()
|
||||
.toUpperCase() === "DEFAULT"
|
||||
);
|
||||
}
|
||||
|
||||
isOrgOwner() {
|
||||
const ownerUid = String(
|
||||
portalData.org.ownerUid || portalData.org.owner || "",
|
||||
)
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const actorUid = String(session.actorUid || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
if (ownerUid && actorUid) {
|
||||
return actorUid === ownerUid;
|
||||
}
|
||||
|
||||
return (
|
||||
String(session.actorName || "")
|
||||
.trim()
|
||||
.toLowerCase() ===
|
||||
String(portalData.org.owner || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
isSessionCeo() {
|
||||
return session.ceo === true;
|
||||
}
|
||||
|
||||
isOrgLeaderOrCeo() {
|
||||
return (
|
||||
this.isOrgOwner() ||
|
||||
this.getNormalizedRole() === "LEADER" ||
|
||||
(this.isDefaultOrg() && this.isSessionCeo())
|
||||
);
|
||||
}
|
||||
|
||||
canManageMembers() {
|
||||
return this.isOrgLeaderOrCeo();
|
||||
}
|
||||
|
||||
canManageTreasury() {
|
||||
return this.isOrgLeaderOrCeo();
|
||||
}
|
||||
|
||||
canDisbandOrg() {
|
||||
return this.isOrgLeaderOrCeo();
|
||||
}
|
||||
}
|
||||
|
||||
return new OrgPortalPermissions();
|
||||
};
|
||||
})();
|
||||
38
arma/ui/apps/logic/portalStore.js
Normal file
38
arma/ui/apps/logic/portalStore.js
Normal file
@ -0,0 +1,38 @@
|
||||
(function () {
|
||||
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
|
||||
|
||||
SharedLogic.createPortalStore = function createPortalStore({
|
||||
createSignal,
|
||||
portalData,
|
||||
}) {
|
||||
class OrgPortalStore {
|
||||
constructor() {
|
||||
[this.getFunds, this.setFunds] = createSignal(portalData.funds);
|
||||
[this.getMembers, this.setMembers] = createSignal([
|
||||
...portalData.members,
|
||||
]);
|
||||
[this.getCreditLines, this.setCreditLines] = createSignal([]);
|
||||
[this.getTreasuryNotice, this.setTreasuryNotice] = createSignal(
|
||||
{
|
||||
type: "",
|
||||
text: "",
|
||||
},
|
||||
);
|
||||
[this.getModal, this.setModal] = createSignal(null);
|
||||
[this.getOrgDisbanded, this.setOrgDisbanded] =
|
||||
createSignal(false);
|
||||
}
|
||||
|
||||
hydrateFromPayload(payload) {
|
||||
this.setFunds(payload.portalData.funds || 0);
|
||||
this.setMembers([...(payload.portalData.members || [])]);
|
||||
this.setCreditLines([]);
|
||||
this.setTreasuryNotice({ type: "", text: "" });
|
||||
this.setModal(null);
|
||||
this.setOrgDisbanded(false);
|
||||
}
|
||||
}
|
||||
|
||||
return new OrgPortalStore();
|
||||
};
|
||||
})();
|
||||
69
arma/ui/apps/logic/registryStore.js
Normal file
69
arma/ui/apps/logic/registryStore.js
Normal file
@ -0,0 +1,69 @@
|
||||
(function () {
|
||||
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
|
||||
|
||||
SharedLogic.createRegistryStore = function createRegistryStore({
|
||||
createSignal,
|
||||
onHydratePortal,
|
||||
}) {
|
||||
class RegistryStore {
|
||||
constructor() {
|
||||
[this.getView, this.setView] = createSignal("home");
|
||||
[this.getIsAuthenticating, this.setIsAuthenticating] =
|
||||
createSignal(false);
|
||||
[this.getLoginError, this.setLoginError] = createSignal("");
|
||||
[this.getIsCreating, this.setIsCreating] = createSignal(false);
|
||||
[this.getCreateError, this.setCreateError] = createSignal("");
|
||||
}
|
||||
|
||||
startLogin() {
|
||||
this.setLoginError("");
|
||||
this.setIsAuthenticating(true);
|
||||
}
|
||||
|
||||
startCreate() {
|
||||
this.setCreateError("");
|
||||
this.setIsCreating(true);
|
||||
}
|
||||
|
||||
failLogin(message) {
|
||||
this.setIsAuthenticating(false);
|
||||
this.setLoginError(message || "Authentication failed.");
|
||||
}
|
||||
|
||||
failCreate(message) {
|
||||
this.setIsCreating(false);
|
||||
this.setCreateError(
|
||||
message || "Organization registration failed.",
|
||||
);
|
||||
}
|
||||
|
||||
hydratePortal(payload) {
|
||||
return Boolean(onHydratePortal && onHydratePortal(payload));
|
||||
}
|
||||
|
||||
completeLogin(payload) {
|
||||
if (!this.hydratePortal(payload)) {
|
||||
this.failLogin("Login response was missing portal data.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setLoginError("");
|
||||
this.setIsAuthenticating(false);
|
||||
}
|
||||
|
||||
completeCreate(payload) {
|
||||
if (!this.hydratePortal(payload)) {
|
||||
this.failCreate(
|
||||
"Organization registration response was missing portal data.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setCreateError("");
|
||||
this.setIsCreating(false);
|
||||
}
|
||||
}
|
||||
|
||||
return new RegistryStore();
|
||||
};
|
||||
})();
|
||||
@ -36,12 +36,39 @@
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (!credentials.email || !credentials.password) {
|
||||
store.failLogin("Enter both email and password.");
|
||||
store.completeLogin(getMockPayload());
|
||||
}, 350);
|
||||
}
|
||||
|
||||
function requestCreateOrg(registration) {
|
||||
store.startCreate();
|
||||
|
||||
const sent = sendEvent("org::create::request", registration);
|
||||
if (sent) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.completeLogin(getMockPayload());
|
||||
window.setTimeout(() => {
|
||||
const orgName = String(registration.orgName || "").trim();
|
||||
if (!orgName) {
|
||||
store.failCreate("Enter an organization name.");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = getMockPayload();
|
||||
payload.portalData.org.name = orgName;
|
||||
payload.portalData.org.tag = String(Date.now()).slice(-10);
|
||||
payload.portalData.org.owner =
|
||||
payload.session.actorName || "Unknown";
|
||||
payload.portalData.org.ownerUid = payload.session.actorUid || "";
|
||||
payload.portalData.org.isDefault = false;
|
||||
payload.session.role = "Leader";
|
||||
payload.session.ceo = false;
|
||||
payload.portalData.members = [
|
||||
{ name: payload.session.actorName || "Unknown" },
|
||||
];
|
||||
|
||||
store.completeCreate(payload);
|
||||
}, 350);
|
||||
}
|
||||
|
||||
@ -64,18 +91,33 @@
|
||||
store.failLogin(payloadData.message || "Authentication failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === "org::create::success") {
|
||||
store.completeCreate(payloadData);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === "org::create::failure") {
|
||||
store.failCreate(
|
||||
payloadData.message || "Organization registration failed.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RegistryApp.bridge = {
|
||||
requestLogin,
|
||||
requestCreateOrg,
|
||||
receive,
|
||||
sendEvent,
|
||||
};
|
||||
|
||||
window.OrgUIBridge = {
|
||||
requestLogin,
|
||||
requestCreateOrg,
|
||||
receive,
|
||||
receiveLoginSuccess: (data) => receive("org::login::success", data),
|
||||
receiveLoginFailure: (data) => receive("org::login::failure", data),
|
||||
receiveCreateSuccess: (data) => receive("org::create::success", data),
|
||||
receiveCreateFailure: (data) => receive("org::create::failure", data),
|
||||
};
|
||||
})();
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
(function () {
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
|
||||
RegistryApp.componentFns = RegistryApp.componentFns || {};
|
||||
|
||||
RegistryApp.componentFns.Footer = function Footer() {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "footer" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "wrapper" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h3", null, "Registry Resources"),
|
||||
h(
|
||||
"ul",
|
||||
{ style: { listStyleType: "none", padding: 0 } },
|
||||
h("li", null, "Registration Guidelines"),
|
||||
h("li", null, "Tax & Fee Schedule"),
|
||||
h("li", null, "Legal Compliance"),
|
||||
h("li", null, "Trademark Database"),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("h3", null, "Bureau Support"),
|
||||
h(
|
||||
"ul",
|
||||
{ style: { listStyleType: "none", padding: 0 } },
|
||||
h("li", null, "Office: Sector 7 Admin Block"),
|
||||
h("li", null, "Hours: 0800 - 1600 (GST)"),
|
||||
h("li", null, "Helpdesk: 555-01-REGISTRY"),
|
||||
h("li", null, "support@org-bureau.gov"),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,84 +0,0 @@
|
||||
.split-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
text-align: left;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.app-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: left;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cancel-link {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.form-feedback {
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.92rem;
|
||||
|
||||
&.is-error {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.split-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
(function () {
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
const store = RegistryApp.store;
|
||||
|
||||
RegistryApp.componentFns = RegistryApp.componentFns || {};
|
||||
|
||||
RegistryApp.componentFns.Header = function Header({ title }) {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "header" },
|
||||
h(
|
||||
"h1",
|
||||
{
|
||||
style: { cursor: "pointer" },
|
||||
onClick: () => store.setView("home"),
|
||||
},
|
||||
title,
|
||||
),
|
||||
h("p", null, "Organization Registration & Management Portal"),
|
||||
);
|
||||
};
|
||||
})();
|
||||
@ -1,12 +0,0 @@
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
(function () {
|
||||
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
|
||||
const { h } = RegistryApp.runtime;
|
||||
const store = RegistryApp.store;
|
||||
|
||||
RegistryApp.componentFns = RegistryApp.componentFns || {};
|
||||
|
||||
RegistryApp.componentFns.HomeView = function HomeView() {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "content" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "card" },
|
||||
h("h2", null, "Create Organization"),
|
||||
h(
|
||||
"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.",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{ onClick: () => store.setView("create") },
|
||||
"Register",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "card" },
|
||||
h("h2", null, "Organization Portal"),
|
||||
h(
|
||||
"p",
|
||||
null,
|
||||
"Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink.",
|
||||
),
|
||||
h("button", { onClick: () => store.setView("login") }, "Login"),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "card", style: { gridColumn: "span 2" } },
|
||||
h("h2", null, "Organization Portal Preview"),
|
||||
h(
|
||||
"p",
|
||||
null,
|
||||
"Review the refactor direction for a player organization portal with fleet, assets, treasury, reputation, roster management, and reserved space for future modules.",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
onClick: () => store.setView("portal"),
|
||||
},
|
||||
"Open Portal Preview",
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user