diff --git a/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf b/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf index 8a10c41..bfe05a9 100644 --- a/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf @@ -32,9 +32,23 @@ diag_log format ["[FORGE:Client:CAD] Handling UI event: %1", _event]; if (_isConfirmDialog) exitWith { true }; switch (_event) do { + case "cad::topbar::ready": { + GVAR(CADUIBridge) call ["handleTopBarReady", []]; + }; case "cad::ready": { GVAR(CADUIBridge) call ["handleReady", [_control, _data]]; }; + case "cad::dispatcher::ready": { + GVAR(CADUIBridge) call ["handleDispatcherReady", []]; + }; + case "cad::mode::set": { + private _mode = ""; + if (_data isEqualType createHashMap) then { + _mode = _data getOrDefault ["mode", ""]; + }; + + GVAR(CADUIBridge) call ["setMode", [_mode]]; + }; case "cad::refresh": { GVAR(CADUIBridge) call ["requestHydrate", []]; }; @@ -76,6 +90,16 @@ switch (_event) do { GVAR(CADUIBridge) call ["requestGroupStatus", [_groupID, _status]]; }; + case "cad::groups::role": { + private _groupID = ""; + private _role = ""; + if (_data isEqualType createHashMap) then { + _groupID = _data getOrDefault ["groupID", ""]; + _role = _data getOrDefault ["role", ""]; + }; + + GVAR(CADUIBridge) call ["requestGroupRole", [_groupID, _role]]; + }; case "map::zoomIn": { private _mapCtrl = uiNamespace getVariable [QGVAR(MapCtrl), controlNull]; if (isNull _mapCtrl) exitWith {}; diff --git a/arma/client/addons/cad/functions/fnc_initRepository.sqf b/arma/client/addons/cad/functions/fnc_initRepository.sqf index ba109da..749e691 100644 --- a/arma/client/addons/cad/functions/fnc_initRepository.sqf +++ b/arma/client/addons/cad/functions/fnc_initRepository.sqf @@ -30,19 +30,35 @@ GVAR(CADRepository) = createHashMapObject [[ _self set ["assignments", []]; _self set ["activity", []]; _self set ["session", createHashMap]; + _self set ["mode", "operations"]; + }], + ["getHydratePayload", compileFinal { + createHashMapFromArray [ + ["groups", +(_self getOrDefault ["groups", []])], + ["contracts", +(_self getOrDefault ["contracts", []])], + ["assignments", +(_self getOrDefault ["assignments", []])], + ["activity", +(_self getOrDefault ["activity", []])], + ["session", +(_self getOrDefault ["session", createHashMap])], + ["mode", _self getOrDefault ["mode", "operations"]] + ] + }], + ["getCurrentGroup", compileFinal { + private _session = _self getOrDefault ["session", createHashMap]; + private _groupID = _session getOrDefault ["groupId", ""]; + if (_groupID isEqualTo "") exitWith { createHashMap }; + + private _groups = _self getOrDefault ["groups", []]; + private _group = _groups findIf { (_x getOrDefault ["groupId", ""]) isEqualTo _groupID }; + if (_group < 0) exitWith { createHashMap }; + + +(_groups # _group) }], ["pushHydratePayload", compileFinal { params [["_bridge", createHashMap, [createHashMap]]]; if (_bridge isEqualTo createHashMap) exitWith { false }; - _bridge call ["sendEvent", ["cad::hydrate", createHashMapFromArray [ - ["groups", +(_self getOrDefault ["groups", []])], - ["contracts", +(_self getOrDefault ["contracts", []])], - ["assignments", +(_self getOrDefault ["assignments", []])], - ["activity", +(_self getOrDefault ["activity", []])], - ["session", +(_self getOrDefault ["session", createHashMap])] - ]]] + _bridge call ["sendEvent", ["cad::hydrate", _self call ["getHydratePayload", []]]] }], ["setHydratePayload", compileFinal { params [["_payload", createHashMap, [createHashMap]]]; @@ -54,6 +70,16 @@ GVAR(CADRepository) = createHashMapObject [[ _self set ["session", +(_payload getOrDefault ["session", createHashMap])]; true }], + ["setMode", compileFinal { + params [["_mode", "operations", [""]]]; + + if !(_mode in ["operations", "dispatch"]) then { + _mode = "operations"; + }; + + _self set ["mode", _mode]; + _mode + }], ["setOpen", compileFinal { params [["_isOpen", false, [false]]]; _self set ["isOpen", _isOpen]; diff --git a/arma/client/addons/cad/functions/fnc_initUI.sqf b/arma/client/addons/cad/functions/fnc_initUI.sqf index bb84979..981d317 100644 --- a/arma/client/addons/cad/functions/fnc_initUI.sqf +++ b/arma/client/addons/cad/functions/fnc_initUI.sqf @@ -27,12 +27,16 @@ private _mapCtrl = _display displayCtrl 1001; private _topBarCtrl = _display displayCtrl 1002; private _bottomBarCtrl = _display displayCtrl 1003; private _sidePanelCtrl = _display displayCtrl 1005; +private _dispatcherCtrl = _display displayCtrl 1006; uiNamespace setVariable [QGVAR(Display), _display]; uiNamespace setVariable [QGVAR(MapCtrl), _mapCtrl]; uiNamespace setVariable [QGVAR(TopBarCtrl), _topBarCtrl]; uiNamespace setVariable [QGVAR(BottomBarCtrl), _bottomBarCtrl]; uiNamespace setVariable [QGVAR(SidePanelCtrl), _sidePanelCtrl]; +uiNamespace setVariable [QGVAR(DispatcherCtrl), _dispatcherCtrl]; + +_dispatcherCtrl ctrlShow false; private _center = if (isNull player) then { [worldSize / 2, worldSize / 2, 0] @@ -43,48 +47,5 @@ private _center = if (isNull player) then { _mapCtrl ctrlMapAnimAdd [0, 0.2, _center]; ctrlMapAnimCommit _mapCtrl; -_mapCtrl ctrlAddEventHandler ["MouseButtonClick", { - params ["_ctrl", "_button", "_xPos", "_yPos"]; - - private _worldPos = _ctrl ctrlMapScreenToWorld [_xPos, _yPos]; - private _bottomBar = uiNamespace getVariable [QGVAR(BottomBarCtrl), controlNull]; - if (isNull _bottomBar) exitWith {}; - - private _jsCode = format [ - "updateStatus('Clicked at: %1, %2');", - round (_worldPos # 0), - round (_worldPos # 1) - ]; - _bottomBar ctrlWebBrowserAction ["ExecJS", _jsCode]; -}]; - -_mapCtrl ctrlAddEventHandler ["MouseMoving", { - params ["_ctrl", "_xPos", "_yPos"]; - - private _worldPos = _ctrl ctrlMapScreenToWorld [_xPos, _yPos]; - private _topBar = uiNamespace getVariable [QGVAR(TopBarCtrl), controlNull]; - if (isNull _topBar) exitWith {}; - - private _jsCode = format [ - "updateCoordinates(%1, %2);", - _worldPos # 0, - _worldPos # 1 - ]; - _topBar ctrlWebBrowserAction ["ExecJS", _jsCode]; -}]; - -[] spawn { - while { !isNull (uiNamespace getVariable [QGVAR(Display), displayNull]) } do { - private _mapCtrl = uiNamespace getVariable [QGVAR(MapCtrl), controlNull]; - private _topBar = uiNamespace getVariable [QGVAR(TopBarCtrl), controlNull]; - - if (!isNull _mapCtrl && { !isNull _topBar }) then { - _topBar ctrlWebBrowserAction ["ExecJS", format ["updateScale(%1);", round (ctrlMapScale _mapCtrl)]]; - }; - - sleep 0.5; - }; -}; - diag_log "[FORGE:Client:CAD] CAD UI initialized."; true diff --git a/arma/client/addons/cad/functions/fnc_initUIBridge.sqf b/arma/client/addons/cad/functions/fnc_initUIBridge.sqf index ab4cb86..0fb004b 100644 --- a/arma/client/addons/cad/functions/fnc_initUIBridge.sqf +++ b/arma/client/addons/cad/functions/fnc_initUIBridge.sqf @@ -26,6 +26,10 @@ private _webUIBridgeDeclaration = _webUIDeclarations get "bridgeDeclaration"; GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [ ["#base", _webUIBridgeDeclaration], ["#type", "CADUIBridgeBaseClass"], + ["#create", compileFinal { + _self set ["dispatcherReady", false]; + _self set ["topBarReady", false]; + }], ["getActiveBrowserControl", compileFinal { private _display = uiNamespace getVariable [QGVAR(Display), displayNull]; if (isNull _display) exitWith { @@ -37,11 +41,115 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [ _self call ["setActiveBrowserControl", [_control]]; _control }], + ["getTopBarControl", compileFinal { + private _display = uiNamespace getVariable [QGVAR(Display), displayNull]; + if (isNull _display) exitWith { controlNull }; + + _display displayCtrl 1002 + }], + ["getBottomBarControl", compileFinal { + private _display = uiNamespace getVariable [QGVAR(Display), displayNull]; + if (isNull _display) exitWith { controlNull }; + + _display displayCtrl 1003 + }], + ["getMapControl", compileFinal { + private _display = uiNamespace getVariable [QGVAR(Display), displayNull]; + if (isNull _display) exitWith { controlNull }; + + _display displayCtrl 1001 + }], + ["getDispatcherControl", compileFinal { + private _display = uiNamespace getVariable [QGVAR(Display), displayNull]; + if (isNull _display) exitWith { controlNull }; + + _display displayCtrl 1006 + }], ["hasOpenScreen", compileFinal { private _screen = _self call ["getScreen", []]; private _control = _self call ["getActiveBrowserControl", []]; !(isNull _control) && { _screen call ["isReady", []] } }], + ["isDispatcher", compileFinal { + if (isNil QGVAR(CADRepository)) exitWith { false }; + + private _session = GVAR(CADRepository) getOrDefault ["session", createHashMap]; + _session getOrDefault ["isDispatcher", false] + }], + ["applyLayout", compileFinal { + private _mode = if (isNil QGVAR(CADRepository)) then { + "operations" + } else { + GVAR(CADRepository) getOrDefault ["mode", "operations"] + }; + + private _mapCtrl = _self call ["getMapControl", []]; + private _bottomBarCtrl = _self call ["getBottomBarControl", []]; + private _sidePanelCtrl = _self call ["getActiveBrowserControl", []]; + private _dispatcherCtrl = _self call ["getDispatcherControl", []]; + + if !(isNull _mapCtrl) then { _mapCtrl ctrlShow (_mode isEqualTo "operations"); }; + if !(isNull _bottomBarCtrl) then { _bottomBarCtrl ctrlShow true; }; + if !(isNull _sidePanelCtrl) then { _sidePanelCtrl ctrlShow (_mode isEqualTo "operations"); }; + if !(isNull _dispatcherCtrl) then { _dispatcherCtrl ctrlShow (_mode isEqualTo "dispatch"); }; + + _self call ["refreshTopBarState", []]; + _self call ["refreshDispatcher", []]; + true + }], + ["setMode", compileFinal { + params [["_mode", "operations", [""]]]; + + if (isNil QGVAR(CADRepository)) exitWith { false }; + + private _targetMode = _mode; + if !(_targetMode in ["operations", "dispatch"]) then { + _targetMode = "operations"; + }; + + if (_targetMode isEqualTo "dispatch" && !(_self call ["isDispatcher", []])) then { + _targetMode = "operations"; + }; + + GVAR(CADRepository) call ["setMode", [_targetMode]]; + _self call ["applyLayout", []] + }], + ["refreshTopBarState", compileFinal { + if !(_self getOrDefault ["topBarReady", false]) exitWith { false }; + + if (isNil QGVAR(CADRepository)) exitWith { false }; + + private _topBarCtrl = _self call ["getTopBarControl", []]; + if (isNull _topBarCtrl) exitWith { false }; + + private _session = +(GVAR(CADRepository) getOrDefault ["session", createHashMap]); + private _currentGroup = GVAR(CADRepository) call ["getCurrentGroup", []]; + private _payload = createHashMapFromArray [ + ["mode", GVAR(CADRepository) getOrDefault ["mode", "operations"]], + ["session", _session], + ["currentGroup", _currentGroup] + ]; + + _topBarCtrl ctrlWebBrowserAction ["ExecJS", format [ + "window.cadTopbar && window.cadTopbar.receiveState(%1);", + toJSON _payload + ]]; + true + }], + ["refreshDispatcher", compileFinal { + if !(_self getOrDefault ["dispatcherReady", false]) exitWith { false }; + if (isNil QGVAR(CADRepository)) exitWith { false }; + + private _dispatcherCtrl = _self call ["getDispatcherControl", []]; + if (isNull _dispatcherCtrl) exitWith { false }; + + private _payload = GVAR(CADRepository) call ["getHydratePayload", []]; + _dispatcherCtrl ctrlWebBrowserAction ["ExecJS", format [ + "window.cadDispatcher && window.cadDispatcher.receiveHydrate(%1);", + toJSON _payload + ]]; + true + }], ["handleReady", compileFinal { params [["_control", controlNull, [controlNull]], ["_data", createHashMap, [createHashMap]]]; @@ -52,8 +160,25 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [ _self call ["requestHydrate", []]; _self call ["refreshHydrate", []]; + _self call ["refreshTopBarState", []]; true }], + ["handleClose", compileFinal { + _self set ["dispatcherReady", false]; + _self set ["topBarReady", false]; + + private _screen = _self call ["getScreen", []]; + _screen call ["dispose", []]; + true + }], + ["handleTopBarReady", compileFinal { + _self set ["topBarReady", true]; + _self call ["refreshTopBarState", []] + }], + ["handleDispatcherReady", compileFinal { + _self set ["dispatcherReady", true]; + _self call ["refreshDispatcher", []] + }], ["requestHydrate", compileFinal { [SRPC(cad,requestHydrateCad), [getPlayerUID player]] call CFUNC(serverEvent); true @@ -90,6 +215,14 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [ [SRPC(cad,requestUpdateCadGroupStatus), [getPlayerUID player, _groupID, _status]] call CFUNC(serverEvent); true }], + ["requestGroupRole", compileFinal { + params [["_groupID", "", [""]], ["_role", "", [""]]]; + + if (_groupID isEqualTo "" || { _role isEqualTo "" }) exitWith { false }; + + [SRPC(cad,requestUpdateCadGroupRole), [getPlayerUID player, _groupID, _role]] call CFUNC(serverEvent); + true + }], ["refreshHydrate", compileFinal { if (isNil QGVAR(CADRepository)) exitWith { false }; GVAR(CADRepository) call ["pushHydratePayload", [_self]] @@ -100,11 +233,29 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [ if (isNil QGVAR(CADRepository)) exitWith { false }; GVAR(CADRepository) call ["setHydratePayload", [_payload]]; - _self call ["refreshHydrate", []] + if !(_self call ["isDispatcher", []]) then { + GVAR(CADRepository) call ["setMode", ["operations"]]; + }; + + _self call ["refreshHydrate", []]; + _self call ["refreshTopBarState", []]; + _self call ["refreshDispatcher", []]; + _self call ["applyLayout", []] }], ["handleAssignmentResponse", compileFinal { params [["_result", createHashMap, [createHashMap]]]; + if (_self getOrDefault ["dispatcherReady", false]) then { + private _dispatcherCtrl = _self call ["getDispatcherControl", []]; + if !(isNull _dispatcherCtrl) then { + _dispatcherCtrl ctrlWebBrowserAction ["ExecJS", format [ + "window.cadDispatcher && window.cadDispatcher.setStatus(%1, %2);", + str (_result getOrDefault ["message", "Task request processed."]), + str ([ "error", "success" ] select (_result getOrDefault ["success", false])) + ]]; + }; + }; + _self call ["sendEvent", ["cad::assignment::response", createHashMapFromArray [ ["message", _result getOrDefault ["message", "Task request processed."]], ["success", _result getOrDefault ["success", false]] @@ -113,6 +264,17 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [ ["handleGroupUpdateResponse", compileFinal { params [["_result", createHashMap, [createHashMap]]]; + if (_self getOrDefault ["dispatcherReady", false]) then { + private _dispatcherCtrl = _self call ["getDispatcherControl", []]; + if !(isNull _dispatcherCtrl) then { + _dispatcherCtrl ctrlWebBrowserAction ["ExecJS", format [ + "window.cadDispatcher && window.cadDispatcher.setStatus(%1, %2);", + str (_result getOrDefault ["message", "Group update processed."]), + str ([ "error", "success" ] select (_result getOrDefault ["success", false])) + ]]; + }; + }; + _self call ["sendEvent", ["cad::group::response", createHashMapFromArray [ ["message", _result getOrDefault ["message", "Group update processed."]], ["success", _result getOrDefault ["success", false]] diff --git a/arma/client/addons/cad/functions/fnc_openUI.sqf b/arma/client/addons/cad/functions/fnc_openUI.sqf index 0d0804b..d648613 100644 --- a/arma/client/addons/cad/functions/fnc_openUI.sqf +++ b/arma/client/addons/cad/functions/fnc_openUI.sqf @@ -28,17 +28,19 @@ if (isNull _display) exitWith { private _topBarCtrl = _display displayCtrl 1002; private _bottomBarCtrl = _display displayCtrl 1003; private _sidePanelCtrl = _display displayCtrl 1005; +private _dispatcherCtrl = _display displayCtrl 1006; { _x ctrlAddEventHandler ["JSDialog", { params ["_control", "_isConfirmDialog", "_message"]; [_control, _isConfirmDialog, _message] call FUNC(handleUIEvents); }]; -} forEach [_topBarCtrl, _bottomBarCtrl, _sidePanelCtrl]; +} forEach [_topBarCtrl, _bottomBarCtrl, _sidePanelCtrl, _dispatcherCtrl]; _topBarCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\topbar.html)]; _bottomBarCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\bottombar.html)]; _sidePanelCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\sidepanel.html)]; +_dispatcherCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\dispatcher.html)]; if !(isNil QGVAR(CADRepository)) then { GVAR(CADRepository) call ["setOpen", [true]]; diff --git a/arma/client/addons/cad/ui/RscMapUI.hpp b/arma/client/addons/cad/ui/RscMapUI.hpp index 37f599d..b323f68 100644 --- a/arma/client/addons/cad/ui/RscMapUI.hpp +++ b/arma/client/addons/cad/ui/RscMapUI.hpp @@ -6,15 +6,24 @@ class RscMapUI { fadeout = 0; duration = 1e+011; onLoad = "uiNamespace setVariable ['forge_client_cad_Display', _this select 0]; [_this select 0] call forge_client_cad_fnc_initUI;"; - onUnLoad = "uiNamespace setVariable ['forge_client_cad_Display', nil]; uiNamespace setVariable ['forge_client_cad_MapCtrl', nil]; uiNamespace setVariable ['forge_client_cad_TopBarCtrl', nil]; uiNamespace setVariable ['forge_client_cad_BottomBarCtrl', nil]; uiNamespace setVariable ['forge_client_cad_SidePanelCtrl', nil]; if !(isNil 'forge_client_cad_CADRepository') then { forge_client_cad_CADRepository set ['isOpen', false]; };"; + onUnLoad = "uiNamespace setVariable ['forge_client_cad_Display', nil]; uiNamespace setVariable ['forge_client_cad_MapCtrl', nil]; uiNamespace setVariable ['forge_client_cad_TopBarCtrl', nil]; uiNamespace setVariable ['forge_client_cad_BottomBarCtrl', nil]; uiNamespace setVariable ['forge_client_cad_SidePanelCtrl', nil]; uiNamespace setVariable ['forge_client_cad_DispatcherCtrl', nil]; if !(isNil 'forge_client_cad_CADRepository') then { forge_client_cad_CADRepository set ['isOpen', false]; };"; class controlsBackground { + class SurfaceBackground: RscText { + idc = -1; + x = "safeZoneX + (safeZoneW * 0.1)"; + y = "safeZoneY + (safeZoneH * 0.1)"; + w = "safeZoneW * 0.8"; + h = "safeZoneH * 0.8"; + colorBackground[] = {0.04, 0.06, 0.09, 0.96}; + }; + class MapControl: RscMapControl { idc = 1001; x = "safeZoneX + (safeZoneW * 0.1)"; // 10% margin (80% width centered) - y = "safeZoneY + (safeZoneH * 0.1) + 0.0926"; // 10% margin + 50px top bar + y = "safeZoneY + (safeZoneH * 0.1) + 0.10372"; // 10% margin + 56px visible top bar w = "safeZoneW * 0.8"; // 80% width - h = "(safeZoneH * 0.8) - 0.0926 - 0.0556"; // 80% height minus top and bottom bars + h = "(safeZoneH * 0.8) - 0.10372 - 0.0556"; // 80% height minus visible top and bottom bars // Map specific settings maxSatelliteAlpha = 0.85; @@ -61,7 +70,7 @@ class RscMapUI { x = "safeZoneX + (safeZoneW * 0.1)"; y = "safeZoneY + (safeZoneH * 0.1)"; w = "safeZoneW * 0.8"; - h = "0.0926"; // 50px + h = "0.24076"; // 130px, allows dropdowns to open over the map colorBackground[] = {0, 0, 0, 0}; }; @@ -81,9 +90,19 @@ class RscMapUI { type = 106; idc = 1005; x = "safeZoneX + (safeZoneW * 0.1) + (safeZoneW * 0.8) - 0.4630"; // Right edge of 80% box minus panel width - y = "safeZoneY + (safeZoneH * 0.1) + 0.0926"; // Below top bar + y = "safeZoneY + (safeZoneH * 0.1) + 0.10372"; // Below visible top bar w = "0.4630"; // ~250px width - h = "(safeZoneH * 0.8) - 0.0926 - 0.0556"; // Full height minus bars + h = "(safeZoneH * 0.8) - 0.10372 - 0.0556"; // Full height minus visible bars + colorBackground[] = {0, 0, 0, 0}; + }; + + class DispatcherBrowser: RscText { + type = 106; + idc = 1006; + x = "safeZoneX + (safeZoneW * 0.1)"; + y = "safeZoneY + (safeZoneH * 0.1) + 0.10372"; + w = "safeZoneW * 0.8"; + h = "(safeZoneH * 0.8) - 0.10372 - 0.0556"; colorBackground[] = {0, 0, 0, 0}; }; }; diff --git a/arma/client/addons/cad/ui/_site/bottombar.html b/arma/client/addons/cad/ui/_site/bottombar.html index 33fb1ec..57d4b5e 100644 --- a/arma/client/addons/cad/ui/_site/bottombar.html +++ b/arma/client/addons/cad/ui/_site/bottombar.html @@ -1 +1 @@ -Map Ready \ No newline at end of file +CAD Systems by IDS v1.0.0 \ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/cad-bottombar.css b/arma/client/addons/cad/ui/_site/cad-bottombar.css index 7133cd2..d6213e6 100644 --- a/arma/client/addons/cad/ui/_site/cad-bottombar.css +++ b/arma/client/addons/cad/ui/_site/cad-bottombar.css @@ -1 +1 @@ -body{-webkit-backdrop-filter:blur(18px);background:linear-gradient(90deg,#0e131bf5,#121720ed 55%,#0d1219f5);border-top:1px solid #ffffff24;justify-content:space-between;align-items:center;min-height:36px;padding:0 20px;display:flex;position:absolute;bottom:0;left:0;right:0;overflow:hidden;box-shadow:0 -12px 26px #0000003d}span{color:#f5f8ffcc;text-shadow:0 1px 10px #00000047;font-size:12px}#statusText{color:var(--accent);font-weight:600} \ No newline at end of file +body{-webkit-backdrop-filter:blur(18px);background:linear-gradient(90deg,#0e131bf5,#121720ed 55%,#0d1219f5);border-top:1px solid #ffffff24;justify-content:space-between;align-items:center;min-height:36px;padding:0 20px;display:flex;position:absolute;bottom:0;left:0;right:0;overflow:hidden;box-shadow:0 -12px 26px #0000003d}.footer-brand,.footer-version{color:#f5f8ffcc;text-shadow:0 1px 10px #00000047;font-size:12px}.footer-brand{color:var(--accent);letter-spacing:.08em;text-transform:uppercase;font-weight:600}.footer-version{color:#f5f8ff9e} \ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/cad-bottombar.js b/arma/client/addons/cad/ui/_site/cad-bottombar.js index d39ab4b..7710154 100644 --- a/arma/client/addons/cad/ui/_site/cad-bottombar.js +++ b/arma/client/addons/cad/ui/_site/cad-bottombar.js @@ -1 +1 @@ -window.CADBottombar=window.CADBottombar||{}; \ No newline at end of file +window.CADBottombar=window.CADBottombar||{init:()=>!0},window.CADBottombar.init(); \ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/cad-dispatcher.css b/arma/client/addons/cad/ui/_site/cad-dispatcher.css new file mode 100644 index 0000000..e658608 --- /dev/null +++ b/arma/client/addons/cad/ui/_site/cad-dispatcher.css @@ -0,0 +1 @@ +html,body{background:radial-gradient(circle at 0 0,#29455d2e,#0000 30%),linear-gradient(#090e14f5,#0f161ffa);width:100%;height:100%;margin:0;padding:0;overflow:hidden}body{color:var(--text);font-family:var(--font)}.dispatch-shell{flex-direction:column;gap:14px;height:100%;padding:18px;display:flex}.dispatch-header{justify-content:space-between;align-items:center;gap:16px;display:flex}.dispatch-kicker{color:var(--accent);text-transform:uppercase;letter-spacing:.1em;margin:0 0 4px;font-size:11px;font-weight:700}.dispatch-header h2{margin:0;font-size:24px;font-weight:650}.dispatch-header button,.dispatch-btn,.dispatch-select{color:var(--text);background:#181f28e6;border:1px solid #ffffff1f}.dispatch-header button,.dispatch-btn{cursor:pointer;padding:10px 14px}.dispatch-btn-secondary{background:#352827eb}.dispatch-status{color:#e9f1f8c7;min-height:20px;font-size:13px}.dispatch-status[data-type=success]{color:#79d28a}.dispatch-status[data-type=error]{color:#ff8a80}.dispatch-metrics{grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;display:grid}.metric-card{background:#0d131ab8;border:1px solid #ffffff14;padding:14px}.metric-label{color:#e9f1f899;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;font-size:11px;display:block}.metric-card strong{font-size:28px;font-weight:700}.dispatch-grid{flex:1;grid-template-columns:repeat(12,minmax(0,1fr));grid-auto-rows:minmax(0,1fr);gap:14px;min-height:0;display:grid}.dispatch-panel{background:#0b1118c7;border:1px solid #ffffff14;flex-direction:column;min-width:0;min-height:0;display:flex}.dispatch-panel-open{grid-column:span 5}.dispatch-panel-assigned{grid-column:span 7}.dispatch-panel-groups{grid-column:span 8}.dispatch-panel-activity{grid-column:span 4}.dispatch-panel-header{border-bottom:1px solid #ffffff14;justify-content:space-between;align-items:center;padding:12px 14px;display:flex}.dispatch-panel-header h3{text-transform:uppercase;letter-spacing:.08em;color:var(--accent);margin:0;font-size:13px}.dispatch-list{flex-direction:column;flex:1;gap:10px;padding:12px;display:flex;overflow:auto}.dispatch-card{background:#131a22b8;border:1px solid #ffffff0f;padding:12px}.dispatch-card-header,.dispatch-meta{justify-content:space-between;gap:10px;display:flex}.dispatch-card-header-actions{align-items:center;gap:8px;display:flex}.dispatch-card-header-main{align-items:center;gap:8px;min-width:0;display:flex}.dispatch-card-header{margin-bottom:8px}.dispatch-description{color:#f1f6fbd1;margin:0 0 10px;font-size:13px;line-height:1.45}.dispatch-meta{color:#e5edf4b3;margin-bottom:10px;font-size:12px}.dispatch-badge{color:var(--accent);text-transform:uppercase;background:#102b3db3;border:1px solid #5bbbff2e;padding:3px 7px;font-size:11px}.dispatch-icon-btn{width:32px;height:32px;color:var(--text);cursor:pointer;background:#181f28eb;border:1px solid #ffffff24;padding:0}.dispatch-icon-btn:hover{background:#202a34f5}.dispatch-actions{flex-direction:column;gap:8px;display:flex}.dispatch-actions-split{margin-top:10px}.dispatch-select{width:100%;padding:9px 10px}.placeholder-message{text-align:center;color:#e9f1f899;padding:18px}.dispatch-modal{z-index:30;position:fixed;inset:0}.dispatch-modal.is-hidden{display:none}.dispatch-modal-backdrop{background:#04080cb8;position:absolute;inset:0}.dispatch-modal-dialog{background:#0b1118fa;border:1px solid #ffffff1f;width:min(480px,100% - 48px);margin:72px auto 0;position:relative;box-shadow:0 24px 64px #0000006b}.dispatch-modal-header,.dispatch-modal-actions{justify-content:space-between;align-items:center;gap:12px;padding:14px 16px;display:flex}.dispatch-modal-header{border-bottom:1px solid #ffffff14}.dispatch-modal-header h3{margin:0;font-size:22px;font-weight:650}.dispatch-modal-body{padding:16px}.dispatch-meta-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-bottom:18px;display:grid}.dispatch-meta-grid strong{margin-top:4px;font-size:14px;font-weight:600;display:block}.dispatch-modal-fields{gap:12px;display:grid}.dispatch-field{gap:6px;display:grid}.dispatch-field span{text-transform:uppercase;letter-spacing:.08em;color:#e9f1f8b3;font-size:12px;font-weight:650}.dispatch-modal-actions{border-top:1px solid #ffffff14;justify-content:flex-end} \ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/cad-dispatcher.js b/arma/client/addons/cad/ui/_site/cad-dispatcher.js new file mode 100644 index 0000000..6da2ab3 --- /dev/null +++ b/arma/client/addons/cad/ui/_site/cad-dispatcher.js @@ -0,0 +1 @@ +window.cadDispatcher={contracts:[],groups:[],activity:[],session:{},editingGroupId:"",statuses:["available","en_route","on_task","holding","danger","refit","offline"],roles:["infantry","recon","armor","air","logistics","support"],init(){document.getElementById("dispatcherRefreshBtn").addEventListener("click",()=>{this.setStatus("Refreshing board...","info"),window.mapUI.sendEvent("cad::refresh",{})}),document.getElementById("dispatcherGroupModalCloseBtn").addEventListener("click",()=>{this.closeGroupModal()}),document.getElementById("dispatcherGroupModalSaveBtn").addEventListener("click",()=>{this.applyGroupUpdates()}),document.querySelector("#dispatcherGroupModal .dispatch-modal-backdrop").addEventListener("click",()=>{this.closeGroupModal()}),window.mapUI.sendEvent("cad::dispatcher::ready",{})},receiveHydrate(t){this.contracts=Array.isArray(t.contracts)?t.contracts:[],this.groups=Array.isArray(t.groups)?t.groups:[],this.activity=Array.isArray(t.activity)?t.activity:[],this.session=t.session&&"object"==typeof t.session?t.session:{};const e=document.getElementById("dispatcherStatusMessage");!e||e.dataset.type&&"info"!==e.dataset.type||this.setStatus("",""),this.syncOpenModal(),this.render()},setStatus(t,e){const s=document.getElementById("dispatcherStatusMessage");s&&(s.textContent=t||"",s.dataset.type=e||"")},assignTask(t){const e=document.getElementById(`dispatcher-assign-group-${t}`);e&&e.value?(this.setStatus("Submitting assignment...","info"),window.mapUI.sendEvent("cad::tasks::assign",{taskID:t,groupID:e.value,note:""})):this.setStatus("Select a group before assigning a contract.","error")},openGroupModal(t){const e=this.groups.find(e=>e.groupId===t);e&&(this.editingGroupId=t,document.getElementById("dispatcherModalGroupCallsign").textContent=e.callsign||e.groupId||"Unknown",document.getElementById("dispatcherModalGroupLeader").textContent=e.leaderName||"Unknown",document.getElementById("dispatcherModalGroupTask").textContent=e.currentTaskId||"None",document.getElementById("dispatcherModalGroupOrg").textContent=e.orgId||"default",document.getElementById("dispatcherModalRoleSelect").innerHTML=this.roles.map(t=>``).join(""),document.getElementById("dispatcherModalStatusSelect").innerHTML=this.statuses.map(t=>``).join(""),document.getElementById("dispatcherGroupModal").classList.remove("is-hidden"))},closeGroupModal(){this.editingGroupId="",document.getElementById("dispatcherGroupModal").classList.add("is-hidden")},syncOpenModal(){if(!this.editingGroupId)return;const t=this.groups.find(t=>t.groupId===this.editingGroupId);t?(document.getElementById("dispatcherModalGroupCallsign").textContent=t.callsign||t.groupId||"Unknown",document.getElementById("dispatcherModalGroupLeader").textContent=t.leaderName||"Unknown",document.getElementById("dispatcherModalGroupTask").textContent=t.currentTaskId||"None",document.getElementById("dispatcherModalGroupOrg").textContent=t.orgId||"default"):this.closeGroupModal()},applyGroupUpdates(){if(!this.editingGroupId)return;const t=this.groups.find(t=>t.groupId===this.editingGroupId);if(!t)return void this.closeGroupModal();const e=document.getElementById("dispatcherModalRoleSelect").value,s=document.getElementById("dispatcherModalStatusSelect").value;let n=!1;e&&e!==(t.role||"")&&(n=!0,this.setStatus("Updating group role...","info"),window.mapUI.sendEvent("cad::groups::role",{groupID:this.editingGroupId,role:e})),s&&s!==(t.status||"")&&(n=!0,this.setStatus("Updating group status...","info"),window.mapUI.sendEvent("cad::groups::status",{groupID:this.editingGroupId,status:s})),n||this.setStatus("No group changes to save.","info"),this.closeGroupModal()},buildGroupEditorButton:t=>`\n \n ⚙\n \n `,renderMetrics(){const t=this.contracts.filter(t=>"unassigned"!==(t.assignmentState||"unassigned")),e=this.contracts.filter(t=>"unassigned"===(t.assignmentState||"unassigned")),s=this.groups.filter(t=>"danger"===(t.status||""));document.getElementById("metricOpenContracts").textContent=e.length,document.getElementById("metricAssignedContracts").textContent=t.length,document.getElementById("metricActiveGroups").textContent=this.groups.length,document.getElementById("metricDangerGroups").textContent=s.length},renderOpenContracts(){const t=document.getElementById("dispatcherOpenContracts"),e=this.contracts.filter(t=>"unassigned"===(t.assignmentState||"unassigned"));if(!e.length)return void(t.innerHTML='

No open contracts.

');const s=this.groups.map(t=>``).join("");t.innerHTML=e.map(t=>{const e=t.taskId||t.taskID||"",n=Array.isArray(t.position)?t.position:[0,0,0];return`\n
\n
\n ${t.title||e}\n ${t.type||"task"}\n
\n

${t.description||""}

\n
\n Unassigned\n X: ${Math.round(n[0]||0)} Y: ${Math.round(n[1]||0)}\n
\n
\n \n \n
\n
\n `}).join("")},renderAssignedContracts(){const t=document.getElementById("dispatcherAssignedContracts"),e=this.contracts.filter(t=>"unassigned"!==(t.assignmentState||"unassigned"));e.length?t.innerHTML=e.map(t=>{const e=t.taskId||t.taskID||"",s=this.groups.find(e=>e.groupId===(t.assignedGroupId||""));return`\n
\n
\n ${t.title||e}\n ${t.assignmentState||"assigned"}\n
\n

${t.description||""}

\n
\n Group: ${s?s.callsign:t.assignedGroupId||"Unknown"}\n Type: ${t.type||"task"}\n
\n
\n `}).join(""):t.innerHTML='

No assigned contracts.

'},renderGroups(){const t=document.getElementById("dispatcherGroups");this.groups.length?t.innerHTML=this.groups.map(t=>`\n
\n
\n
\n ${t.callsign||t.groupId}\n ${t.role||"group"}\n
\n
\n ${this.buildGroupEditorButton(t.groupId)}\n
\n
\n
\n Leader: ${t.leaderName||"Unknown"}\n Status: ${t.status||"unknown"}\n
\n
\n Org: ${t.orgId||"default"}\n Task: ${t.currentTaskId||"None"}\n
\n
\n `).join(""):t.innerHTML='

No active groups available.

'},renderActivity(){const t=document.getElementById("dispatcherActivity");this.activity.length?t.innerHTML=this.activity.slice().reverse().slice(0,12).map(t=>`\n
\n
\n ${t.type||"activity"}\n ${Math.round(t.timestamp||0)}s\n
\n

${t.message||""}

\n
\n `).join(""):t.innerHTML='

No recent activity.

'},render(){this.renderMetrics(),this.renderOpenContracts(),this.renderAssignedContracts(),this.renderGroups(),this.renderActivity()}},window.cadDispatcher.init(); \ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/cad-sidepanel.css b/arma/client/addons/cad/ui/_site/cad-sidepanel.css index 112ce7f..eae1ee5 100644 --- a/arma/client/addons/cad/ui/_site/cad-sidepanel.css +++ b/arma/client/addons/cad/ui/_site/cad-sidepanel.css @@ -1 +1 @@ -html,body{background:var(--panel);border-left:1px solid var(--stroke);width:100%;height:100%;box-shadow:var(--shadow);-webkit-backdrop-filter:blur(12px);margin:0;padding:0;overflow:hidden}body{opacity:1;visibility:visible}.panel-header{border-bottom:1px solid var(--stroke);background:linear-gradient(#ffffff0d,#0000);justify-content:space-between;align-items:center;padding:14px;display:flex}.panel-header h3{color:var(--accent);text-transform:uppercase;letter-spacing:.8px;font-size:14px;font-weight:650}.panel-content{height:calc(100% - 56px);padding:14px;overflow:auto}.placeholder-message{text-align:center;padding:20px}.placeholder-message p{color:var(--muted);font-size:13px;font-style:italic}.task-toolbar{margin-bottom:10px}.cad-tabs{grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:12px;display:grid}.cad-tab{color:#f3f6f9c7;text-transform:uppercase;letter-spacing:.08em;cursor:pointer;background:#141b21e0;border:1px solid #ffffff24;padding:8px 10px;font-size:11px}.cad-tab:hover{color:#f3f6f9;background:#1f282ff0}.cad-tab.is-active{color:var(--accent);background:#0f283af5;border-color:#5bbbff6b}.cad-tab-panels{min-height:0}.cad-section{display:none}.cad-section.is-active{display:block}.cad-section-header{color:var(--accent);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;font-size:12px;font-weight:700}.task-toolbar button,.task-accept-btn,.task-secondary-btn,.cad-select{color:#f3f6f9;background:#1e252be6;border:1px solid #fff3;width:100%;padding:8px 10px}.task-toolbar button,.task-accept-btn,.task-secondary-btn{cursor:pointer}.task-toolbar button:hover,.task-accept-btn:hover,.task-secondary-btn:hover{background:#2e3942f2}.task-toolbar button:disabled,.task-accept-btn:disabled,.task-secondary-btn:disabled{opacity:.55;cursor:default}.task-status-message{color:#cdd6dd;min-height:18px;margin-bottom:10px;font-size:12px}.task-status-message[data-type=success]{color:#79d28a}.task-status-message[data-type=error]{color:#ff8a80}.task-list{flex-direction:column;gap:10px;display:flex}.task-action-stack,.task-action-row{flex-direction:column;gap:8px;display:flex}.task-action-row{flex-direction:row}.task-card{background:#0c10149e;border:1px solid #ffffff14;padding:10px}.task-card-header{justify-content:space-between;gap:8px;margin-bottom:8px;display:flex}.task-type{opacity:.7;text-transform:uppercase;font-size:11px}.task-description{margin:0 0 8px;font-size:12px;line-height:1.4}.task-meta{opacity:.8;justify-content:space-between;gap:8px;margin-bottom:8px;font-size:11px;display:flex}.task-secondary-btn{background:#3c302deb} \ No newline at end of file +html,body{background:var(--panel);border-left:1px solid var(--stroke);width:100%;height:100%;box-shadow:var(--shadow);-webkit-backdrop-filter:blur(12px);margin:0;padding:0;overflow:hidden}body{opacity:1;visibility:visible}.panel-header{border-bottom:1px solid var(--stroke);background:linear-gradient(#ffffff0d,#0000);justify-content:space-between;align-items:center;padding:14px;display:flex}.panel-header h3{color:var(--accent);text-transform:uppercase;letter-spacing:.8px;font-size:14px;font-weight:650}.panel-content{height:calc(100% - 56px);padding:14px;overflow:auto}.placeholder-message{text-align:center;padding:20px}.placeholder-message p{color:var(--muted);font-size:13px;font-style:italic}.task-toolbar{margin-bottom:10px}.cad-tabs{grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:12px;display:grid}.cad-tab{color:#f3f6f9c7;text-transform:uppercase;letter-spacing:.08em;cursor:pointer;background:#141b21e0;border:1px solid #ffffff24;padding:8px 10px;font-size:11px}.cad-tab:hover{color:#f3f6f9;background:#1f282ff0}.cad-tab.is-active{color:var(--accent);background:#0f283af5;border-color:#5bbbff6b}.cad-tab-panels{min-height:0}.cad-section{display:none}.cad-section.is-active{display:block}.cad-section-header{color:var(--accent);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;font-size:12px;font-weight:700}.task-toolbar button,.task-accept-btn,.task-secondary-btn,.cad-select{color:#f3f6f9;background:#1e252be6;border:1px solid #fff3;width:100%;padding:8px 10px}.task-toolbar button,.task-accept-btn,.task-secondary-btn{cursor:pointer}.task-toolbar button:hover,.task-accept-btn:hover,.task-secondary-btn:hover{background:#2e3942f2}.task-toolbar button:disabled,.task-accept-btn:disabled,.task-secondary-btn:disabled{opacity:.55;cursor:default}.task-status-message{color:#cdd6dd;min-height:18px;margin-bottom:10px;font-size:12px}.task-status-message[data-type=success]{color:#79d28a}.task-status-message[data-type=error]{color:#ff8a80}.task-list{flex-direction:column;gap:10px;display:flex}.task-action-stack,.task-action-row{flex-direction:column;gap:8px;display:flex}.task-action-row{flex-direction:row}.task-card{background:#0c10149e;border:1px solid #ffffff14;padding:10px}.task-card-header{justify-content:space-between;gap:8px;margin-bottom:8px;display:flex}.task-type{opacity:.7;text-transform:uppercase;font-size:11px}.task-description{margin:0 0 8px;font-size:12px;line-height:1.4}.task-meta{opacity:.8;justify-content:space-between;gap:8px;margin-bottom:8px;font-size:11px;display:flex}.task-secondary-btn{background:#3c302deb}.roster-summary-card{background:#10171dd1;border:1px solid #ffffff14;padding:10px}.roster-member-card{background:#0c1014bd}.roster-leader-badge{color:var(--accent);letter-spacing:.06em;text-transform:uppercase;background:#0f283ad1;border:1px solid #5bbbff47;align-items:center;padding:2px 8px;font-size:10px;font-weight:700;display:inline-flex} \ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/cad-sidepanel.js b/arma/client/addons/cad/ui/_site/cad-sidepanel.js index fa1bdc4..c27970a 100644 --- a/arma/client/addons/cad/ui/_site/cad-sidepanel.js +++ b/arma/client/addons/cad/ui/_site/cad-sidepanel.js @@ -1 +1 @@ -window.cadTasks={contracts:[],groups:[],activity:[],session:{},activeTab:"contracts",statuses:["available","en_route","on_task","holding","danger","refit","offline"],init(){const s=document.getElementById("refreshCadBtn");s&&s.addEventListener("click",()=>this.refresh()),document.querySelectorAll(".cad-tab").forEach(s=>{s.addEventListener("click",()=>{this.setActiveTab(s.dataset.tab||"contracts")})}),window.ForgeBridge.on("cad::hydrate",s=>{this.setHydratePayload(s||{})}),window.ForgeBridge.on("cad::assignment::response",s=>{this.handleServerResponse(!!s.success,s.message||"")}),window.ForgeBridge.on("cad::group::response",s=>{this.handleServerResponse(!!s.success,s.message||"")}),window.ForgeBridge.ready({loaded:!0})},setActiveTab(s){this.activeTab=s||"contracts",document.querySelectorAll(".cad-tab").forEach(s=>{s.classList.toggle("is-active",s.dataset.tab===this.activeTab)}),document.querySelectorAll("[data-panel]").forEach(s=>{s.classList.toggle("is-active",s.dataset.panel===this.activeTab)})},setHydratePayload(s){this.contracts=Array.isArray(s.contracts)?s.contracts:[],this.groups=Array.isArray(s.groups)?s.groups:[],this.activity=Array.isArray(s.activity)?s.activity:[],this.session=s.session&&"object"==typeof s.session?s.session:{};const t=document.getElementById("cadStatusMessage");!t||t.dataset.type&&"info"!==t.dataset.type||this.setStatus("",""),this.render()},setStatus(s,t){const e=document.getElementById("cadStatusMessage");e&&(e.textContent=s||"",e.dataset.type=t||"")},handleServerResponse(s,t){this.setStatus(t||(s?"CAD update succeeded.":"CAD update failed."),s?"success":"error")},refresh(){this.setStatus("Refreshing board...","info"),window.mapUI.sendEvent("cad::refresh",{})},assignTask(s){const t=document.getElementById(`assign-group-${s}`);t&&t.value?(this.setStatus("Submitting assignment...","info"),window.mapUI.sendEvent("cad::tasks::assign",{taskID:s,groupID:t.value,note:""})):this.setStatus("Select a group before assigning a contract.","error")},acknowledgeTask(s){this.setStatus("Acknowledging contract...","info"),window.mapUI.sendEvent("cad::tasks::acknowledge",{taskID:s})},declineTask(s){this.setStatus("Declining contract...","info"),window.mapUI.sendEvent("cad::tasks::decline",{taskID:s})},updateGroupStatus(s,t){this.setStatus("Updating group status...","info"),window.mapUI.sendEvent("cad::groups::status",{groupID:s,status:t})},getPlayerGroupId(){return this.session.groupId||""},canDispatch(){return!!this.session.isDispatcher},isLeader(){return!!this.session.isLeader},renderContracts(){const s=document.getElementById("taskList");if(!s)return;if(!this.contracts.length)return void(s.innerHTML='

No active contracts are available.

');const t=this.getPlayerGroupId();s.innerHTML=this.contracts.map(s=>{const e=s.taskId||s.taskID||"",a=Array.isArray(s.position)?s.position:[0,0,0],n=s.assignedGroupId||"",i=s.assignmentState||"unassigned",o=this.groups.find(s=>s.groupId===n),r=this.isLeader()&&n===t,c=this.groups.map(s=>``).join("");return`\n
\n
\n ${s.title||e}\n ${s.type||"task"}\n
\n

${s.description||""}

\n
\n ${"unassigned"===i?"Available":`${i}: ${o?o.callsign:n}`}\n X: ${Math.round(a[0]||0)} Y: ${Math.round(a[1]||0)}\n
\n ${this.canDispatch()?`
\n \n \n
`:""}\n ${r&&"assigned"===i?`
\n \n \n
`:""}\n
\n `}).join("")},renderGroups(){const s=document.getElementById("groupList");if(!s)return;if(!this.groups.length)return void(s.innerHTML='

No active groups are available.

');const t=this.getPlayerGroupId();s.innerHTML=this.groups.map(s=>{const e=this.canDispatch()||this.isLeader()&&s.groupId===t,a=this.statuses.map(t=>``).join("");return`\n
\n
\n ${s.callsign||s.groupId}\n ${s.role||"group"}\n
\n
\n Leader: ${s.leaderName||"Unknown"}\n Status: ${s.status||"unknown"}\n
\n
\n Org: ${s.orgId||"default"}\n Task: ${s.currentTaskId||"None"}\n
\n ${e?`
\n \n \n
`:""}\n
\n `}).join("")},renderActivity(){const s=document.getElementById("activityList");s&&(this.activity.length?s.innerHTML=this.activity.slice().reverse().slice(0,8).map(s=>`\n
\n
\n ${s.type||"activity"}\n ${Math.round(s.timestamp||0)}s\n
\n

${s.message||""}

\n
\n `).join(""):s.innerHTML='

No recent activity.

')},render(){this.renderContracts(),this.renderGroups(),this.renderActivity(),this.setActiveTab(this.activeTab)}},window.cadTasks.init(); \ No newline at end of file +window.cadTasks={contracts:[],groups:[],activity:[],session:{},mode:"operations",activeTab:"contracts",statuses:["available","en_route","on_task","holding","danger","refit","offline"],roles:["infantry","recon","armor","air","logistics","support"],init(){const s=document.getElementById("refreshCadBtn");s&&s.addEventListener("click",()=>this.refresh()),document.querySelectorAll(".cad-tab").forEach(s=>{s.addEventListener("click",()=>{this.setActiveTab(s.dataset.tab||"contracts")})}),window.ForgeBridge.on("cad::hydrate",s=>{this.setHydratePayload(s||{})}),window.ForgeBridge.on("cad::assignment::response",s=>{this.handleServerResponse(!!s.success,s.message||"")}),window.ForgeBridge.on("cad::group::response",s=>{this.handleServerResponse(!!s.success,s.message||"")}),window.ForgeBridge.ready({loaded:!0})},setActiveTab(s){this.activeTab=s||"contracts",document.querySelectorAll(".cad-tab").forEach(s=>{s.classList.toggle("is-active",s.dataset.tab===this.activeTab)}),document.querySelectorAll("[data-panel]").forEach(s=>{s.classList.toggle("is-active",s.dataset.panel===this.activeTab)})},setHydratePayload(s){this.contracts=Array.isArray(s.contracts)?s.contracts:[],this.groups=Array.isArray(s.groups)?s.groups:[],this.activity=Array.isArray(s.activity)?s.activity:[],this.session=s.session&&"object"==typeof s.session?s.session:{},this.mode=s&&"string"==typeof s.mode?s.mode:"operations";const t=document.getElementById("cadStatusMessage");!t||t.dataset.type&&"info"!==t.dataset.type||this.setStatus("",""),this.render()},setStatus(s,t){const e=document.getElementById("cadStatusMessage");e&&(e.textContent=s||"",e.dataset.type=t||"")},handleServerResponse(s,t){this.setStatus(t||(s?"CAD update succeeded.":"CAD update failed."),s?"success":"error")},refresh(){this.setStatus("Refreshing board...","info"),window.mapUI.sendEvent("cad::refresh",{})},acknowledgeTask(s){this.setStatus("Acknowledging contract...","info"),window.mapUI.sendEvent("cad::tasks::acknowledge",{taskID:s})},declineTask(s){this.setStatus("Declining contract...","info"),window.mapUI.sendEvent("cad::tasks::decline",{taskID:s})},updateGroupStatus(s,t){this.setStatus("Updating group status...","info"),window.mapUI.sendEvent("cad::groups::status",{groupID:s,status:t})},updateGroupRole(s,t){this.setStatus("Updating group role...","info"),window.mapUI.sendEvent("cad::groups::role",{groupID:s,role:t})},getPlayerGroupId(){return this.session.groupId||""},getCurrentGroup(){const s=this.getPlayerGroupId();return this.groups.find(t=>t.groupId===s)||null},normalizeCollection:s=>Array.isArray(s)?s:s&&"object"==typeof s?Object.values(s):[],canDispatch(){return!!this.session.isDispatcher},isDispatchMode(){return"dispatch"===this.mode},isLeader(){return!!this.session.isLeader},renderContracts(){const s=document.getElementById("taskList");if(!s)return;const t=this.getPlayerGroupId(),e=this.contracts.filter(s=>(s.assignedGroupId||"")===t);e.length?s.innerHTML=e.map(s=>{const e=s.taskId||s.taskID||"",a=Array.isArray(s.position)?s.position:[0,0,0],n=s.assignedGroupId||"",r=s.assignmentState||"unassigned",i=this.groups.find(s=>s.groupId===n),o=this.isLeader()&&n===t;return`\n
\n
\n ${s.title||e}\n ${s.type||"task"}\n
\n

${s.description||""}

\n
\n ${"unassigned"===r?"Available":`${r}: ${i?i.callsign:n}`}\n X: ${Math.round(a[0]||0)} Y: ${Math.round(a[1]||0)}\n
\n ${o&&"assigned"===r?`
\n \n \n
`:""}\n
\n `}).join(""):s.innerHTML='

No contract is currently assigned to your group.

'},renderRoster(){const s=document.getElementById("rosterList");if(!s)return;const t=this.getCurrentGroup();if(!t)return void(s.innerHTML='

Your group is not currently available.

');const e=this.normalizeCollection(t.members);e.length?s.innerHTML=`\n
\n
\n ${t.callsign||t.groupId||"Current Group"}\n ${e.length} member${1===e.length?"":"s"}\n
\n
\n Leader: ${t.leaderName||"Unknown"}\n Status: ${t.status||"unknown"}\n
\n
\n Role: ${t.role||"unassigned"}\n Task: ${t.currentTaskId||"None"}\n
\n
\n ${e.map(s=>{const t=(s.lifeState||"unknown").replaceAll("_"," "),e=s.isLeader?'Leader':"";return`\n
\n
\n ${s.name||"Unknown Operator"}\n ${t}\n
\n
\n ${s.uid||"No UID"}\n ${e}\n
\n
\n `}).join("")}\n `:s.innerHTML='

No roster members are currently available.

'},renderActivity(){const s=document.getElementById("activityList");s&&(this.activity.length?s.innerHTML=this.activity.slice().reverse().slice(0,8).map(s=>`\n
\n
\n ${s.type||"activity"}\n ${Math.round(s.timestamp||0)}s\n
\n

${s.message||""}

\n
\n `).join(""):s.innerHTML='

No recent activity.

')},render(){this.renderContracts(),this.renderRoster(),this.renderActivity(),this.setActiveTab(this.activeTab)}},window.cadTasks.init(); \ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/cad-topbar.css b/arma/client/addons/cad/ui/_site/cad-topbar.css index 59129ec..5ae3100 100644 --- a/arma/client/addons/cad/ui/_site/cad-topbar.css +++ b/arma/client/addons/cad/ui/_site/cad-topbar.css @@ -1 +1 @@ -body{-webkit-backdrop-filter:blur(18px);background:linear-gradient(90deg,#10161ff5,#131a24f0 55%,#0f141cf5);border-bottom:1px solid #ffffff24;justify-content:space-between;align-items:center;height:56px;padding:0 20px;display:flex;position:absolute;top:0;left:0;right:0;overflow:hidden;box-shadow:0 14px 28px #00000047}.logo{color:var(--accent);text-transform:uppercase;letter-spacing:.4px;text-shadow:0 1px 12px #00000059;font-size:16px;font-weight:650}.controls{align-items:center;gap:10px;display:flex}.search-input{color:var(--text);background:#ffffff14;border:1px solid #ffffff24;border-radius:999px;outline:none;width:250px;padding:10px 12px;font-size:13px;box-shadow:inset 0 1px #ffffff08}.search-input::placeholder{color:var(--muted2)}.search-input:focus{background:#ffffff1c;border-color:#68c4ff73}.info{color:#f5f8ffd6;font-size:12px;font-family:var(--font);text-shadow:0 1px 10px #00000047;gap:20px;display:flex} \ No newline at end of file +body{background:0 0;grid-template-columns:auto minmax(0,1fr) auto auto;align-items:center;column-gap:16px;height:60px;padding:0 16px;display:grid;position:absolute;top:0;left:0;right:0;overflow:visible}body:before{content:"";height:60px;box-shadow:none;-webkit-backdrop-filter:blur(18px);z-index:0;pointer-events:none;background:linear-gradient(90deg,#10161ff5,#131a24f0 55%,#0f141cf5);border-bottom:none;position:absolute;inset:0 0 auto}body>*{z-index:1;position:relative}.logo{color:var(--accent);text-transform:uppercase;letter-spacing:.08em;text-shadow:0 1px 12px #00000059;font-size:15px;font-weight:650}.header-main{align-items:center;gap:12px;min-width:0;display:flex}.title-block{flex-direction:column;flex:none;gap:1px;min-width:0;display:flex}.title-kicker{color:#dae3ec8f;text-transform:uppercase;letter-spacing:.12em;font-size:10px}.title-main{color:#f5f8ffeb;font-size:15px;font-weight:600}.operator-strip{flex:auto;align-items:center;gap:8px;min-width:0;display:flex}.operator-strip.is-hidden,.operator-controls.is-hidden{display:none}.operator-info{flex-direction:column;gap:0;min-width:88px;display:flex}.operator-label{color:#dae3ec80;text-transform:uppercase;letter-spacing:.12em;font-size:9px}.operator-info strong{color:#f5f8ffe6;font-size:12px;font-weight:550}.operator-controls{align-items:center;gap:6px;min-width:0;display:flex}.operator-select{min-width:92px;max-width:112px;color:var(--text);background:#0e141cf5;border:1px solid #ffffff24;padding:5px 8px;font-size:11px}.btn-operator{text-transform:uppercase;letter-spacing:.08em;min-width:84px;font-size:10px}.mode-controls{justify-self:end;align-items:center;gap:8px;display:flex}.mode-controls.is-hidden{display:none}.controls{justify-self:end;align-items:center;gap:8px;display:flex}.mode-text{color:#e9f1f8b8;text-transform:uppercase;letter-spacing:.1em;font-size:10px}.mode-switch{align-items:center;width:54px;height:28px;display:inline-flex;position:relative}.mode-switch input{opacity:0;pointer-events:none;position:absolute}.mode-slider{background:#161d27eb;border:1px solid #ffffff24;border-radius:999px;width:54px;height:28px;transition:border-color .16s,background .16s;position:relative;box-shadow:inset 0 1px 10px #00000038}.mode-slider:after{content:"";background:linear-gradient(#edf4fbfa,#bdcdddeb);border-radius:50%;width:20px;height:20px;transition:transform .16s,background .16s;position:absolute;top:3px;left:3px;box-shadow:0 4px 12px #00000042}.mode-switch input:checked+.mode-slider{background:#0e2538f2;border-color:#5bbbff6b}.mode-switch input:checked+.mode-slider:after{background:linear-gradient(#83d4fffa,#48aae7f0);transform:translate(26px)}.btn-close{min-width:42px}body[data-mode=operations]{pointer-events:none}body[data-mode=operations] .logo,body[data-mode=operations] .title-block,body[data-mode=operations] .operator-strip,body[data-mode=operations] .operator-controls,body[data-mode=operations] .mode-controls,body[data-mode=operations] .controls,body[data-mode=operations] .mode-switch,body[data-mode=operations] .mode-switch *,body[data-mode=operations] button,body[data-mode=operations] select,body[data-mode=operations] label{pointer-events:auto} \ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/cad-topbar.js b/arma/client/addons/cad/ui/_site/cad-topbar.js index 91698ea..b347404 100644 --- a/arma/client/addons/cad/ui/_site/cad-topbar.js +++ b/arma/client/addons/cad/ui/_site/cad-topbar.js @@ -1 +1 @@ -document.getElementById("btnZoomIn").addEventListener("click",()=>{window.mapUI.sendEvent("map::zoomIn",null)}),document.getElementById("btnZoomOut").addEventListener("click",()=>{window.mapUI.sendEvent("map::zoomOut",null)}),document.getElementById("btnClose").addEventListener("click",()=>{window.mapUI.sendEvent("map::close",null)}),document.getElementById("searchBox").addEventListener("keypress",e=>{"Enter"===e.key&&window.mapUI.sendEvent("map::search",e.target.value)}); \ No newline at end of file +window.cadTopbar={mode:"operations",currentGroup:null,session:{},init(){document.getElementById("btnClose").addEventListener("click",()=>{window.mapUI.sendEvent("map::close",null)}),document.getElementById("modeToggle").addEventListener("change",e=>{window.mapUI.sendEvent("cad::mode::set",{mode:e.target.checked?"dispatch":"operations"})}),document.getElementById("operatorRoleBtn").addEventListener("click",()=>{this.currentGroup&&window.mapUI.sendEvent("cad::groups::role",{groupID:this.currentGroup.groupId||"",role:document.getElementById("operatorRoleSelect").value})}),document.getElementById("operatorStatusBtn").addEventListener("click",()=>{this.currentGroup&&window.mapUI.sendEvent("cad::groups::status",{groupID:this.currentGroup.groupId||"",status:document.getElementById("operatorStatusSelect").value})}),window.mapUI.sendEvent("cad::topbar::ready",{})},formatLocation(e){const t=Array.isArray(e?.position)?e.position:[0,0,0];return`X: ${Math.round(t[0]||0).toString().padStart(4,"0")} Y: ${Math.round(t[1]||0).toString().padStart(4,"0")}`},receiveState(e){this.session=e&&e.session&&"object"==typeof e.session?e.session:{},this.mode=e&&"string"==typeof e.mode?e.mode:"operations",this.currentGroup=e&&e.currentGroup&&"object"==typeof e.currentGroup?e.currentGroup:null;const t=document.getElementById("modeControls"),o=!!this.session.isDispatcher,r=!(!this.currentGroup||!this.session.isLeader&&!this.session.isDispatcher),n=document.getElementById("operatorStrip"),s=document.getElementById("operatorControls");t.classList.toggle("is-hidden",!o),n.classList.toggle("is-hidden","operations"!==this.mode||!this.currentGroup),s.classList.toggle("is-hidden",!r),document.body.dataset.mode=this.mode,document.body.dataset.dispatcher=o?"true":"false",document.getElementById("modeToggle").checked="dispatch"===this.mode,document.getElementById("operatorGroupName").textContent=this.currentGroup?this.currentGroup.callsign||this.currentGroup.groupId||"Current Group":"No Group",document.getElementById("operatorLocation").textContent=this.currentGroup?this.formatLocation(this.currentGroup):"Unavailable",this.currentGroup&&(document.getElementById("operatorRoleSelect").value=this.currentGroup.role||"infantry",document.getElementById("operatorStatusSelect").value=this.currentGroup.status||"available")}},window.cadTopbar.init(); \ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/dispatcher.html b/arma/client/addons/cad/ui/_site/dispatcher.html new file mode 100644 index 0000000..f44f296 --- /dev/null +++ b/arma/client/addons/cad/ui/_site/dispatcher.html @@ -0,0 +1 @@ +

Dispatch Dashboard

Operational Board

Open Contracts 0
Assigned Contracts 0
Active Groups 0
Groups In Danger 0

Available Contracts

Assigned Contracts

Group Board

Activity Feed

\ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/sidepanel.html b/arma/client/addons/cad/ui/_site/sidepanel.html index 778ef42..7917e62 100644 --- a/arma/client/addons/cad/ui/_site/sidepanel.html +++ b/arma/client/addons/cad/ui/_site/sidepanel.html @@ -1 +1 @@ -

CAD System

Contracts

Loading contracts...

Groups

Loading groups...

Activity

No recent activity.

\ No newline at end of file +

CAD System

Contracts

Loading contracts...

Roster

Loading roster...

Activity

No recent activity.

\ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/topbar.html b/arma/client/addons/cad/ui/_site/topbar.html index 6eed3c9..4e31fcb 100644 --- a/arma/client/addons/cad/ui/_site/topbar.html +++ b/arma/client/addons/cad/ui/_site/topbar.html @@ -1 +1 @@ -
X: 0000 Y: 0000 Scale: 1:1000
\ No newline at end of file +
Cad Systems FORGE Command & Dispatch
\ No newline at end of file diff --git a/arma/client/addons/cad/ui/src/bottombar.html b/arma/client/addons/cad/ui/src/bottombar.html index b87d3cb..061c255 100644 --- a/arma/client/addons/cad/ui/src/bottombar.html +++ b/arma/client/addons/cad/ui/src/bottombar.html @@ -4,8 +4,8 @@ - Map Ready - + CAD Systems by IDS + v1.0.0 + + diff --git a/arma/client/addons/cad/ui/src/dispatcher.js b/arma/client/addons/cad/ui/src/dispatcher.js new file mode 100644 index 0000000..b29b427 --- /dev/null +++ b/arma/client/addons/cad/ui/src/dispatcher.js @@ -0,0 +1,393 @@ +window.cadDispatcher = { + contracts: [], + groups: [], + activity: [], + session: {}, + editingGroupId: "", + statuses: [ + "available", + "en_route", + "on_task", + "holding", + "danger", + "refit", + "offline", + ], + roles: ["infantry", "recon", "armor", "air", "logistics", "support"], + init() { + document + .getElementById("dispatcherRefreshBtn") + .addEventListener("click", () => { + this.setStatus("Refreshing board...", "info"); + window.mapUI.sendEvent("cad::refresh", {}); + }); + + document + .getElementById("dispatcherGroupModalCloseBtn") + .addEventListener("click", () => { + this.closeGroupModal(); + }); + + document + .getElementById("dispatcherGroupModalSaveBtn") + .addEventListener("click", () => { + this.applyGroupUpdates(); + }); + + document + .querySelector("#dispatcherGroupModal .dispatch-modal-backdrop") + .addEventListener("click", () => { + this.closeGroupModal(); + }); + + window.mapUI.sendEvent("cad::dispatcher::ready", {}); + }, + receiveHydrate(payload) { + this.contracts = Array.isArray(payload.contracts) + ? payload.contracts + : []; + this.groups = Array.isArray(payload.groups) ? payload.groups : []; + this.activity = Array.isArray(payload.activity) ? payload.activity : []; + this.session = + payload.session && typeof payload.session === "object" + ? payload.session + : {}; + + const statusEl = document.getElementById("dispatcherStatusMessage"); + if ( + statusEl && + (!statusEl.dataset.type || statusEl.dataset.type === "info") + ) { + this.setStatus("", ""); + } + + this.syncOpenModal(); + this.render(); + }, + setStatus(message, type) { + const statusEl = document.getElementById("dispatcherStatusMessage"); + if (!statusEl) { + return; + } + + statusEl.textContent = message || ""; + statusEl.dataset.type = type || ""; + }, + assignTask(taskID) { + const selector = document.getElementById( + `dispatcher-assign-group-${taskID}`, + ); + if (!selector || !selector.value) { + this.setStatus( + "Select a group before assigning a contract.", + "error", + ); + return; + } + + this.setStatus("Submitting assignment...", "info"); + window.mapUI.sendEvent("cad::tasks::assign", { + taskID: taskID, + groupID: selector.value, + note: "", + }); + }, + openGroupModal(groupID) { + const group = this.groups.find((entry) => entry.groupId === groupID); + if (!group) { + return; + } + + this.editingGroupId = groupID; + document.getElementById("dispatcherModalGroupCallsign").textContent = + group.callsign || group.groupId || "Unknown"; + document.getElementById("dispatcherModalGroupLeader").textContent = + group.leaderName || "Unknown"; + document.getElementById("dispatcherModalGroupTask").textContent = + group.currentTaskId || "None"; + document.getElementById("dispatcherModalGroupOrg").textContent = + group.orgId || "default"; + document.getElementById("dispatcherModalRoleSelect").innerHTML = + this.roles + .map( + (role) => + ``, + ) + .join(""); + document.getElementById("dispatcherModalStatusSelect").innerHTML = + this.statuses + .map( + (status) => + ``, + ) + .join(""); + + document + .getElementById("dispatcherGroupModal") + .classList.remove("is-hidden"); + }, + + closeGroupModal() { + this.editingGroupId = ""; + document + .getElementById("dispatcherGroupModal") + .classList.add("is-hidden"); + }, + + syncOpenModal() { + if (!this.editingGroupId) { + return; + } + + const group = this.groups.find( + (entry) => entry.groupId === this.editingGroupId, + ); + if (!group) { + this.closeGroupModal(); + return; + } + + document.getElementById("dispatcherModalGroupCallsign").textContent = + group.callsign || group.groupId || "Unknown"; + document.getElementById("dispatcherModalGroupLeader").textContent = + group.leaderName || "Unknown"; + document.getElementById("dispatcherModalGroupTask").textContent = + group.currentTaskId || "None"; + document.getElementById("dispatcherModalGroupOrg").textContent = + group.orgId || "default"; + }, + + applyGroupUpdates() { + if (!this.editingGroupId) { + return; + } + + const group = this.groups.find( + (entry) => entry.groupId === this.editingGroupId, + ); + if (!group) { + this.closeGroupModal(); + return; + } + + const roleValue = document.getElementById( + "dispatcherModalRoleSelect", + ).value; + const statusValue = document.getElementById( + "dispatcherModalStatusSelect", + ).value; + let hasChanges = false; + + if (roleValue && roleValue !== (group.role || "")) { + hasChanges = true; + this.setStatus("Updating group role...", "info"); + window.mapUI.sendEvent("cad::groups::role", { + groupID: this.editingGroupId, + role: roleValue, + }); + } + + if (statusValue && statusValue !== (group.status || "")) { + hasChanges = true; + this.setStatus("Updating group status...", "info"); + window.mapUI.sendEvent("cad::groups::status", { + groupID: this.editingGroupId, + status: statusValue, + }); + } + + if (!hasChanges) { + this.setStatus("No group changes to save.", "info"); + } + + this.closeGroupModal(); + }, + + buildGroupEditorButton(groupID) { + return ` + + `; + }, + renderMetrics() { + const assignedContracts = this.contracts.filter( + (entry) => (entry.assignmentState || "unassigned") !== "unassigned", + ); + const openContracts = this.contracts.filter( + (entry) => (entry.assignmentState || "unassigned") === "unassigned", + ); + const dangerGroups = this.groups.filter( + (group) => (group.status || "") === "danger", + ); + + document.getElementById("metricOpenContracts").textContent = + openContracts.length; + document.getElementById("metricAssignedContracts").textContent = + assignedContracts.length; + document.getElementById("metricActiveGroups").textContent = + this.groups.length; + document.getElementById("metricDangerGroups").textContent = + dangerGroups.length; + }, + renderOpenContracts() { + const container = document.getElementById("dispatcherOpenContracts"); + const openContracts = this.contracts.filter( + (entry) => (entry.assignmentState || "unassigned") === "unassigned", + ); + + if (!openContracts.length) { + container.innerHTML = + '

No open contracts.

'; + return; + } + + const groupOptions = this.groups + .map( + (group) => + ``, + ) + .join(""); + + container.innerHTML = openContracts + .map((task) => { + const taskId = task.taskId || task.taskID || ""; + const position = Array.isArray(task.position) + ? task.position + : [0, 0, 0]; + + return ` +
+
+ ${task.title || taskId} + ${task.type || "task"} +
+

${task.description || ""}

+
+ Unassigned + X: ${Math.round(position[0] || 0)} Y: ${Math.round(position[1] || 0)} +
+
+ + +
+
+ `; + }) + .join(""); + }, + renderAssignedContracts() { + const container = document.getElementById( + "dispatcherAssignedContracts", + ); + const assignedContracts = this.contracts.filter( + (entry) => (entry.assignmentState || "unassigned") !== "unassigned", + ); + + if (!assignedContracts.length) { + container.innerHTML = + '

No assigned contracts.

'; + return; + } + + container.innerHTML = assignedContracts + .map((task) => { + const taskId = task.taskId || task.taskID || ""; + const assignedGroup = this.groups.find( + (group) => group.groupId === (task.assignedGroupId || ""), + ); + + return ` +
+
+ ${task.title || taskId} + ${task.assignmentState || "assigned"} +
+

${task.description || ""}

+
+ Group: ${assignedGroup ? assignedGroup.callsign : task.assignedGroupId || "Unknown"} + Type: ${task.type || "task"} +
+
+ `; + }) + .join(""); + }, + renderGroups() { + const container = document.getElementById("dispatcherGroups"); + if (!this.groups.length) { + container.innerHTML = + '

No active groups available.

'; + return; + } + + container.innerHTML = this.groups + .map((group) => { + return ` +
+
+
+ ${group.callsign || group.groupId} + ${group.role || "group"} +
+
+ ${this.buildGroupEditorButton(group.groupId)} +
+
+
+ Leader: ${group.leaderName || "Unknown"} + Status: ${group.status || "unknown"} +
+
+ Org: ${group.orgId || "default"} + Task: ${group.currentTaskId || "None"} +
+
+ `; + }) + .join(""); + }, + renderActivity() { + const container = document.getElementById("dispatcherActivity"); + if (!this.activity.length) { + container.innerHTML = + '

No recent activity.

'; + return; + } + + container.innerHTML = this.activity + .slice() + .reverse() + .slice(0, 12) + .map( + (entry) => ` +
+
+ ${entry.type || "activity"} + ${Math.round(entry.timestamp || 0)}s +
+

${entry.message || ""}

+
+ `, + ) + .join(""); + }, + render() { + this.renderMetrics(); + this.renderOpenContracts(); + this.renderAssignedContracts(); + this.renderGroups(); + this.renderActivity(); + }, +}; + +window.cadDispatcher.init(); diff --git a/arma/client/addons/cad/ui/src/sidepanel.html b/arma/client/addons/cad/ui/src/sidepanel.html index 6457059..9fa2c33 100644 --- a/arma/client/addons/cad/ui/src/sidepanel.html +++ b/arma/client/addons/cad/ui/src/sidepanel.html @@ -22,12 +22,12 @@ Contracts - ` - : "" - } ${ isAssignedToLeader && assignmentState === "assigned" ? `
@@ -206,59 +210,66 @@ window.cadTasks = { }) .join(""); }, - renderGroups() { - const listEl = document.getElementById("groupList"); + renderRoster() { + const listEl = document.getElementById("rosterList"); if (!listEl) { return; } - if (!this.groups.length) { + const currentGroup = this.getCurrentGroup(); + if (!currentGroup) { listEl.innerHTML = - '

No active groups are available.

'; + '

Your group is not currently available.

'; return; } - const currentGroupId = this.getPlayerGroupId(); - listEl.innerHTML = this.groups - .map((group) => { - const canUpdate = - this.canDispatch() || - (this.isLeader() && group.groupId === currentGroupId); - const statusOptions = this.statuses - .map( - (status) => - ``, - ) - .join(""); + const roster = this.normalizeCollection(currentGroup.members); - return ` -
+ if (!roster.length) { + listEl.innerHTML = + '

No roster members are currently available.

'; + return; + } + + listEl.innerHTML = ` +
+
+ ${currentGroup.callsign || currentGroup.groupId || "Current Group"} + ${roster.length} member${roster.length === 1 ? "" : "s"} +
+
+ Leader: ${currentGroup.leaderName || "Unknown"} + Status: ${currentGroup.status || "unknown"} +
+
+ Role: ${currentGroup.role || "unassigned"} + Task: ${currentGroup.currentTaskId || "None"} +
+
+ ${roster + .map((member) => { + const lifeState = ( + member.lifeState || "unknown" + ).replaceAll("_", " "); + const leaderBadge = member.isLeader + ? 'Leader' + : ""; + + return ` +
- ${group.callsign || group.groupId} - ${group.role || "group"} + ${member.name || "Unknown Operator"} + ${lifeState}
- Leader: ${group.leaderName || "Unknown"} - Status: ${group.status || "unknown"} + ${member.uid || "No UID"} + ${leaderBadge}
-
- Org: ${group.orgId || "default"} - Task: ${group.currentTaskId || "None"} -
- ${ - canUpdate - ? `
- - -
` - : "" - }
`; - }) - .join(""); + }) + .join("")} + `; }, renderActivity() { const listEl = document.getElementById("activityList"); @@ -291,7 +302,7 @@ window.cadTasks = { }, render() { this.renderContracts(); - this.renderGroups(); + this.renderRoster(); this.renderActivity(); this.setActiveTab(this.activeTab); }, diff --git a/arma/client/addons/cad/ui/src/styles/bottombar.css b/arma/client/addons/cad/ui/src/styles/bottombar.css index 99dcfd2..b9468e2 100644 --- a/arma/client/addons/cad/ui/src/styles/bottombar.css +++ b/arma/client/addons/cad/ui/src/styles/bottombar.css @@ -21,13 +21,20 @@ body { overflow: hidden; } -span { +.footer-brand, +.footer-version { color: rgba(245, 248, 255, 0.8); font-size: 12px; text-shadow: 0 1px 10px rgba(0, 0, 0, 0.28); } -#statusText { +.footer-brand { color: var(--accent); font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.footer-version { + color: rgba(245, 248, 255, 0.62); } diff --git a/arma/client/addons/cad/ui/src/styles/dispatcher.css b/arma/client/addons/cad/ui/src/styles/dispatcher.css new file mode 100644 index 0000000..5b50549 --- /dev/null +++ b/arma/client/addons/cad/ui/src/styles/dispatcher.css @@ -0,0 +1,339 @@ +html, +body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + overflow: hidden; + background: + radial-gradient( + circle at top left, + rgba(41, 69, 93, 0.18), + transparent 30% + ), + linear-gradient(180deg, rgba(9, 14, 20, 0.96), rgba(15, 22, 31, 0.98)); +} + +body { + color: var(--text); + font-family: var(--font); +} + +.dispatch-shell { + height: 100%; + display: flex; + flex-direction: column; + padding: 18px; + gap: 14px; +} + +.dispatch-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.dispatch-kicker { + margin: 0 0 4px; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.1em; + font-size: 11px; + font-weight: 700; +} + +.dispatch-header h2 { + margin: 0; + font-size: 24px; + font-weight: 650; +} + +.dispatch-header button, +.dispatch-btn, +.dispatch-select { + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(24, 31, 40, 0.9); + color: var(--text); +} + +.dispatch-header button, +.dispatch-btn { + padding: 10px 14px; + cursor: pointer; +} + +.dispatch-btn-secondary { + background: rgba(53, 40, 39, 0.92); +} + +.dispatch-status { + min-height: 20px; + font-size: 13px; + color: rgba(233, 241, 248, 0.78); +} + +.dispatch-status[data-type="success"] { + color: #79d28a; +} + +.dispatch-status[data-type="error"] { + color: #ff8a80; +} + +.dispatch-metrics { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.metric-card { + padding: 14px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(13, 19, 26, 0.72); +} + +.metric-label { + display: block; + margin-bottom: 8px; + color: rgba(233, 241, 248, 0.6); + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 11px; +} + +.metric-card strong { + font-size: 28px; + font-weight: 700; +} + +.dispatch-grid { + flex: 1; + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + grid-auto-rows: minmax(0, 1fr); + gap: 14px; + min-height: 0; +} + +.dispatch-panel { + display: flex; + flex-direction: column; + min-height: 0; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(11, 17, 24, 0.78); + min-width: 0; +} + +.dispatch-panel-open { + grid-column: span 5; +} + +.dispatch-panel-assigned { + grid-column: span 7; +} + +.dispatch-panel-groups { + grid-column: span 8; +} + +.dispatch-panel-activity { + grid-column: span 4; +} + +.dispatch-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 14px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.dispatch-panel-header h3 { + margin: 0; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--accent); +} + +.dispatch-list { + flex: 1; + overflow: auto; + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px; +} + +.dispatch-card { + padding: 12px; + border: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(19, 26, 34, 0.72); +} + +.dispatch-card-header, +.dispatch-meta { + display: flex; + justify-content: space-between; + gap: 10px; +} + +.dispatch-card-header-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.dispatch-card-header-main { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.dispatch-card-header { + margin-bottom: 8px; +} + +.dispatch-description { + margin: 0 0 10px; + line-height: 1.45; + color: rgba(241, 246, 251, 0.82); + font-size: 13px; +} + +.dispatch-meta { + margin-bottom: 10px; + font-size: 12px; + color: rgba(229, 237, 244, 0.7); +} + +.dispatch-badge { + padding: 3px 7px; + border: 1px solid rgba(91, 187, 255, 0.18); + background: rgba(16, 43, 61, 0.7); + color: var(--accent); + font-size: 11px; + text-transform: uppercase; +} + +.dispatch-icon-btn { + width: 32px; + height: 32px; + padding: 0; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(24, 31, 40, 0.92); + color: var(--text); + cursor: pointer; +} + +.dispatch-icon-btn:hover { + background: rgba(32, 42, 52, 0.96); +} + +.dispatch-actions { + display: flex; + flex-direction: column; + gap: 8px; +} + +.dispatch-actions-split { + margin-top: 10px; +} + +.dispatch-select { + width: 100%; + padding: 9px 10px; +} + +.placeholder-message { + padding: 18px; + text-align: center; + color: rgba(233, 241, 248, 0.6); +} + +.dispatch-modal { + position: fixed; + inset: 0; + z-index: 30; +} + +.dispatch-modal.is-hidden { + display: none; +} + +.dispatch-modal-backdrop { + position: absolute; + inset: 0; + background: rgba(4, 8, 12, 0.72); +} + +.dispatch-modal-dialog { + position: relative; + width: min(480px, calc(100% - 48px)); + margin: 72px auto 0; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(11, 17, 24, 0.98); + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.42); +} + +.dispatch-modal-header, +.dispatch-modal-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 16px; +} + +.dispatch-modal-header { + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.dispatch-modal-header h3 { + margin: 0; + font-size: 22px; + font-weight: 650; +} + +.dispatch-modal-body { + padding: 16px; +} + +.dispatch-meta-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + margin-bottom: 18px; +} + +.dispatch-meta-grid strong { + display: block; + margin-top: 4px; + font-size: 14px; + font-weight: 600; +} + +.dispatch-modal-fields { + display: grid; + gap: 12px; +} + +.dispatch-field { + display: grid; + gap: 6px; +} + +.dispatch-field span { + font-size: 12px; + font-weight: 650; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(233, 241, 248, 0.7); +} + +.dispatch-modal-actions { + justify-content: flex-end; + border-top: 1px solid rgba(255, 255, 255, 0.08); +} diff --git a/arma/client/addons/cad/ui/src/styles/sidepanel.css b/arma/client/addons/cad/ui/src/styles/sidepanel.css index 5224e84..5c3ce15 100644 --- a/arma/client/addons/cad/ui/src/styles/sidepanel.css +++ b/arma/client/addons/cad/ui/src/styles/sidepanel.css @@ -208,3 +208,26 @@ body { .task-secondary-btn { background: rgba(60, 48, 45, 0.92); } + +.roster-summary-card { + padding: 10px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(16, 23, 29, 0.82); +} + +.roster-member-card { + background: rgba(12, 16, 20, 0.74); +} + +.roster-leader-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border: 1px solid rgba(91, 187, 255, 0.28); + background: rgba(15, 40, 58, 0.82); + color: var(--accent); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; +} diff --git a/arma/client/addons/cad/ui/src/styles/topbar.css b/arma/client/addons/cad/ui/src/styles/topbar.css index 3649d05..e361389 100644 --- a/arma/client/addons/cad/ui/src/styles/topbar.css +++ b/arma/client/addons/cad/ui/src/styles/topbar.css @@ -3,65 +3,237 @@ body { top: 0; left: 0; right: 0; - height: 56px; - display: flex; + height: 60px; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; - justify-content: space-between; - padding: 0 20px; + column-gap: 16px; + padding: 0 16px; + background: transparent; + overflow: visible; +} + +body::before { + content: ""; + position: absolute; + inset: 0 0 auto 0; + height: 60px; background: linear-gradient( 90deg, rgba(16, 22, 31, 0.96), rgba(19, 26, 36, 0.94) 55%, rgba(15, 20, 28, 0.96) ); - border-bottom: 1px solid rgba(255, 255, 255, 0.14); - box-shadow: 0 14px 28px rgba(0, 0, 0, 0.28); + border-bottom: none; + box-shadow: none; backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); - overflow: hidden; + z-index: 0; + pointer-events: none; +} + +body > * { + position: relative; + z-index: 1; } .logo { color: var(--accent); - font-size: 16px; + font-size: 15px; font-weight: 650; text-transform: uppercase; - letter-spacing: 0.4px; + letter-spacing: 0.08em; text-shadow: 0 1px 12px rgba(0, 0, 0, 0.35); } +.header-main { + min-width: 0; + display: flex; + align-items: center; + gap: 12px; +} + +.title-block { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; + flex: 0 0 auto; +} + +.title-kicker { + color: rgba(218, 227, 236, 0.56); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.title-main { + color: rgba(245, 248, 255, 0.92); + font-size: 15px; + font-weight: 600; +} + +.operator-strip { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex: 1 1 auto; +} + +.operator-strip.is-hidden, +.operator-controls.is-hidden { + display: none; +} + +.operator-info { + display: flex; + flex-direction: column; + min-width: 88px; + gap: 0; +} + +.operator-label { + color: rgba(218, 227, 236, 0.5); + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.operator-info strong { + color: rgba(245, 248, 255, 0.9); + font-size: 12px; + font-weight: 550; +} + +.operator-controls { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.operator-select { + min-width: 92px; + max-width: 112px; + padding: 5px 8px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(14, 20, 28, 0.96); + color: var(--text); + font-size: 11px; +} + +.btn-operator { + min-width: 84px; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.mode-controls { + display: flex; + gap: 8px; + align-items: center; + justify-self: end; +} + +.mode-controls.is-hidden { + display: none; +} + .controls { display: flex; - gap: 10px; + gap: 8px; + align-items: center; + justify-self: end; +} + +.mode-text { + color: rgba(233, 241, 248, 0.72); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.mode-switch { + position: relative; + width: 54px; + height: 28px; + display: inline-flex; align-items: center; } -.search-input { - background: rgba(255, 255, 255, 0.08); +.mode-switch input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.mode-slider { + position: relative; + width: 54px; + height: 28px; border: 1px solid rgba(255, 255, 255, 0.14); - color: var(--text); - padding: 10px 12px; border-radius: 999px; - width: 250px; - outline: none; - font-size: 13px; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); + background: rgba(22, 29, 39, 0.92); + box-shadow: inset 0 1px 10px rgba(0, 0, 0, 0.22); + transition: + border-color 0.16s ease, + background 0.16s ease; } -.search-input::placeholder { - color: var(--muted2); +.mode-slider::after { + content: ""; + position: absolute; + top: 3px; + left: 3px; + width: 20px; + height: 20px; + border-radius: 50%; + background: linear-gradient( + 180deg, + rgba(237, 244, 251, 0.98), + rgba(189, 205, 221, 0.92) + ); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.26); + transition: + transform 0.16s ease, + background 0.16s ease; } -.search-input:focus { - border-color: rgba(104, 196, 255, 0.45); - background: rgba(255, 255, 255, 0.11); +.mode-switch input:checked + .mode-slider { + border-color: rgba(91, 187, 255, 0.42); + background: rgba(14, 37, 56, 0.95); } -.info { - display: flex; - gap: 20px; - color: rgba(245, 248, 255, 0.84); - font-size: 12px; - font-family: var(--font); - text-shadow: 0 1px 10px rgba(0, 0, 0, 0.28); +.mode-switch input:checked + .mode-slider::after { + transform: translateX(26px); + background: linear-gradient( + 180deg, + rgba(131, 212, 255, 0.98), + rgba(72, 170, 231, 0.94) + ); +} + +.btn-close { + min-width: 42px; +} + +body[data-mode="operations"] { + pointer-events: none; +} + +body[data-mode="operations"] .logo, +body[data-mode="operations"] .title-block, +body[data-mode="operations"] .operator-strip, +body[data-mode="operations"] .operator-controls, +body[data-mode="operations"] .mode-controls, +body[data-mode="operations"] .controls, +body[data-mode="operations"] .mode-switch, +body[data-mode="operations"] .mode-switch *, +body[data-mode="operations"] button, +body[data-mode="operations"] select, +body[data-mode="operations"] label { + pointer-events: auto; } diff --git a/arma/client/addons/cad/ui/src/topbar.html b/arma/client/addons/cad/ui/src/topbar.html index 83f8ffa..0e0db35 100644 --- a/arma/client/addons/cad/ui/src/topbar.html +++ b/arma/client/addons/cad/ui/src/topbar.html @@ -5,20 +5,65 @@ -
- - - - +
+
+ Cad Systems + FORGE Command & Dispatch +
+
-
- X: 0000 Y: 0000 - Scale: 1:1000 + +
+