Implement org credit line debt and bank repayment flow #2
@ -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 {};
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]]
|
||||
|
||||
@ -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]];
|
||||
|
||||
@ -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};
|
||||
};
|
||||
};
|
||||
|
||||
@ -1 +1 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"></head><body><span id="statusText">Map Ready</span> <span id="selectionInfo"></span><script>window.MapLoader={loadCSS:e=>A3API.RequestFile(e).then(e=>{const o=document.createElement("style");o.textContent=e,document.head.appendChild(o)}),loadJS(path){return A3API.RequestFile(path).then(js=>{eval(js)})},loadAll(e){return e.reduce((e,o)=>e.then(()=>o.endsWith(".css")?this.loadCSS(o):o.endsWith(".js")?this.loadJS(o):Promise.resolve()),Promise.resolve())}},MapLoader.loadAll(["forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-bottombar.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-bottombar.js"]).catch(e=>console.error("[BOTTOMBAR] Load error:",e))</script></body></html>
|
||||
<!doctype html><html><head><meta charset="UTF-8"></head><body><span class="footer-brand">CAD Systems by IDS</span> <span class="footer-version">v1.0.0</span><script>window.MapLoader={loadCSS:e=>A3API.RequestFile(e).then(e=>{const o=document.createElement("style");o.textContent=e,document.head.appendChild(o)}),loadJS(path){return A3API.RequestFile(path).then(js=>{eval(js)})},loadAll(e){return e.reduce((e,o)=>e.then(()=>o.endsWith(".css")?this.loadCSS(o):o.endsWith(".js")?this.loadJS(o):Promise.resolve()),Promise.resolve())}},MapLoader.loadAll(["forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-bottombar.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-bottombar.js"]).catch(e=>console.error("[BOTTOMBAR] Load error:",e))</script></body></html>
|
||||
@ -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}
|
||||
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}
|
||||
@ -1 +1 @@
|
||||
window.CADBottombar=window.CADBottombar||{};
|
||||
window.CADBottombar=window.CADBottombar||{init:()=>!0},window.CADBottombar.init();
|
||||
1
arma/client/addons/cad/ui/_site/cad-dispatcher.css
Normal file
1
arma/client/addons/cad/ui/_site/cad-dispatcher.css
Normal file
@ -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}
|
||||
1
arma/client/addons/cad/ui/_site/cad-dispatcher.js
Normal file
1
arma/client/addons/cad/ui/_site/cad-dispatcher.js
Normal file
File diff suppressed because one or more lines are too long
@ -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}
|
||||
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}
|
||||
File diff suppressed because one or more lines are too long
@ -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}
|
||||
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}
|
||||
@ -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)});
|
||||
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();
|
||||
1
arma/client/addons/cad/ui/_site/dispatcher.html
Normal file
1
arma/client/addons/cad/ui/_site/dispatcher.html
Normal file
@ -0,0 +1 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"></head><body><div class="dispatch-shell"><header class="dispatch-header"><div><p class="dispatch-kicker">Dispatch Dashboard</p><h2>Operational Board</h2></div><button id="dispatcherRefreshBtn" type="button">Refresh Board</button></header><div id="dispatcherStatusMessage" class="dispatch-status"></div><section class="dispatch-metrics"><div class="metric-card"><span class="metric-label">Open Contracts</span> <strong id="metricOpenContracts">0</strong></div><div class="metric-card"><span class="metric-label">Assigned Contracts</span> <strong id="metricAssignedContracts">0</strong></div><div class="metric-card"><span class="metric-label">Active Groups</span> <strong id="metricActiveGroups">0</strong></div><div class="metric-card"><span class="metric-label">Groups In Danger</span> <strong id="metricDangerGroups">0</strong></div></section><div class="dispatch-grid"><section class="dispatch-panel dispatch-panel-open"><div class="dispatch-panel-header"><h3>Available Contracts</h3></div><div id="dispatcherOpenContracts" class="dispatch-list"></div></section><section class="dispatch-panel dispatch-panel-assigned"><div class="dispatch-panel-header"><h3>Assigned Contracts</h3></div><div id="dispatcherAssignedContracts" class="dispatch-list"></div></section><section class="dispatch-panel dispatch-panel-groups"><div class="dispatch-panel-header"><h3>Group Board</h3></div><div id="dispatcherGroups" class="dispatch-list"></div></section><section class="dispatch-panel dispatch-panel-activity"><div class="dispatch-panel-header"><h3>Activity Feed</h3></div><div id="dispatcherActivity" class="dispatch-list"></div></section></div><div id="dispatcherGroupModal" class="dispatch-modal is-hidden"><div class="dispatch-modal-backdrop"></div><div class="dispatch-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="dispatcherGroupModalTitle"><div class="dispatch-modal-header"><div><p class="dispatch-kicker">Group Editor</p><h3 id="dispatcherGroupModalTitle">Manage Group</h3></div><button id="dispatcherGroupModalCloseBtn" class="dispatch-icon-btn" type="button" aria-label="Close group editor">x</button></div><div class="dispatch-modal-body"><div class="dispatch-meta-grid"><div><span class="metric-label">Callsign</span> <strong id="dispatcherModalGroupCallsign">-</strong></div><div><span class="metric-label">Leader</span> <strong id="dispatcherModalGroupLeader">-</strong></div><div><span class="metric-label">Current Task</span> <strong id="dispatcherModalGroupTask">None</strong></div><div><span class="metric-label">Org</span> <strong id="dispatcherModalGroupOrg">default</strong></div></div><div class="dispatch-modal-fields"><label class="dispatch-field"><span>Role</span> <select id="dispatcherModalRoleSelect" class="dispatch-select"></select></label> <label class="dispatch-field"><span>Status</span> <select id="dispatcherModalStatusSelect" class="dispatch-select"></select></label></div></div><div class="dispatch-modal-actions"><button id="dispatcherGroupModalSaveBtn" type="button" class="dispatch-btn">Save Changes</button></div></div></div></div><script>window.MapLoader={loadCSS:e=>A3API.RequestFile(e).then(e=>{const d=document.createElement("style");d.textContent=e,document.head.appendChild(d)}),loadJS(path){return A3API.RequestFile(path).then(js=>{eval(js)})},loadAll(e){return e.reduce((e,d)=>e.then(()=>d.endsWith(".css")?this.loadCSS(d):d.endsWith(".js")?this.loadJS(d):Promise.resolve()),Promise.resolve())}},MapLoader.loadAll(["forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-dispatcher.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-dispatcher.js"]).catch(e=>console.error("[DISPATCHER] Load error:",e))</script></body></html>
|
||||
@ -1 +1 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"></head><body><div class="panel-header"><h3>CAD System</h3></div><div class="panel-content"><div class="task-toolbar"><button id="refreshCadBtn" type="button">Refresh Board</button></div><div id="cadStatusMessage" class="task-status-message"></div><div class="cad-tabs" role="tablist" aria-label="CAD Sections"><button id="tabContractsBtn" class="cad-tab is-active" type="button" data-tab="contracts">Contracts</button> <button id="tabGroupsBtn" class="cad-tab" type="button" data-tab="groups">Groups</button> <button id="tabActivityBtn" class="cad-tab" type="button" data-tab="activity">Activity</button></div><div class="cad-tab-panels"><div id="contractsPanel" class="cad-section is-active" data-panel="contracts"><div class="cad-section-header">Contracts</div><div id="taskList" class="task-list"><div class="placeholder-message"><p>Loading contracts...</p></div></div></div><div id="groupsPanel" class="cad-section" data-panel="groups"><div class="cad-section-header">Groups</div><div id="groupList" class="task-list"><div class="placeholder-message"><p>Loading groups...</p></div></div></div><div id="activityPanel" class="cad-section" data-panel="activity"><div class="cad-section-header">Activity</div><div id="activityList" class="task-list"><div class="placeholder-message"><p>No recent activity.</p></div></div></div></div></div><script>window.MapLoader={loadCSS:e=>A3API.RequestFile(e).then(e=>{const d=document.createElement("style");d.textContent=e,document.head.appendChild(d)}),loadJS(path){return A3API.RequestFile(path).then(js=>{eval(js)})},loadAll(e){return e.reduce((e,d)=>e.then(()=>d.endsWith(".css")?this.loadCSS(d):d.endsWith(".js")?this.loadJS(d):Promise.resolve()),Promise.resolve())}},MapLoader.loadAll(["forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-sidepanel.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-sidepanel.js"]).catch(e=>console.error("[SIDEPANEL] Load error:",e))</script></body></html>
|
||||
<!doctype html><html><head><meta charset="UTF-8"></head><body><div class="panel-header"><h3>CAD System</h3></div><div class="panel-content"><div class="task-toolbar"><button id="refreshCadBtn" type="button">Refresh Board</button></div><div id="cadStatusMessage" class="task-status-message"></div><div class="cad-tabs" role="tablist" aria-label="CAD Sections"><button id="tabContractsBtn" class="cad-tab is-active" type="button" data-tab="contracts">Contracts</button> <button id="tabRosterBtn" class="cad-tab" type="button" data-tab="roster">Roster</button> <button id="tabActivityBtn" class="cad-tab" type="button" data-tab="activity">Activity</button></div><div class="cad-tab-panels"><div id="contractsPanel" class="cad-section is-active" data-panel="contracts"><div class="cad-section-header">Contracts</div><div id="taskList" class="task-list"><div class="placeholder-message"><p>Loading contracts...</p></div></div></div><div id="rosterPanel" class="cad-section" data-panel="roster"><div class="cad-section-header">Roster</div><div id="rosterList" class="task-list"><div class="placeholder-message"><p>Loading roster...</p></div></div></div><div id="activityPanel" class="cad-section" data-panel="activity"><div class="cad-section-header">Activity</div><div id="activityList" class="task-list"><div class="placeholder-message"><p>No recent activity.</p></div></div></div></div></div><script>window.MapLoader={loadCSS:e=>A3API.RequestFile(e).then(e=>{const d=document.createElement("style");d.textContent=e,document.head.appendChild(d)}),loadJS(path){return A3API.RequestFile(path).then(js=>{eval(js)})},loadAll(e){return e.reduce((e,d)=>e.then(()=>d.endsWith(".css")?this.loadCSS(d):d.endsWith(".js")?this.loadJS(d):Promise.resolve()),Promise.resolve())}},MapLoader.loadAll(["forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-sidepanel.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-sidepanel.js"]).catch(e=>console.error("[SIDEPANEL] Load error:",e))</script></body></html>
|
||||
@ -1 +1 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"></head><body><div class="logo">FORGE OS</div><div class="controls"><button id="btnZoomIn" class="btn">+</button> <button id="btnZoomOut" class="btn">-</button> <input id="searchBox" placeholder="Search location..." class="search-input"> <button id="btnClose" class="btn btn-close">X</button></div><div class="info"><span id="coordsDisplay">X: 0000 Y: 0000</span> <span id="scaleDisplay">Scale: 1:1000</span></div><script>window.MapLoader={loadCSS:e=>A3API.RequestFile(e).then(e=>{const o=document.createElement("style");o.textContent=e,document.head.appendChild(o)}),loadJS(path){return A3API.RequestFile(path).then(js=>{eval(js)})},loadAll(e){return e.reduce((e,o)=>e.then(()=>o.endsWith(".css")?this.loadCSS(o):o.endsWith(".js")?this.loadJS(o):Promise.resolve()),Promise.resolve())}},MapLoader.loadAll(["forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-topbar.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-topbar.js"]).catch(e=>console.error("[TOPBAR] Load error:",e))</script></body></html>
|
||||
<!doctype html><html><head><meta charset="UTF-8"></head><body><div class="logo">FORGE OS</div><div class="header-main"><div class="title-block"><span class="title-kicker">Cad Systems</span> <strong class="title-main">FORGE Command & Dispatch</strong></div><div id="operatorStrip" class="operator-strip is-hidden"><div class="operator-info"><span class="operator-label">Current Group</span> <strong id="operatorGroupName">No Group</strong></div><div class="operator-info"><span class="operator-label">Location</span> <strong id="operatorLocation">Unavailable</strong></div><div id="operatorControls" class="operator-controls is-hidden"><select id="operatorRoleSelect" class="operator-select"><option value="infantry">infantry</option><option value="recon">recon</option><option value="armor">armor</option><option value="air">air</option><option value="logistics">logistics</option><option value="support">support</option></select> <button id="operatorRoleBtn" class="btn btn-operator" type="button">Update Role</button> <select id="operatorStatusSelect" class="operator-select"><option value="available">available</option><option value="en_route">en route</option><option value="on_task">on task</option><option value="holding">holding</option><option value="danger">danger</option><option value="refit">refit</option><option value="offline">offline</option></select> <button id="operatorStatusBtn" class="btn btn-operator" type="button">Update Status</button></div></div></div><div id="modeControls" class="mode-controls is-hidden"><span class="mode-text">Ops</span> <label class="mode-switch" for="modeToggle"><input id="modeToggle" type="checkbox"> <span class="mode-slider"></span></label> <span class="mode-text">Dispatch</span></div><div class="controls"><button id="btnClose" class="btn btn-close">X</button></div><script>window.MapLoader={loadCSS:e=>A3API.RequestFile(e).then(e=>{const o=document.createElement("style");o.textContent=e,document.head.appendChild(o)}),loadJS(path){return A3API.RequestFile(path).then(js=>{eval(js)})},loadAll(e){return e.reduce((e,o)=>e.then(()=>o.endsWith(".css")?this.loadCSS(o):o.endsWith(".js")?this.loadJS(o):Promise.resolve()),Promise.resolve())}},MapLoader.loadAll(["forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-topbar.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-topbar.js"]).catch(e=>console.error("[TOPBAR] Load error:",e))</script></body></html>
|
||||
@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<span id="statusText">Map Ready</span>
|
||||
<span id="selectionInfo"></span>
|
||||
<span class="footer-brand">CAD Systems by IDS</span>
|
||||
<span class="footer-version">v1.0.0</span>
|
||||
|
||||
<script>
|
||||
window.MapLoader = {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Bottombar UI Component
|
||||
* Displays status and selection information.
|
||||
*/
|
||||
window.CADBottombar = window.CADBottombar || {
|
||||
init() {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
window.CADBottombar = window.CADBottombar || {};
|
||||
window.CADBottombar.init();
|
||||
|
||||
193
arma/client/addons/cad/ui/src/dispatcher.html
Normal file
193
arma/client/addons/cad/ui/src/dispatcher.html
Normal file
@ -0,0 +1,193 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="dispatch-shell">
|
||||
<header class="dispatch-header">
|
||||
<div>
|
||||
<p class="dispatch-kicker">Dispatch Dashboard</p>
|
||||
<h2>Operational Board</h2>
|
||||
</div>
|
||||
<button id="dispatcherRefreshBtn" type="button">
|
||||
Refresh Board
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div id="dispatcherStatusMessage" class="dispatch-status"></div>
|
||||
|
||||
<section class="dispatch-metrics">
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Open Contracts</span>
|
||||
<strong id="metricOpenContracts">0</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Assigned Contracts</span>
|
||||
<strong id="metricAssignedContracts">0</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Active Groups</span>
|
||||
<strong id="metricActiveGroups">0</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Groups In Danger</span>
|
||||
<strong id="metricDangerGroups">0</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="dispatch-grid">
|
||||
<section class="dispatch-panel dispatch-panel-open">
|
||||
<div class="dispatch-panel-header">
|
||||
<h3>Available Contracts</h3>
|
||||
</div>
|
||||
<div
|
||||
id="dispatcherOpenContracts"
|
||||
class="dispatch-list"
|
||||
></div>
|
||||
</section>
|
||||
|
||||
<section class="dispatch-panel dispatch-panel-assigned">
|
||||
<div class="dispatch-panel-header">
|
||||
<h3>Assigned Contracts</h3>
|
||||
</div>
|
||||
<div
|
||||
id="dispatcherAssignedContracts"
|
||||
class="dispatch-list"
|
||||
></div>
|
||||
</section>
|
||||
|
||||
<section class="dispatch-panel dispatch-panel-groups">
|
||||
<div class="dispatch-panel-header">
|
||||
<h3>Group Board</h3>
|
||||
</div>
|
||||
<div id="dispatcherGroups" class="dispatch-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="dispatch-panel dispatch-panel-activity">
|
||||
<div class="dispatch-panel-header">
|
||||
<h3>Activity Feed</h3>
|
||||
</div>
|
||||
<div id="dispatcherActivity" class="dispatch-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="dispatcherGroupModal" class="dispatch-modal is-hidden">
|
||||
<div class="dispatch-modal-backdrop"></div>
|
||||
<div
|
||||
class="dispatch-modal-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="dispatcherGroupModalTitle"
|
||||
>
|
||||
<div class="dispatch-modal-header">
|
||||
<div>
|
||||
<p class="dispatch-kicker">Group Editor</p>
|
||||
<h3 id="dispatcherGroupModalTitle">Manage Group</h3>
|
||||
</div>
|
||||
<button
|
||||
id="dispatcherGroupModalCloseBtn"
|
||||
class="dispatch-icon-btn"
|
||||
type="button"
|
||||
aria-label="Close group editor"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
<div class="dispatch-modal-body">
|
||||
<div class="dispatch-meta-grid">
|
||||
<div>
|
||||
<span class="metric-label">Callsign</span>
|
||||
<strong id="dispatcherModalGroupCallsign"
|
||||
>-</strong
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span class="metric-label">Leader</span>
|
||||
<strong id="dispatcherModalGroupLeader"
|
||||
>-</strong
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span class="metric-label">Current Task</span>
|
||||
<strong id="dispatcherModalGroupTask"
|
||||
>None</strong
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span class="metric-label">Org</span>
|
||||
<strong id="dispatcherModalGroupOrg"
|
||||
>default</strong
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dispatch-modal-fields">
|
||||
<label class="dispatch-field">
|
||||
<span>Role</span>
|
||||
<select
|
||||
id="dispatcherModalRoleSelect"
|
||||
class="dispatch-select"
|
||||
></select>
|
||||
</label>
|
||||
<label class="dispatch-field">
|
||||
<span>Status</span>
|
||||
<select
|
||||
id="dispatcherModalStatusSelect"
|
||||
class="dispatch-select"
|
||||
></select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dispatch-modal-actions">
|
||||
<button
|
||||
id="dispatcherGroupModalSaveBtn"
|
||||
type="button"
|
||||
class="dispatch-btn"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.MapLoader = {
|
||||
loadCSS(path) {
|
||||
return A3API.RequestFile(path).then((css) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
},
|
||||
loadJS(path) {
|
||||
return A3API.RequestFile(path).then((js) => {
|
||||
eval(js);
|
||||
});
|
||||
},
|
||||
loadAll(resources) {
|
||||
return resources.reduce((promise, resource) => {
|
||||
return promise.then(() => {
|
||||
if (resource.endsWith(".css")) {
|
||||
return this.loadCSS(resource);
|
||||
}
|
||||
|
||||
if (resource.endsWith(".js")) {
|
||||
return this.loadJS(resource);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}, Promise.resolve());
|
||||
},
|
||||
};
|
||||
|
||||
MapLoader.loadAll([
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-dispatcher.css",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-dispatcher.js",
|
||||
]).catch((err) => console.error("[DISPATCHER] Load error:", err));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
393
arma/client/addons/cad/ui/src/dispatcher.js
Normal file
393
arma/client/addons/cad/ui/src/dispatcher.js
Normal file
@ -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) =>
|
||||
`<option value="${role}" ${role === group.role ? "selected" : ""}>${role.replaceAll("_", " ")}</option>`,
|
||||
)
|
||||
.join("");
|
||||
document.getElementById("dispatcherModalStatusSelect").innerHTML =
|
||||
this.statuses
|
||||
.map(
|
||||
(status) =>
|
||||
`<option value="${status}" ${status === group.status ? "selected" : ""}>${status.replaceAll("_", " ")}</option>`,
|
||||
)
|
||||
.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 `
|
||||
<button
|
||||
type="button"
|
||||
class="dispatch-icon-btn"
|
||||
onclick="window.cadDispatcher.openGroupModal('${groupID}')"
|
||||
aria-label="Edit group"
|
||||
title="Edit group"
|
||||
>
|
||||
⚙
|
||||
</button>
|
||||
`;
|
||||
},
|
||||
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 =
|
||||
'<div class="placeholder-message"><p>No open contracts.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const groupOptions = this.groups
|
||||
.map(
|
||||
(group) =>
|
||||
`<option value="${group.groupId}">${group.callsign || group.groupId}</option>`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
container.innerHTML = openContracts
|
||||
.map((task) => {
|
||||
const taskId = task.taskId || task.taskID || "";
|
||||
const position = Array.isArray(task.position)
|
||||
? task.position
|
||||
: [0, 0, 0];
|
||||
|
||||
return `
|
||||
<article class="dispatch-card">
|
||||
<header class="dispatch-card-header">
|
||||
<strong>${task.title || taskId}</strong>
|
||||
<span class="dispatch-badge">${task.type || "task"}</span>
|
||||
</header>
|
||||
<p class="dispatch-description">${task.description || ""}</p>
|
||||
<div class="dispatch-meta">
|
||||
<span>Unassigned</span>
|
||||
<span>X: ${Math.round(position[0] || 0)} Y: ${Math.round(position[1] || 0)}</span>
|
||||
</div>
|
||||
<div class="dispatch-actions">
|
||||
<select id="dispatcher-assign-group-${taskId}" class="dispatch-select">
|
||||
<option value="">Assign to group</option>
|
||||
${groupOptions}
|
||||
</select>
|
||||
<button type="button" class="dispatch-btn" onclick="window.cadDispatcher.assignTask('${taskId}')">Assign</button>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
},
|
||||
renderAssignedContracts() {
|
||||
const container = document.getElementById(
|
||||
"dispatcherAssignedContracts",
|
||||
);
|
||||
const assignedContracts = this.contracts.filter(
|
||||
(entry) => (entry.assignmentState || "unassigned") !== "unassigned",
|
||||
);
|
||||
|
||||
if (!assignedContracts.length) {
|
||||
container.innerHTML =
|
||||
'<div class="placeholder-message"><p>No assigned contracts.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = assignedContracts
|
||||
.map((task) => {
|
||||
const taskId = task.taskId || task.taskID || "";
|
||||
const assignedGroup = this.groups.find(
|
||||
(group) => group.groupId === (task.assignedGroupId || ""),
|
||||
);
|
||||
|
||||
return `
|
||||
<article class="dispatch-card">
|
||||
<header class="dispatch-card-header">
|
||||
<strong>${task.title || taskId}</strong>
|
||||
<span class="dispatch-badge">${task.assignmentState || "assigned"}</span>
|
||||
</header>
|
||||
<p class="dispatch-description">${task.description || ""}</p>
|
||||
<div class="dispatch-meta">
|
||||
<span>Group: ${assignedGroup ? assignedGroup.callsign : task.assignedGroupId || "Unknown"}</span>
|
||||
<span>Type: ${task.type || "task"}</span>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
},
|
||||
renderGroups() {
|
||||
const container = document.getElementById("dispatcherGroups");
|
||||
if (!this.groups.length) {
|
||||
container.innerHTML =
|
||||
'<div class="placeholder-message"><p>No active groups available.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = this.groups
|
||||
.map((group) => {
|
||||
return `
|
||||
<article class="dispatch-card dispatch-card-group">
|
||||
<header class="dispatch-card-header">
|
||||
<div class="dispatch-card-header-main">
|
||||
<strong>${group.callsign || group.groupId}</strong>
|
||||
<span class="dispatch-badge">${group.role || "group"}</span>
|
||||
</div>
|
||||
<div class="dispatch-card-header-actions">
|
||||
${this.buildGroupEditorButton(group.groupId)}
|
||||
</div>
|
||||
</header>
|
||||
<div class="dispatch-meta">
|
||||
<span>Leader: ${group.leaderName || "Unknown"}</span>
|
||||
<span>Status: ${group.status || "unknown"}</span>
|
||||
</div>
|
||||
<div class="dispatch-meta">
|
||||
<span>Org: ${group.orgId || "default"}</span>
|
||||
<span>Task: ${group.currentTaskId || "None"}</span>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
},
|
||||
renderActivity() {
|
||||
const container = document.getElementById("dispatcherActivity");
|
||||
if (!this.activity.length) {
|
||||
container.innerHTML =
|
||||
'<div class="placeholder-message"><p>No recent activity.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = this.activity
|
||||
.slice()
|
||||
.reverse()
|
||||
.slice(0, 12)
|
||||
.map(
|
||||
(entry) => `
|
||||
<article class="dispatch-card">
|
||||
<header class="dispatch-card-header">
|
||||
<strong>${entry.type || "activity"}</strong>
|
||||
<span class="dispatch-badge">${Math.round(entry.timestamp || 0)}s</span>
|
||||
</header>
|
||||
<p class="dispatch-description">${entry.message || ""}</p>
|
||||
</article>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
},
|
||||
render() {
|
||||
this.renderMetrics();
|
||||
this.renderOpenContracts();
|
||||
this.renderAssignedContracts();
|
||||
this.renderGroups();
|
||||
this.renderActivity();
|
||||
},
|
||||
};
|
||||
|
||||
window.cadDispatcher.init();
|
||||
@ -22,12 +22,12 @@
|
||||
Contracts
|
||||
</button>
|
||||
<button
|
||||
id="tabGroupsBtn"
|
||||
id="tabRosterBtn"
|
||||
class="cad-tab"
|
||||
type="button"
|
||||
data-tab="groups"
|
||||
data-tab="roster"
|
||||
>
|
||||
Groups
|
||||
Roster
|
||||
</button>
|
||||
<button
|
||||
id="tabActivityBtn"
|
||||
@ -51,11 +51,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="groupsPanel" class="cad-section" data-panel="groups">
|
||||
<div class="cad-section-header">Groups</div>
|
||||
<div id="groupList" class="task-list">
|
||||
<div id="rosterPanel" class="cad-section" data-panel="roster">
|
||||
<div class="cad-section-header">Roster</div>
|
||||
<div id="rosterList" class="task-list">
|
||||
<div class="placeholder-message">
|
||||
<p>Loading groups...</p>
|
||||
<p>Loading roster...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@ window.cadTasks = {
|
||||
groups: [],
|
||||
activity: [],
|
||||
session: {},
|
||||
mode: "operations",
|
||||
activeTab: "contracts",
|
||||
statuses: [
|
||||
"available",
|
||||
@ -13,6 +14,7 @@ window.cadTasks = {
|
||||
"refit",
|
||||
"offline",
|
||||
],
|
||||
roles: ["infantry", "recon", "armor", "air", "logistics", "support"],
|
||||
init() {
|
||||
const refreshBtn = document.getElementById("refreshCadBtn");
|
||||
if (refreshBtn) {
|
||||
@ -66,6 +68,10 @@ window.cadTasks = {
|
||||
payload.session && typeof payload.session === "object"
|
||||
? payload.session
|
||||
: {};
|
||||
this.mode =
|
||||
payload && typeof payload.mode === "string"
|
||||
? payload.mode
|
||||
: "operations";
|
||||
|
||||
const statusEl = document.getElementById("cadStatusMessage");
|
||||
if (
|
||||
@ -97,23 +103,6 @@ window.cadTasks = {
|
||||
this.setStatus("Refreshing board...", "info");
|
||||
window.mapUI.sendEvent("cad::refresh", {});
|
||||
},
|
||||
assignTask(taskID) {
|
||||
const selector = document.getElementById(`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: "",
|
||||
});
|
||||
},
|
||||
acknowledgeTask(taskID) {
|
||||
this.setStatus("Acknowledging contract...", "info");
|
||||
window.mapUI.sendEvent("cad::tasks::acknowledge", { taskID: taskID });
|
||||
@ -129,12 +118,40 @@ window.cadTasks = {
|
||||
status: status,
|
||||
});
|
||||
},
|
||||
updateGroupRole(groupID, role) {
|
||||
this.setStatus("Updating group role...", "info");
|
||||
window.mapUI.sendEvent("cad::groups::role", {
|
||||
groupID: groupID,
|
||||
role: role,
|
||||
});
|
||||
},
|
||||
getPlayerGroupId() {
|
||||
return this.session.groupId || "";
|
||||
},
|
||||
getCurrentGroup() {
|
||||
const currentGroupId = this.getPlayerGroupId();
|
||||
return (
|
||||
this.groups.find((group) => group.groupId === currentGroupId) ||
|
||||
null
|
||||
);
|
||||
},
|
||||
normalizeCollection(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value && typeof value === "object") {
|
||||
return Object.values(value);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
canDispatch() {
|
||||
return !!this.session.isDispatcher;
|
||||
},
|
||||
isDispatchMode() {
|
||||
return this.mode === "dispatch";
|
||||
},
|
||||
isLeader() {
|
||||
return !!this.session.isLeader;
|
||||
},
|
||||
@ -144,14 +161,18 @@ window.cadTasks = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.contracts.length) {
|
||||
const currentGroupId = this.getPlayerGroupId();
|
||||
const visibleContracts = this.contracts.filter(
|
||||
(task) => (task.assignedGroupId || "") === currentGroupId,
|
||||
);
|
||||
|
||||
if (!visibleContracts.length) {
|
||||
listEl.innerHTML =
|
||||
'<div class="placeholder-message"><p>No active contracts are available.</p></div>';
|
||||
'<div class="placeholder-message"><p>No contract is currently assigned to your group.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const currentGroupId = this.getPlayerGroupId();
|
||||
listEl.innerHTML = this.contracts
|
||||
listEl.innerHTML = visibleContracts
|
||||
.map((task) => {
|
||||
const taskId = task.taskId || task.taskID || "";
|
||||
const position = Array.isArray(task.position)
|
||||
@ -164,12 +185,6 @@ window.cadTasks = {
|
||||
);
|
||||
const isAssignedToLeader =
|
||||
this.isLeader() && assignedGroupId === currentGroupId;
|
||||
const groupOptions = this.groups
|
||||
.map(
|
||||
(group) =>
|
||||
`<option value="${group.groupId}">${group.callsign || group.groupId}</option>`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<div class="task-card" data-task-id="${taskId}">
|
||||
@ -182,17 +197,6 @@ window.cadTasks = {
|
||||
<span>${assignmentState === "unassigned" ? "Available" : `${assignmentState}: ${assignedGroup ? assignedGroup.callsign : assignedGroupId}`}</span>
|
||||
<span>X: ${Math.round(position[0] || 0)} Y: ${Math.round(position[1] || 0)}</span>
|
||||
</div>
|
||||
${
|
||||
this.canDispatch()
|
||||
? `<div class="task-action-stack">
|
||||
<select id="assign-group-${taskId}" class="cad-select">
|
||||
<option value="">Assign to group</option>
|
||||
${groupOptions}
|
||||
</select>
|
||||
<button type="button" class="task-accept-btn" onclick="window.cadTasks.assignTask('${taskId}')">Assign Contract</button>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
isAssignedToLeader && assignmentState === "assigned"
|
||||
? `<div class="task-action-row">
|
||||
@ -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 =
|
||||
'<div class="placeholder-message"><p>No active groups are available.</p></div>';
|
||||
'<div class="placeholder-message"><p>Your group is not currently available.</p></div>';
|
||||
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) =>
|
||||
`<option value="${status}" ${status === group.status ? "selected" : ""}>${status.replaceAll("_", " ")}</option>`,
|
||||
)
|
||||
.join("");
|
||||
const roster = this.normalizeCollection(currentGroup.members);
|
||||
|
||||
return `
|
||||
<div class="task-card" data-group-id="${group.groupId}">
|
||||
if (!roster.length) {
|
||||
listEl.innerHTML =
|
||||
'<div class="placeholder-message"><p>No roster members are currently available.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = `
|
||||
<div class="roster-summary-card">
|
||||
<div class="task-card-header">
|
||||
<strong>${currentGroup.callsign || currentGroup.groupId || "Current Group"}</strong>
|
||||
<span class="task-type">${roster.length} member${roster.length === 1 ? "" : "s"}</span>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span>Leader: ${currentGroup.leaderName || "Unknown"}</span>
|
||||
<span>Status: ${currentGroup.status || "unknown"}</span>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span>Role: ${currentGroup.role || "unassigned"}</span>
|
||||
<span>Task: ${currentGroup.currentTaskId || "None"}</span>
|
||||
</div>
|
||||
</div>
|
||||
${roster
|
||||
.map((member) => {
|
||||
const lifeState = (
|
||||
member.lifeState || "unknown"
|
||||
).replaceAll("_", " ");
|
||||
const leaderBadge = member.isLeader
|
||||
? '<span class="roster-leader-badge">Leader</span>'
|
||||
: "";
|
||||
|
||||
return `
|
||||
<div class="task-card roster-member-card" data-member-id="${member.uid || ""}">
|
||||
<div class="task-card-header">
|
||||
<strong>${group.callsign || group.groupId}</strong>
|
||||
<span class="task-type">${group.role || "group"}</span>
|
||||
<strong>${member.name || "Unknown Operator"}</strong>
|
||||
<span class="task-type">${lifeState}</span>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span>Leader: ${group.leaderName || "Unknown"}</span>
|
||||
<span>Status: ${group.status || "unknown"}</span>
|
||||
<span>${member.uid || "No UID"}</span>
|
||||
<span>${leaderBadge}</span>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span>Org: ${group.orgId || "default"}</span>
|
||||
<span>Task: ${group.currentTaskId || "None"}</span>
|
||||
</div>
|
||||
${
|
||||
canUpdate
|
||||
? `<div class="task-action-stack">
|
||||
<select id="group-status-${group.groupId}" class="cad-select">
|
||||
${statusOptions}
|
||||
</select>
|
||||
<button type="button" class="task-accept-btn" onclick="window.cadTasks.updateGroupStatus('${group.groupId}', document.getElementById('group-status-${group.groupId}').value)">Update Status</button>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.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);
|
||||
},
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
339
arma/client/addons/cad/ui/src/styles/dispatcher.css
Normal file
339
arma/client/addons/cad/ui/src/styles/dispatcher.css
Normal file
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -5,20 +5,65 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="logo">FORGE OS</div>
|
||||
<div class="controls">
|
||||
<button id="btnZoomIn" class="btn">+</button>
|
||||
<button id="btnZoomOut" class="btn">-</button>
|
||||
<input
|
||||
type="text"
|
||||
id="searchBox"
|
||||
placeholder="Search location..."
|
||||
class="search-input"
|
||||
/>
|
||||
<button id="btnClose" class="btn btn-close">X</button>
|
||||
<div class="header-main">
|
||||
<div class="title-block">
|
||||
<span class="title-kicker">Cad Systems</span>
|
||||
<strong class="title-main">FORGE Command & Dispatch</strong>
|
||||
</div>
|
||||
<div id="operatorStrip" class="operator-strip is-hidden">
|
||||
<div class="operator-info">
|
||||
<span class="operator-label">Current Group</span>
|
||||
<strong id="operatorGroupName">No Group</strong>
|
||||
</div>
|
||||
<div class="operator-info">
|
||||
<span class="operator-label">Location</span>
|
||||
<strong id="operatorLocation">Unavailable</strong>
|
||||
</div>
|
||||
<div id="operatorControls" class="operator-controls is-hidden">
|
||||
<select id="operatorRoleSelect" class="operator-select">
|
||||
<option value="infantry">infantry</option>
|
||||
<option value="recon">recon</option>
|
||||
<option value="armor">armor</option>
|
||||
<option value="air">air</option>
|
||||
<option value="logistics">logistics</option>
|
||||
<option value="support">support</option>
|
||||
</select>
|
||||
<button
|
||||
id="operatorRoleBtn"
|
||||
class="btn btn-operator"
|
||||
type="button"
|
||||
>
|
||||
Update Role
|
||||
</button>
|
||||
<select id="operatorStatusSelect" class="operator-select">
|
||||
<option value="available">available</option>
|
||||
<option value="en_route">en route</option>
|
||||
<option value="on_task">on task</option>
|
||||
<option value="holding">holding</option>
|
||||
<option value="danger">danger</option>
|
||||
<option value="refit">refit</option>
|
||||
<option value="offline">offline</option>
|
||||
</select>
|
||||
<button
|
||||
id="operatorStatusBtn"
|
||||
class="btn btn-operator"
|
||||
type="button"
|
||||
>
|
||||
Update Status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span id="coordsDisplay">X: 0000 Y: 0000</span>
|
||||
<span id="scaleDisplay">Scale: 1:1000</span>
|
||||
<div id="modeControls" class="mode-controls is-hidden">
|
||||
<span class="mode-text">Ops</span>
|
||||
<label class="mode-switch" for="modeToggle">
|
||||
<input id="modeToggle" type="checkbox" />
|
||||
<span class="mode-slider"></span>
|
||||
</label>
|
||||
<span class="mode-text">Dispatch</span>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button id="btnClose" class="btn btn-close">X</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,17 +1,114 @@
|
||||
document.getElementById("btnZoomIn").addEventListener("click", () => {
|
||||
window.mapUI.sendEvent("map::zoomIn", null);
|
||||
});
|
||||
window.cadTopbar = {
|
||||
mode: "operations",
|
||||
currentGroup: null,
|
||||
session: {},
|
||||
init() {
|
||||
document.getElementById("btnClose").addEventListener("click", () => {
|
||||
window.mapUI.sendEvent("map::close", null);
|
||||
});
|
||||
|
||||
document.getElementById("btnZoomOut").addEventListener("click", () => {
|
||||
window.mapUI.sendEvent("map::zoomOut", null);
|
||||
});
|
||||
document
|
||||
.getElementById("modeToggle")
|
||||
.addEventListener("change", (event) => {
|
||||
window.mapUI.sendEvent("cad::mode::set", {
|
||||
mode: event.target.checked ? "dispatch" : "operations",
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("btnClose").addEventListener("click", () => {
|
||||
window.mapUI.sendEvent("map::close", null);
|
||||
});
|
||||
document
|
||||
.getElementById("operatorRoleBtn")
|
||||
.addEventListener("click", () => {
|
||||
if (!this.currentGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("searchBox").addEventListener("keypress", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
window.mapUI.sendEvent("map::search", event.target.value);
|
||||
}
|
||||
});
|
||||
window.mapUI.sendEvent("cad::groups::role", {
|
||||
groupID: this.currentGroup.groupId || "",
|
||||
role: document.getElementById("operatorRoleSelect").value,
|
||||
});
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("operatorStatusBtn")
|
||||
.addEventListener("click", () => {
|
||||
if (!this.currentGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.mapUI.sendEvent("cad::groups::status", {
|
||||
groupID: this.currentGroup.groupId || "",
|
||||
status: document.getElementById("operatorStatusSelect")
|
||||
.value,
|
||||
});
|
||||
});
|
||||
|
||||
window.mapUI.sendEvent("cad::topbar::ready", {});
|
||||
},
|
||||
formatLocation(group) {
|
||||
const position = Array.isArray(group?.position)
|
||||
? group.position
|
||||
: [0, 0, 0];
|
||||
return `X: ${Math.round(position[0] || 0)
|
||||
.toString()
|
||||
.padStart(4, "0")} Y: ${Math.round(position[1] || 0)
|
||||
.toString()
|
||||
.padStart(4, "0")}`;
|
||||
},
|
||||
receiveState(payload) {
|
||||
this.session =
|
||||
payload && payload.session && typeof payload.session === "object"
|
||||
? payload.session
|
||||
: {};
|
||||
this.mode =
|
||||
payload && typeof payload.mode === "string"
|
||||
? payload.mode
|
||||
: "operations";
|
||||
this.currentGroup =
|
||||
payload &&
|
||||
payload.currentGroup &&
|
||||
typeof payload.currentGroup === "object"
|
||||
? payload.currentGroup
|
||||
: null;
|
||||
|
||||
const modeControls = document.getElementById("modeControls");
|
||||
const canDispatch = !!this.session.isDispatcher;
|
||||
const canOperateGroup =
|
||||
!!this.currentGroup &&
|
||||
(!!this.session.isLeader || !!this.session.isDispatcher);
|
||||
const operatorStrip = document.getElementById("operatorStrip");
|
||||
const operatorControls = document.getElementById("operatorControls");
|
||||
|
||||
modeControls.classList.toggle("is-hidden", !canDispatch);
|
||||
operatorStrip.classList.toggle(
|
||||
"is-hidden",
|
||||
this.mode !== "operations" || !this.currentGroup,
|
||||
);
|
||||
operatorControls.classList.toggle("is-hidden", !canOperateGroup);
|
||||
|
||||
document.body.dataset.mode = this.mode;
|
||||
document.body.dataset.dispatcher = canDispatch ? "true" : "false";
|
||||
|
||||
document.getElementById("modeToggle").checked =
|
||||
this.mode === "dispatch";
|
||||
|
||||
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";
|
||||
|
||||
if (this.currentGroup) {
|
||||
document.getElementById("operatorRoleSelect").value =
|
||||
this.currentGroup.role || "infantry";
|
||||
document.getElementById("operatorStatusSelect").value =
|
||||
this.currentGroup.status || "available";
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
window.cadTopbar.init();
|
||||
|
||||
@ -20,6 +20,11 @@ export default {
|
||||
output: "cad-sidepanel.js",
|
||||
sources: ["src/sidepanel.js"],
|
||||
},
|
||||
{
|
||||
name: "CAD dispatcher app",
|
||||
output: "cad-dispatcher.js",
|
||||
sources: ["src/dispatcher.js"],
|
||||
},
|
||||
{
|
||||
name: "CAD bottombar app",
|
||||
output: "cad-bottombar.js",
|
||||
@ -42,6 +47,11 @@ export default {
|
||||
output: "cad-sidepanel.css",
|
||||
sources: ["src/styles/sidepanel.css"],
|
||||
},
|
||||
{
|
||||
name: "CAD dispatcher styles",
|
||||
output: "cad-dispatcher.css",
|
||||
sources: ["src/styles/dispatcher.css"],
|
||||
},
|
||||
{
|
||||
name: "CAD bottombar styles",
|
||||
output: "cad-bottombar.css",
|
||||
@ -59,6 +69,11 @@ export default {
|
||||
output: "sidepanel.html",
|
||||
source: "src/sidepanel.html",
|
||||
},
|
||||
{
|
||||
name: "CAD dispatcher page",
|
||||
output: "dispatcher.html",
|
||||
source: "src/dispatcher.html",
|
||||
},
|
||||
{
|
||||
name: "CAD bottombar page",
|
||||
output: "bottombar.html",
|
||||
|
||||
@ -3,15 +3,20 @@
|
||||
if (isNil QGVAR(OrgRepository)) then { call FUNC(initRepository); };
|
||||
if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initUIBridge); };
|
||||
|
||||
[QGVAR(initOrg), {
|
||||
GVAR(OrgRepository) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitOrg), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(OrgUIBridge) call ["refreshPortal", []];
|
||||
GVAR(OrgRepository) call ["markLoaded", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncOrg), {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
|
||||
GVAR(OrgRepository) call ["markLoaded", []];
|
||||
GVAR(OrgUIBridge) call ["refreshPortal", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ GVAR(OrgRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
true
|
||||
}],
|
||||
["save", compileFinal {
|
||||
[SRPC(bank,requestSaveOrg), [getPlayerUID player]] call CFUNC(serverEvent);
|
||||
[SRPC(org,requestSaveOrg), [getPlayerUID player]] call CFUNC(serverEvent);
|
||||
_self set ["lastSave", time];
|
||||
}]
|
||||
];
|
||||
|
||||
@ -1 +1,5 @@
|
||||
PREP(initActivityRepository);
|
||||
PREP(initAssignmentRepository);
|
||||
PREP(initCadStore);
|
||||
PREP(initGroupRepository);
|
||||
PREP(initPermissionService);
|
||||
|
||||
@ -84,3 +84,18 @@ call FUNC(initCadStore);
|
||||
[CRPC(cad,responseCadGroupUpdate), [_result], _player] call CFUNC(targetEvent);
|
||||
[CRPC(cad,responseHydrateCad), [GVAR(CadStore) call ["buildHydratePayload", [_uid]]], _player] call CFUNC(targetEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestUpdateCadGroupRole), {
|
||||
params [["_uid", "", [""]], ["_groupID", "", [""]], ["_role", "", [""]]];
|
||||
|
||||
if (_uid isEqualTo "" || { _groupID isEqualTo "" } || { _role isEqualTo "" }) exitWith {
|
||||
["WARNING", "Invalid CAD group role payload."] call EFUNC(common,log);
|
||||
};
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
if (_player isEqualTo objNull) exitWith {};
|
||||
|
||||
private _result = GVAR(CadStore) call ["updateGroupRole", [_uid, _groupID, _role]];
|
||||
[CRPC(cad,responseCadGroupUpdate), [_result], _player] call CFUNC(targetEvent);
|
||||
[CRPC(cad,responseHydrateCad), [GVAR(CadStore) call ["buildHydratePayload", [_uid]]], _player] call CFUNC(targetEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initActivityRepository.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-30
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the CAD activity repository for recent operational events.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* CAD activity repository object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_server_cad_fnc_initActivityRepository
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(ActivityRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "CadActivityRepositoryBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["activityRegistry", []];
|
||||
}],
|
||||
["appendActivity", compileFinal {
|
||||
params [
|
||||
["_type", "", [""]],
|
||||
["_message", "", [""]],
|
||||
["_taskID", "", [""]],
|
||||
["_groupID", "", [""]],
|
||||
["_actorUid", "", [""]]
|
||||
];
|
||||
|
||||
if (_type isEqualTo "" || { _message isEqualTo "" }) exitWith { false };
|
||||
|
||||
private _activityRegistry = +(_self getOrDefault ["activityRegistry", []]);
|
||||
_activityRegistry pushBack createHashMapFromArray [
|
||||
["type", _type],
|
||||
["message", _message],
|
||||
["timestamp", serverTime],
|
||||
["taskId", _taskID],
|
||||
["groupId", _groupID],
|
||||
["actorUid", _actorUid]
|
||||
];
|
||||
|
||||
if ((count _activityRegistry) > 50) then {
|
||||
_activityRegistry deleteRange [0, (count _activityRegistry) - 50];
|
||||
};
|
||||
|
||||
_self set ["activityRegistry", _activityRegistry];
|
||||
true
|
||||
}],
|
||||
["getActivity", compileFinal {
|
||||
+(_self getOrDefault ["activityRegistry", []])
|
||||
}]
|
||||
];
|
||||
|
||||
createHashMapObject [GVAR(ActivityRepositoryBaseClass)]
|
||||
@ -0,0 +1,243 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initAssignmentRepository.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-30
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the CAD assignment repository for contract assignment
|
||||
* state and dispatcher/group-leader task actions.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* CAD assignment repository object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_server_cad_fnc_initAssignmentRepository
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "CadAssignmentRepositoryBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["assignmentRegistry", createHashMap];
|
||||
}],
|
||||
["pruneAssignments", compileFinal {
|
||||
private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
|
||||
private _keysToRemove = [];
|
||||
|
||||
{
|
||||
private _status = EGVAR(task,TaskStore) call ["getTaskStatus", [_x]];
|
||||
if !(_status in ["active", ""]) then {
|
||||
_keysToRemove pushBack _x;
|
||||
};
|
||||
} forEach _assignmentRegistry;
|
||||
|
||||
{
|
||||
_assignmentRegistry deleteAt _x;
|
||||
} forEach _keysToRemove;
|
||||
|
||||
_self set ["assignmentRegistry", _assignmentRegistry];
|
||||
count _keysToRemove
|
||||
}],
|
||||
["getAssignments", compileFinal {
|
||||
values (_self getOrDefault ["assignmentRegistry", createHashMap])
|
||||
}],
|
||||
["buildContracts", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
_self call ["pruneAssignments", []];
|
||||
|
||||
private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
|
||||
private _contracts = [];
|
||||
private _permissionService = _self getOrDefault ["permissionService", createHashMap];
|
||||
private _groupRepository = _self getOrDefault ["groupRepository", createHashMap];
|
||||
private _canDispatch = _permissionService call ["canDispatch", [_uid]];
|
||||
private _playerGroupId = _groupRepository call ["getPlayerGroupId", [_uid]];
|
||||
|
||||
{
|
||||
private _taskID = _x getOrDefault ["taskID", ""];
|
||||
if (_taskID isEqualTo "") then { continue; };
|
||||
|
||||
private _assignment = _assignmentRegistry getOrDefault [_taskID, createHashMap];
|
||||
private _entry = +_x;
|
||||
_entry set ["taskId", _taskID];
|
||||
_entry set ["assignedGroupId", _assignment getOrDefault ["groupId", ""]];
|
||||
_entry set ["assignmentState", [_assignment getOrDefault ["state", ""], "unassigned"] select (_assignment isEqualTo createHashMap)];
|
||||
|
||||
if (!_canDispatch) then {
|
||||
private _assignedGroupId = _entry getOrDefault ["assignedGroupId", ""];
|
||||
if (_assignedGroupId isEqualTo "") then { continue; };
|
||||
if (_assignedGroupId isNotEqualTo _playerGroupId) then { continue; };
|
||||
};
|
||||
|
||||
_contracts pushBack _entry;
|
||||
} forEach (EGVAR(task,TaskStore) call ["getActiveTaskCatalog", []]);
|
||||
|
||||
_contracts
|
||||
}],
|
||||
["assignTaskToGroup", compileFinal {
|
||||
params [
|
||||
["_requesterUid", "", [""]],
|
||||
["_taskID", "", [""]],
|
||||
["_groupID", "", [""]],
|
||||
["_note", "", [""]]
|
||||
];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Unable to assign task."],
|
||||
["assignment", createHashMap]
|
||||
];
|
||||
|
||||
private _permissionService = _self getOrDefault ["permissionService", createHashMap];
|
||||
if !(_permissionService call ["canDispatch", [_requesterUid]]) exitWith {
|
||||
_result set ["message", "You are not authorized to assign contracts."];
|
||||
_result
|
||||
};
|
||||
|
||||
if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_taskID]]) isNotEqualTo "active") exitWith {
|
||||
_result set ["message", "Task is no longer active."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _groupRepository = _self getOrDefault ["groupRepository", createHashMap];
|
||||
private _groupRecord = _groupRepository call ["getGroupRecord", [_groupID]];
|
||||
if (_groupRecord isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Selected group is unavailable."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _leaderUid = _groupRecord getOrDefault ["leaderUid", ""];
|
||||
if (_leaderUid isEqualTo "") exitWith {
|
||||
_result set ["message", "Selected group has no online leader."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer);
|
||||
private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
|
||||
private _assignment = createHashMapFromArray [
|
||||
["taskId", _taskID],
|
||||
["groupId", _groupID],
|
||||
["assignedByUid", _requesterUid],
|
||||
["assignedByName", ["Dispatcher", name _requesterPlayer] select (_requesterPlayer isNotEqualTo objNull)],
|
||||
["assignedAt", serverTime],
|
||||
["state", "assigned"],
|
||||
["note", _note]
|
||||
];
|
||||
|
||||
_assignmentRegistry set [_taskID, _assignment];
|
||||
_self set ["assignmentRegistry", _assignmentRegistry];
|
||||
|
||||
private _activityRepository = _self getOrDefault ["activityRepository", createHashMap];
|
||||
_activityRepository call ["appendActivity", [
|
||||
"task_assigned",
|
||||
format ["%1 assigned %2 to %3.", _assignment get "assignedByName", _taskID, _groupRecord getOrDefault ["callsign", _groupID]],
|
||||
_taskID,
|
||||
_groupID,
|
||||
_requesterUid
|
||||
]];
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", "Task assigned."];
|
||||
_result set ["assignment", _assignment];
|
||||
_result set ["leaderUid", _leaderUid];
|
||||
_result
|
||||
}],
|
||||
["acknowledgeTask", compileFinal {
|
||||
params [["_requesterUid", "", [""]], ["_taskID", "", [""]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Unable to acknowledge task."],
|
||||
["assignment", createHashMap]
|
||||
];
|
||||
|
||||
private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
|
||||
private _assignment = +(_assignmentRegistry getOrDefault [_taskID, createHashMap]);
|
||||
if (_assignment isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Task is not assigned."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _groupID = _assignment getOrDefault ["groupId", ""];
|
||||
private _groupRepository = _self getOrDefault ["groupRepository", createHashMap];
|
||||
if !(_groupRepository call ["isGroupLeader", [_requesterUid, _groupID]]) exitWith {
|
||||
_result set ["message", "Only the assigned group leader can acknowledge this task."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _bindResult = EGVAR(task,TaskStore) call ["bindTaskOwnership", [_taskID, _requesterUid]];
|
||||
if !(_bindResult getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _bindResult getOrDefault ["message", "Failed to bind task ownership."]];
|
||||
_result
|
||||
};
|
||||
|
||||
_assignment set ["state", "acknowledged"];
|
||||
_assignment set ["acknowledgedAt", serverTime];
|
||||
_assignmentRegistry set [_taskID, _assignment];
|
||||
_self set ["assignmentRegistry", _assignmentRegistry];
|
||||
|
||||
private _activityRepository = _self getOrDefault ["activityRepository", createHashMap];
|
||||
_activityRepository call ["appendActivity", [
|
||||
"task_acknowledged",
|
||||
format ["%1 acknowledged %2.", _requesterUid, _taskID],
|
||||
_taskID,
|
||||
_groupID,
|
||||
_requesterUid
|
||||
]];
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", "Task acknowledged."];
|
||||
_result set ["assignment", _assignment];
|
||||
_result
|
||||
}],
|
||||
["declineTask", compileFinal {
|
||||
params [["_requesterUid", "", [""]], ["_taskID", "", [""]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Unable to decline task."],
|
||||
["assignment", createHashMap]
|
||||
];
|
||||
|
||||
private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
|
||||
private _assignment = +(_assignmentRegistry getOrDefault [_taskID, createHashMap]);
|
||||
if (_assignment isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Task is not assigned."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _groupID = _assignment getOrDefault ["groupId", ""];
|
||||
private _groupRepository = _self getOrDefault ["groupRepository", createHashMap];
|
||||
if !(_groupRepository call ["isGroupLeader", [_requesterUid, _groupID]]) exitWith {
|
||||
_result set ["message", "Only the assigned group leader can decline this task."];
|
||||
_result
|
||||
};
|
||||
|
||||
_assignment set ["state", "declined"];
|
||||
_assignment set ["declinedAt", serverTime];
|
||||
_assignmentRegistry deleteAt _taskID;
|
||||
_self set ["assignmentRegistry", _assignmentRegistry];
|
||||
|
||||
private _activityRepository = _self getOrDefault ["activityRepository", createHashMap];
|
||||
_activityRepository call ["appendActivity", [
|
||||
"task_declined",
|
||||
format ["%1 declined %2.", _requesterUid, _taskID],
|
||||
_taskID,
|
||||
_groupID,
|
||||
_requesterUid
|
||||
]];
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", "Task declined and returned to the contract board."];
|
||||
_result set ["assignment", _assignment];
|
||||
_result
|
||||
}]
|
||||
];
|
||||
|
||||
createHashMapObject [GVAR(AssignmentRepositoryBaseClass)]
|
||||
@ -7,8 +7,8 @@
|
||||
* Public: Yes
|
||||
*
|
||||
* Description:
|
||||
* Initializes the CAD store for group tracking, assignment state,
|
||||
* activity history, and CAD hydrate payloads.
|
||||
* Initializes the CAD store as a coordinator over activity, group,
|
||||
* assignment, and permission domain objects.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
@ -24,254 +24,58 @@
|
||||
GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "CadStoreBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["groupRegistry", createHashMap];
|
||||
_self set ["assignmentRegistry", createHashMap];
|
||||
_self set ["activityRegistry", []];
|
||||
_self set ["validStatuses", [
|
||||
"available",
|
||||
"en_route",
|
||||
"on_task",
|
||||
"holding",
|
||||
"danger",
|
||||
"refit",
|
||||
"offline"
|
||||
]];
|
||||
private _activityRepository = call FUNC(initActivityRepository);
|
||||
private _permissionService = call FUNC(initPermissionService);
|
||||
private _groupRepository = call FUNC(initGroupRepository);
|
||||
private _assignmentRepository = call FUNC(initAssignmentRepository);
|
||||
|
||||
_groupRepository set ["activityRepository", _activityRepository];
|
||||
_groupRepository set ["assignmentRepository", _assignmentRepository];
|
||||
_groupRepository set ["permissionService", _permissionService];
|
||||
|
||||
_assignmentRepository set ["activityRepository", _activityRepository];
|
||||
_assignmentRepository set ["groupRepository", _groupRepository];
|
||||
_assignmentRepository set ["permissionService", _permissionService];
|
||||
|
||||
_self set ["ActivityRepository", _activityRepository];
|
||||
_self set ["PermissionService", _permissionService];
|
||||
_self set ["GroupRepository", _groupRepository];
|
||||
_self set ["AssignmentRepository", _assignmentRepository];
|
||||
|
||||
["INFO", "CAD Store Initialized!"] call EFUNC(common,log);
|
||||
}],
|
||||
["appendActivity", compileFinal {
|
||||
params [
|
||||
["_type", "", [""]],
|
||||
["_message", "", [""]],
|
||||
["_taskID", "", [""]],
|
||||
["_groupID", "", [""]],
|
||||
["_actorUid", "", [""]]
|
||||
];
|
||||
|
||||
if (_type isEqualTo "" || { _message isEqualTo "" }) exitWith { false };
|
||||
|
||||
private _activityRegistry = +(_self getOrDefault ["activityRegistry", []]);
|
||||
_activityRegistry pushBack createHashMapFromArray [
|
||||
["type", _type],
|
||||
["message", _message],
|
||||
["timestamp", serverTime],
|
||||
["taskId", _taskID],
|
||||
["groupId", _groupID],
|
||||
["actorUid", _actorUid]
|
||||
];
|
||||
|
||||
if ((count _activityRegistry) > 50) then {
|
||||
_activityRegistry deleteRange [0, (count _activityRegistry) - 50];
|
||||
};
|
||||
|
||||
_self set ["activityRegistry", _activityRegistry];
|
||||
true
|
||||
(_self get "ActivityRepository") call ["appendActivity", _this]
|
||||
}],
|
||||
["resolveGroupId", compileFinal {
|
||||
params [["_group", grpNull, [grpNull]]];
|
||||
|
||||
if (isNull _group) exitWith { "" };
|
||||
|
||||
private _leader = leader _group;
|
||||
private _leaderUid = if (isNull _leader) then { "" } else { getPlayerUID _leader };
|
||||
if (_leaderUid isNotEqualTo "") exitWith { format ["group:%1", _leaderUid] };
|
||||
|
||||
private _groupLabel = groupId _group;
|
||||
if (_groupLabel isNotEqualTo "") exitWith { format ["group:%1", _groupLabel] };
|
||||
|
||||
str _group
|
||||
(_self get "GroupRepository") call ["resolveGroupId", _this]
|
||||
}],
|
||||
["canDispatch", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
if (_uid isEqualTo "") exitWith { false };
|
||||
|
||||
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
|
||||
if (_actor isEqualTo createHashMap) then {
|
||||
_actor = EGVAR(actor,ActorStore) call ["init", [_uid]];
|
||||
};
|
||||
|
||||
private _orgID = _actor getOrDefault ["organization", "default"];
|
||||
private _org = EGVAR(org,Registry) getOrDefault [_orgID, createHashMap];
|
||||
if (_org isEqualTo createHashMap) then {
|
||||
_org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
|
||||
};
|
||||
|
||||
if (_org getOrDefault ["owner", ""] isEqualTo _uid) exitWith { true };
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
if (_player isEqualTo objNull) exitWith { false };
|
||||
|
||||
(_orgID isEqualTo "default") && { vehicleVarName _player isEqualTo "ceo" }
|
||||
(_self get "PermissionService") call ["canDispatch", _this]
|
||||
}],
|
||||
["getCurrentTaskIdForGroup", compileFinal {
|
||||
params [["_groupID", "", [""]]];
|
||||
|
||||
if (_groupID isEqualTo "") exitWith { "" };
|
||||
|
||||
private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
|
||||
private _taskID = "";
|
||||
{
|
||||
if ((_y getOrDefault ["groupId", ""]) isNotEqualTo _groupID) then { continue; };
|
||||
if !((_y getOrDefault ["state", ""]) in ["assigned", "acknowledged"]) then { continue; };
|
||||
if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; };
|
||||
|
||||
_taskID = _x;
|
||||
} forEach _assignmentRegistry;
|
||||
|
||||
_taskID
|
||||
(_self get "GroupRepository") call ["getCurrentTaskIdForGroup", _this]
|
||||
}],
|
||||
["syncGroups", compileFinal {
|
||||
private _previousRegistry = _self getOrDefault ["groupRegistry", createHashMap];
|
||||
private _nextRegistry = createHashMap;
|
||||
|
||||
{
|
||||
if (side _x isNotEqualTo west) then { continue; };
|
||||
|
||||
private _members = (units _x) select { isPlayer _x };
|
||||
if (_members isEqualTo []) then { continue; };
|
||||
|
||||
private _leader = leader _x;
|
||||
if (isNull _leader || { !isPlayer _leader }) then {
|
||||
_leader = _members # 0;
|
||||
};
|
||||
|
||||
private _groupID = _self call ["resolveGroupId", [_x]];
|
||||
if (_groupID isEqualTo "") then { continue; };
|
||||
|
||||
private _leaderUid = getPlayerUID _leader;
|
||||
private _actor = EGVAR(actor,Registry) getOrDefault [_leaderUid, createHashMap];
|
||||
if (_actor isEqualTo createHashMap && { _leaderUid isNotEqualTo "" }) then {
|
||||
_actor = EGVAR(actor,ActorStore) call ["init", [_leaderUid]];
|
||||
};
|
||||
|
||||
private _orgID = _actor getOrDefault ["organization", "default"];
|
||||
if (_orgID isEqualTo "") then { _orgID = "default"; };
|
||||
|
||||
private _existingRecord = +(_previousRegistry getOrDefault [_groupID, createHashMap]);
|
||||
private _memberUids = [];
|
||||
{
|
||||
private _memberUid = getPlayerUID _x;
|
||||
if (_memberUid isNotEqualTo "") then {
|
||||
_memberUids pushBack _memberUid;
|
||||
};
|
||||
} forEach _members;
|
||||
|
||||
private _record = createHashMapFromArray [
|
||||
["groupId", _groupID],
|
||||
["callsign", [groupId _x, _groupID] select ((groupId _x) isEqualTo "")],
|
||||
["leaderUid", _leaderUid],
|
||||
["leaderName", name _leader],
|
||||
["memberUids", _memberUids],
|
||||
["orgId", _orgID],
|
||||
["role", _existingRecord getOrDefault ["role", "infantry"]],
|
||||
["status", _existingRecord getOrDefault ["status", "available"]],
|
||||
["position", getPosATL _leader],
|
||||
["currentTaskId", _self call ["getCurrentTaskIdForGroup", [_groupID]]],
|
||||
["lastUpdate", serverTime]
|
||||
];
|
||||
|
||||
_nextRegistry set [_groupID, _record];
|
||||
} forEach allGroups;
|
||||
|
||||
_self set ["groupRegistry", _nextRegistry];
|
||||
_nextRegistry
|
||||
(_self get "GroupRepository") call ["syncGroups", _this]
|
||||
}],
|
||||
["getGroupRecord", compileFinal {
|
||||
params [["_groupID", "", [""]]];
|
||||
|
||||
if (_groupID isEqualTo "") exitWith { createHashMap };
|
||||
|
||||
private _groupRegistry = _self call ["syncGroups", []];
|
||||
+(_groupRegistry getOrDefault [_groupID, createHashMap])
|
||||
(_self get "GroupRepository") call ["getGroupRecord", _this]
|
||||
}],
|
||||
["getPlayerGroupId", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
if (_uid isEqualTo "") exitWith { "" };
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
if (_player isEqualTo objNull) exitWith { "" };
|
||||
|
||||
_self call ["resolveGroupId", [group _player]]
|
||||
(_self get "GroupRepository") call ["getPlayerGroupId", _this]
|
||||
}],
|
||||
["isGroupLeader", compileFinal {
|
||||
params [["_uid", "", [""]], ["_groupID", "", [""]]];
|
||||
|
||||
if (_uid isEqualTo "" || { _groupID isEqualTo "" }) exitWith { false };
|
||||
|
||||
private _groupRecord = _self call ["getGroupRecord", [_groupID]];
|
||||
(_groupRecord getOrDefault ["leaderUid", ""]) isEqualTo _uid
|
||||
(_self get "GroupRepository") call ["isGroupLeader", _this]
|
||||
}],
|
||||
["pruneAssignments", compileFinal {
|
||||
private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
|
||||
private _keysToRemove = [];
|
||||
|
||||
{
|
||||
private _status = EGVAR(task,TaskStore) call ["getTaskStatus", [_x]];
|
||||
if !(_status in ["active", ""]) then {
|
||||
_keysToRemove pushBack _x;
|
||||
};
|
||||
} forEach _assignmentRegistry;
|
||||
|
||||
{
|
||||
_assignmentRegistry deleteAt _x;
|
||||
} forEach _keysToRemove;
|
||||
|
||||
_self set ["assignmentRegistry", _assignmentRegistry];
|
||||
count _keysToRemove
|
||||
(_self get "AssignmentRepository") call ["pruneAssignments", _this]
|
||||
}],
|
||||
["buildContracts", compileFinal {
|
||||
_self call ["pruneAssignments", []];
|
||||
|
||||
private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
|
||||
private _contracts = [];
|
||||
|
||||
{
|
||||
private _taskID = _x getOrDefault ["taskID", ""];
|
||||
if (_taskID isEqualTo "") then { continue; };
|
||||
|
||||
private _assignment = _assignmentRegistry getOrDefault [_taskID, createHashMap];
|
||||
private _entry = +_x;
|
||||
_entry set ["taskId", _taskID];
|
||||
_entry set ["assignedGroupId", _assignment getOrDefault ["groupId", ""]];
|
||||
_entry set ["assignmentState", [_assignment getOrDefault ["state", ""], "unassigned"] select (_assignment isEqualTo createHashMap)];
|
||||
_contracts pushBack _entry;
|
||||
} forEach (EGVAR(task,TaskStore) call ["getActiveTaskCatalog", []]);
|
||||
|
||||
_contracts
|
||||
(_self get "AssignmentRepository") call ["buildContracts", _this]
|
||||
}],
|
||||
["buildGroups", compileFinal {
|
||||
private _groupRegistry = _self call ["syncGroups", []];
|
||||
private _groups = [];
|
||||
|
||||
{
|
||||
_groups pushBack +_y;
|
||||
} forEach _groupRegistry;
|
||||
|
||||
_groups
|
||||
}],
|
||||
["buildHydratePayload", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
private _activity = +(_self getOrDefault ["activityRegistry", []]);
|
||||
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
|
||||
if (_actor isEqualTo createHashMap && { _uid isNotEqualTo "" }) then {
|
||||
_actor = EGVAR(actor,ActorStore) call ["init", [_uid]];
|
||||
};
|
||||
|
||||
createHashMapFromArray [
|
||||
["groups", _self call ["buildGroups", []]],
|
||||
["contracts", _self call ["buildContracts", []]],
|
||||
["assignments", values (_self getOrDefault ["assignmentRegistry", createHashMap])],
|
||||
["activity", _activity],
|
||||
["session", createHashMapFromArray [
|
||||
["uid", _uid],
|
||||
["orgId", _actor getOrDefault ["organization", "default"]],
|
||||
["isDispatcher", _self call ["canDispatch", [_uid]]],
|
||||
["groupId", _self call ["getPlayerGroupId", [_uid]]],
|
||||
["isLeader", _self call ["isGroupLeader", [_uid, _self call ["getPlayerGroupId", [_uid]]]]]
|
||||
]]
|
||||
]
|
||||
(_self get "GroupRepository") call ["buildGroups", _this]
|
||||
}],
|
||||
["notifyPlayer", compileFinal {
|
||||
params [
|
||||
@ -290,63 +94,12 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
|
||||
true
|
||||
}],
|
||||
["assignTaskToGroup", compileFinal {
|
||||
params [
|
||||
["_requesterUid", "", [""]],
|
||||
["_taskID", "", [""]],
|
||||
["_groupID", "", [""]],
|
||||
["_note", "", [""]]
|
||||
];
|
||||
private _result = (_self get "AssignmentRepository") call ["assignTaskToGroup", _this];
|
||||
if !(_result getOrDefault ["success", false]) exitWith { _result };
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Unable to assign task."],
|
||||
["assignment", createHashMap]
|
||||
];
|
||||
|
||||
if !(_self call ["canDispatch", [_requesterUid]]) exitWith {
|
||||
_result set ["message", "You are not authorized to assign contracts."];
|
||||
_result
|
||||
};
|
||||
|
||||
if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_taskID]]) isNotEqualTo "active") exitWith {
|
||||
_result set ["message", "Task is no longer active."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _groupRecord = _self call ["getGroupRecord", [_groupID]];
|
||||
if (_groupRecord isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Selected group is unavailable."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _leaderUid = _groupRecord getOrDefault ["leaderUid", ""];
|
||||
if (_leaderUid isEqualTo "") exitWith {
|
||||
_result set ["message", "Selected group has no online leader."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer);
|
||||
private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
|
||||
private _assignment = createHashMapFromArray [
|
||||
["taskId", _taskID],
|
||||
["groupId", _groupID],
|
||||
["assignedByUid", _requesterUid],
|
||||
["assignedByName", ["Dispatcher", name _requesterPlayer] select (_requesterPlayer isNotEqualTo objNull)],
|
||||
["assignedAt", serverTime],
|
||||
["state", "assigned"],
|
||||
["note", _note]
|
||||
];
|
||||
|
||||
_assignmentRegistry set [_taskID, _assignment];
|
||||
_self set ["assignmentRegistry", _assignmentRegistry];
|
||||
|
||||
_self call ["appendActivity", [
|
||||
"task_assigned",
|
||||
format ["%1 assigned %2 to %3.", _assignment get "assignedByName", _taskID, _groupRecord getOrDefault ["callsign", _groupID]],
|
||||
_taskID,
|
||||
_groupID,
|
||||
_requesterUid
|
||||
]];
|
||||
private _assignment = _result getOrDefault ["assignment", createHashMap];
|
||||
private _taskID = _assignment getOrDefault ["taskId", ""];
|
||||
private _leaderUid = _result getOrDefault ["leaderUid", ""];
|
||||
|
||||
_self call ["notifyPlayer", [
|
||||
_leaderUid,
|
||||
@ -355,142 +108,48 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
|
||||
format ["Contract assigned: %1. Open CAD to review and acknowledge.", _taskID]
|
||||
]];
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", "Task assigned."];
|
||||
_result set ["assignment", _assignment];
|
||||
_result
|
||||
}],
|
||||
["acknowledgeTask", compileFinal {
|
||||
params [["_requesterUid", "", [""]], ["_taskID", "", [""]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Unable to acknowledge task."],
|
||||
["assignment", createHashMap]
|
||||
];
|
||||
|
||||
private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
|
||||
private _assignment = +(_assignmentRegistry getOrDefault [_taskID, createHashMap]);
|
||||
if (_assignment isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Task is not assigned."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _groupID = _assignment getOrDefault ["groupId", ""];
|
||||
if !(_self call ["isGroupLeader", [_requesterUid, _groupID]]) exitWith {
|
||||
_result set ["message", "Only the assigned group leader can acknowledge this task."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _bindResult = EGVAR(task,TaskStore) call ["bindTaskOwnership", [_taskID, _requesterUid]];
|
||||
if !(_bindResult getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _bindResult getOrDefault ["message", "Failed to bind task ownership."]];
|
||||
_result
|
||||
};
|
||||
|
||||
_assignment set ["state", "acknowledged"];
|
||||
_assignment set ["acknowledgedAt", serverTime];
|
||||
_assignmentRegistry set [_taskID, _assignment];
|
||||
_self set ["assignmentRegistry", _assignmentRegistry];
|
||||
|
||||
_self call ["appendActivity", [
|
||||
"task_acknowledged",
|
||||
format ["%1 acknowledged %2.", _requesterUid, _taskID],
|
||||
_taskID,
|
||||
_groupID,
|
||||
_requesterUid
|
||||
]];
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", "Task acknowledged."];
|
||||
_result set ["assignment", _assignment];
|
||||
_result
|
||||
(_self get "AssignmentRepository") call ["acknowledgeTask", _this]
|
||||
}],
|
||||
["declineTask", compileFinal {
|
||||
params [["_requesterUid", "", [""]], ["_taskID", "", [""]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Unable to decline task."],
|
||||
["assignment", createHashMap]
|
||||
];
|
||||
|
||||
private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
|
||||
private _assignment = +(_assignmentRegistry getOrDefault [_taskID, createHashMap]);
|
||||
if (_assignment isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Task is not assigned."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _groupID = _assignment getOrDefault ["groupId", ""];
|
||||
if !(_self call ["isGroupLeader", [_requesterUid, _groupID]]) exitWith {
|
||||
_result set ["message", "Only the assigned group leader can decline this task."];
|
||||
_result
|
||||
};
|
||||
|
||||
_assignment set ["state", "declined"];
|
||||
_assignment set ["declinedAt", serverTime];
|
||||
_assignmentRegistry set [_taskID, _assignment];
|
||||
_self set ["assignmentRegistry", _assignmentRegistry];
|
||||
|
||||
_self call ["appendActivity", [
|
||||
"task_declined",
|
||||
format ["%1 declined %2.", _requesterUid, _taskID],
|
||||
_taskID,
|
||||
_groupID,
|
||||
_requesterUid
|
||||
]];
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", "Task declined."];
|
||||
_result set ["assignment", _assignment];
|
||||
_result
|
||||
(_self get "AssignmentRepository") call ["declineTask", _this]
|
||||
}],
|
||||
["updateGroupStatus", compileFinal {
|
||||
params [["_requesterUid", "", [""]], ["_groupID", "", [""]], ["_status", "", [""]]];
|
||||
(_self get "GroupRepository") call ["updateGroupStatus", _this]
|
||||
}],
|
||||
["updateGroupRole", compileFinal {
|
||||
(_self get "GroupRepository") call ["updateGroupRole", _this]
|
||||
}],
|
||||
["buildHydratePayload", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Unable to update group status."],
|
||||
["group", createHashMap]
|
||||
];
|
||||
private _activityRepository = _self get "ActivityRepository";
|
||||
private _permissionService = _self get "PermissionService";
|
||||
private _groupRepository = _self get "GroupRepository";
|
||||
private _assignmentRepository = _self get "AssignmentRepository";
|
||||
|
||||
private _finalStatus = toLowerANSI _status;
|
||||
if !(_finalStatus in (_self getOrDefault ["validStatuses", []])) exitWith {
|
||||
_result set ["message", "Invalid group status."];
|
||||
_result
|
||||
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
|
||||
if (_actor isEqualTo createHashMap && { _uid isNotEqualTo "" }) then {
|
||||
_actor = EGVAR(actor,ActorStore) call ["init", [_uid]];
|
||||
};
|
||||
|
||||
private _isAuthorized = (_self call ["isGroupLeader", [_requesterUid, _groupID]]) || { _self call ["canDispatch", [_requesterUid]] };
|
||||
if !_isAuthorized exitWith {
|
||||
_result set ["message", "You are not authorized to update that group."];
|
||||
_result
|
||||
};
|
||||
private _groupID = _groupRepository call ["getPlayerGroupId", [_uid]];
|
||||
|
||||
private _groupRegistry = _self call ["syncGroups", []];
|
||||
private _groupRecord = +(_groupRegistry getOrDefault [_groupID, createHashMap]);
|
||||
if (_groupRecord isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Group could not be resolved."];
|
||||
_result
|
||||
};
|
||||
|
||||
_groupRecord set ["status", _finalStatus];
|
||||
_groupRecord set ["lastUpdate", serverTime];
|
||||
_groupRegistry set [_groupID, _groupRecord];
|
||||
_self set ["groupRegistry", _groupRegistry];
|
||||
|
||||
_self call ["appendActivity", [
|
||||
"group_status",
|
||||
format ["%1 updated %2 to %3.", _requesterUid, _groupRecord getOrDefault ["callsign", _groupID], _finalStatus],
|
||||
"",
|
||||
_groupID,
|
||||
_requesterUid
|
||||
]];
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", "Group status updated."];
|
||||
_result set ["group", _groupRecord];
|
||||
_result
|
||||
createHashMapFromArray [
|
||||
["groups", _groupRepository call ["buildGroups", []]],
|
||||
["contracts", _assignmentRepository call ["buildContracts", [_uid]]],
|
||||
["assignments", _assignmentRepository call ["getAssignments", []]],
|
||||
["activity", _activityRepository call ["getActivity", []]],
|
||||
["session", createHashMapFromArray [
|
||||
["uid", _uid],
|
||||
["orgId", _actor getOrDefault ["organization", "default"]],
|
||||
["isDispatcher", _permissionService call ["canDispatch", [_uid]]],
|
||||
["groupId", _groupID],
|
||||
["isLeader", _groupRepository call ["isGroupLeader", [_uid, _groupID]]]
|
||||
]]
|
||||
]
|
||||
}]
|
||||
];
|
||||
|
||||
|
||||
304
arma/server/addons/cad/functions/fnc_initGroupRepository.sqf
Normal file
304
arma/server/addons/cad/functions/fnc_initGroupRepository.sqf
Normal file
@ -0,0 +1,304 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initGroupRepository.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-30
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the CAD group repository for live group state, roles,
|
||||
* and dispatcher/leader-managed group profiles.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* CAD group repository object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_server_cad_fnc_initGroupRepository
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "CadGroupRepositoryBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["groupRegistry", createHashMap];
|
||||
_self set ["groupProfileRegistry", createHashMap];
|
||||
_self set ["validStatuses", [
|
||||
"available",
|
||||
"en_route",
|
||||
"on_task",
|
||||
"holding",
|
||||
"danger",
|
||||
"refit",
|
||||
"offline"
|
||||
]];
|
||||
_self set ["validRoles", [
|
||||
"infantry",
|
||||
"recon",
|
||||
"armor",
|
||||
"air",
|
||||
"logistics",
|
||||
"support"
|
||||
]];
|
||||
}],
|
||||
["resolveGroupId", compileFinal {
|
||||
params [["_group", grpNull, [grpNull]]];
|
||||
|
||||
if (isNull _group) exitWith { "" };
|
||||
|
||||
private _leader = leader _group;
|
||||
private _leaderUid = if (isNull _leader) then { "" } else { getPlayerUID _leader };
|
||||
if (_leaderUid isNotEqualTo "") exitWith { format ["group:%1", _leaderUid] };
|
||||
|
||||
private _groupLabel = groupId _group;
|
||||
if (_groupLabel isNotEqualTo "") exitWith { format ["group:%1", _groupLabel] };
|
||||
|
||||
str _group
|
||||
}],
|
||||
["getCurrentTaskIdForGroup", compileFinal {
|
||||
params [["_groupID", "", [""]]];
|
||||
|
||||
if (_groupID isEqualTo "") exitWith { "" };
|
||||
|
||||
private _assignmentRepository = _self getOrDefault ["assignmentRepository", createHashMap];
|
||||
private _assignmentRegistry = _assignmentRepository getOrDefault ["assignmentRegistry", createHashMap];
|
||||
private _taskID = "";
|
||||
|
||||
{
|
||||
if ((_y getOrDefault ["groupId", ""]) isNotEqualTo _groupID) then { continue; };
|
||||
if !((_y getOrDefault ["state", ""]) in ["assigned", "acknowledged"]) then { continue; };
|
||||
if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; };
|
||||
|
||||
_taskID = _x;
|
||||
} forEach _assignmentRegistry;
|
||||
|
||||
_taskID
|
||||
}],
|
||||
["syncGroups", compileFinal {
|
||||
private _previousRegistry = _self getOrDefault ["groupRegistry", createHashMap];
|
||||
private _profileRegistry = _self getOrDefault ["groupProfileRegistry", createHashMap];
|
||||
private _nextRegistry = createHashMap;
|
||||
|
||||
{
|
||||
private _group = _x;
|
||||
if (side _group isNotEqualTo west) then { continue; };
|
||||
|
||||
private _members = allPlayers select { group _x isEqualTo _group };
|
||||
if (_members isEqualTo []) then { continue; };
|
||||
|
||||
private _leader = leader _group;
|
||||
if (isNull _leader || { !isPlayer _leader }) then {
|
||||
_leader = _members # 0;
|
||||
};
|
||||
|
||||
private _groupID = _self call ["resolveGroupId", [_group]];
|
||||
if (_groupID isEqualTo "") then { continue; };
|
||||
|
||||
private _leaderUid = getPlayerUID _leader;
|
||||
private _actor = EGVAR(actor,Registry) getOrDefault [_leaderUid, createHashMap];
|
||||
if (_actor isEqualTo createHashMap && { _leaderUid isNotEqualTo "" }) then {
|
||||
_actor = EGVAR(actor,ActorStore) call ["init", [_leaderUid]];
|
||||
};
|
||||
|
||||
private _orgID = _actor getOrDefault ["organization", "default"];
|
||||
if (_orgID isEqualTo "") then { _orgID = "default"; };
|
||||
|
||||
private _existingRecord = +(_previousRegistry getOrDefault [_groupID, createHashMap]);
|
||||
private _profile = +(_profileRegistry getOrDefault [_groupID, createHashMap]);
|
||||
private _memberUids = [];
|
||||
private _memberRoster = [];
|
||||
|
||||
{
|
||||
private _memberUid = getPlayerUID _x;
|
||||
private _memberState = toLowerANSI (lifeState _x);
|
||||
|
||||
if (_memberUid isNotEqualTo "") then {
|
||||
_memberUids pushBack _memberUid;
|
||||
};
|
||||
|
||||
_memberRoster pushBack (createHashMapFromArray [
|
||||
["uid", _memberUid],
|
||||
["name", name _x],
|
||||
["lifeState", _memberState],
|
||||
["isLeader", _x isEqualTo _leader]
|
||||
]);
|
||||
} forEach _members;
|
||||
|
||||
private _record = createHashMapFromArray [
|
||||
["groupId", _groupID],
|
||||
["callsign", [groupId _group, _groupID] select ((groupId _group) isEqualTo "")],
|
||||
["leaderUid", _leaderUid],
|
||||
["leaderName", name _leader],
|
||||
["memberUids", _memberUids],
|
||||
["members", _memberRoster],
|
||||
["orgId", _orgID],
|
||||
["role", [_existingRecord getOrDefault ["role", "infantry"], _profile getOrDefault ["role", _existingRecord getOrDefault ["role", "infantry"]]] select (_profile isNotEqualTo createHashMap)],
|
||||
["status", [_existingRecord getOrDefault ["status", "available"], _profile getOrDefault ["status", _existingRecord getOrDefault ["status", "available"]]] select (_profile isNotEqualTo createHashMap)],
|
||||
["position", getPosATL _leader],
|
||||
["currentTaskId", _self call ["getCurrentTaskIdForGroup", [_groupID]]],
|
||||
["lastUpdate", serverTime]
|
||||
];
|
||||
|
||||
_nextRegistry set [_groupID, _record];
|
||||
_profileRegistry set [_groupID, createHashMapFromArray [
|
||||
["role", _record getOrDefault ["role", "infantry"]],
|
||||
["status", _record getOrDefault ["status", "available"]]
|
||||
]];
|
||||
} forEach allGroups;
|
||||
|
||||
_self set ["groupProfileRegistry", _profileRegistry];
|
||||
_self set ["groupRegistry", _nextRegistry];
|
||||
_nextRegistry
|
||||
}],
|
||||
["getGroupRecord", compileFinal {
|
||||
params [["_groupID", "", [""]]];
|
||||
|
||||
if (_groupID isEqualTo "") exitWith { createHashMap };
|
||||
|
||||
private _groupRegistry = _self call ["syncGroups", []];
|
||||
+(_groupRegistry getOrDefault [_groupID, createHashMap])
|
||||
}],
|
||||
["getPlayerGroupId", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
if (_uid isEqualTo "") exitWith { "" };
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
if (_player isEqualTo objNull) exitWith { "" };
|
||||
|
||||
_self call ["resolveGroupId", [group _player]]
|
||||
}],
|
||||
["isGroupLeader", compileFinal {
|
||||
params [["_uid", "", [""]], ["_groupID", "", [""]]];
|
||||
|
||||
if (_uid isEqualTo "" || { _groupID isEqualTo "" }) exitWith { false };
|
||||
|
||||
private _groupRecord = _self call ["getGroupRecord", [_groupID]];
|
||||
(_groupRecord getOrDefault ["leaderUid", ""]) isEqualTo _uid
|
||||
}],
|
||||
["buildGroups", compileFinal {
|
||||
private _groupRegistry = _self call ["syncGroups", []];
|
||||
private _groups = [];
|
||||
|
||||
{
|
||||
_groups pushBack +_y;
|
||||
} forEach _groupRegistry;
|
||||
|
||||
_groups
|
||||
}],
|
||||
["updateGroupStatus", compileFinal {
|
||||
params [["_requesterUid", "", [""]], ["_groupID", "", [""]], ["_status", "", [""]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Unable to update group status."],
|
||||
["group", createHashMap]
|
||||
];
|
||||
|
||||
private _finalStatus = toLowerANSI _status;
|
||||
if !(_finalStatus in (_self getOrDefault ["validStatuses", []])) exitWith {
|
||||
_result set ["message", "Invalid group status."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _permissionService = _self getOrDefault ["permissionService", createHashMap];
|
||||
private _isAuthorized = (_self call ["isGroupLeader", [_requesterUid, _groupID]]) || { _permissionService call ["canDispatch", [_requesterUid]] };
|
||||
if !_isAuthorized exitWith {
|
||||
_result set ["message", "You are not authorized to update that group."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _groupRegistry = _self call ["syncGroups", []];
|
||||
private _groupRecord = +(_groupRegistry getOrDefault [_groupID, createHashMap]);
|
||||
if (_groupRecord isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Group could not be resolved."];
|
||||
_result
|
||||
};
|
||||
|
||||
_groupRecord set ["status", _finalStatus];
|
||||
_groupRecord set ["lastUpdate", serverTime];
|
||||
_groupRegistry set [_groupID, _groupRecord];
|
||||
_self set ["groupRegistry", _groupRegistry];
|
||||
|
||||
private _profileRegistry = _self getOrDefault ["groupProfileRegistry", createHashMap];
|
||||
private _profile = +(_profileRegistry getOrDefault [_groupID, createHashMap]);
|
||||
_profile set ["role", _groupRecord getOrDefault ["role", "infantry"]];
|
||||
_profile set ["status", _finalStatus];
|
||||
_profileRegistry set [_groupID, _profile];
|
||||
_self set ["groupProfileRegistry", _profileRegistry];
|
||||
|
||||
private _activityRepository = _self getOrDefault ["activityRepository", createHashMap];
|
||||
_activityRepository call ["appendActivity", [
|
||||
"group_status",
|
||||
format ["%1 updated %2 to %3.", _requesterUid, _groupRecord getOrDefault ["callsign", _groupID], _finalStatus],
|
||||
"",
|
||||
_groupID,
|
||||
_requesterUid
|
||||
]];
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", "Group status updated."];
|
||||
_result set ["group", _groupRecord];
|
||||
_result
|
||||
}],
|
||||
["updateGroupRole", compileFinal {
|
||||
params [["_requesterUid", "", [""]], ["_groupID", "", [""]], ["_role", "", [""]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Unable to update group role."],
|
||||
["group", createHashMap]
|
||||
];
|
||||
|
||||
private _finalRole = toLowerANSI _role;
|
||||
if !(_finalRole in (_self getOrDefault ["validRoles", []])) exitWith {
|
||||
_result set ["message", "Invalid group role."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _permissionService = _self getOrDefault ["permissionService", createHashMap];
|
||||
private _isAuthorized = (_self call ["isGroupLeader", [_requesterUid, _groupID]]) || { _permissionService call ["canDispatch", [_requesterUid]] };
|
||||
if !_isAuthorized exitWith {
|
||||
_result set ["message", "You are not authorized to update that group role."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _groupRegistry = _self call ["syncGroups", []];
|
||||
private _groupRecord = +(_groupRegistry getOrDefault [_groupID, createHashMap]);
|
||||
if (_groupRecord isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Group could not be resolved."];
|
||||
_result
|
||||
};
|
||||
|
||||
_groupRecord set ["role", _finalRole];
|
||||
_groupRecord set ["lastUpdate", serverTime];
|
||||
_groupRegistry set [_groupID, _groupRecord];
|
||||
_self set ["groupRegistry", _groupRegistry];
|
||||
|
||||
private _profileRegistry = _self getOrDefault ["groupProfileRegistry", createHashMap];
|
||||
private _profile = +(_profileRegistry getOrDefault [_groupID, createHashMap]);
|
||||
_profile set ["role", _finalRole];
|
||||
_profile set ["status", _groupRecord getOrDefault ["status", "available"]];
|
||||
_profileRegistry set [_groupID, _profile];
|
||||
_self set ["groupProfileRegistry", _profileRegistry];
|
||||
|
||||
private _activityRepository = _self getOrDefault ["activityRepository", createHashMap];
|
||||
_activityRepository call ["appendActivity", [
|
||||
"group_role",
|
||||
format ["%1 updated %2 role to %3.", _requesterUid, _groupRecord getOrDefault ["callsign", _groupID], _finalRole],
|
||||
"",
|
||||
_groupID,
|
||||
_requesterUid
|
||||
]];
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", "Group role updated."];
|
||||
_result set ["group", _groupRecord];
|
||||
_result
|
||||
}]
|
||||
];
|
||||
|
||||
createHashMapObject [GVAR(GroupRepositoryBaseClass)]
|
||||
@ -0,0 +1,48 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initPermissionService.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-30
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the CAD permission service for dispatcher authorization checks.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* CAD permission service object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_server_cad_fnc_initPermissionService
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(PermissionServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "CadPermissionServiceBaseClass"],
|
||||
["canDispatch", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
if (_uid isEqualTo "") exitWith { false };
|
||||
|
||||
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
|
||||
if (_actor isEqualTo createHashMap) exitWith { false };
|
||||
|
||||
private _orgID = _actor getOrDefault ["organization", "default"];
|
||||
private _org = EGVAR(org,Registry) getOrDefault [_orgID, createHashMap];
|
||||
if (_org isEqualTo createHashMap) exitWith { false };
|
||||
|
||||
private _owner = _org getOrDefault ["owner", ""];
|
||||
if (_owner isEqualTo _uid) exitWith { true };
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
if (_player isEqualTo objNull) exitWith { false };
|
||||
|
||||
private _playerVar = toLowerANSI (vehicleVarName _player);
|
||||
(_orgID isEqualTo "default") && { _playerVar in ["ceo", "dispatch"] }
|
||||
}]
|
||||
];
|
||||
|
||||
createHashMapObject [GVAR(PermissionServiceBaseClass)]
|
||||
Loading…
x
Reference in New Issue
Block a user