Support dispatch board and map view switching

- Add dispatchView state to the CAD repository and hydrate payloads
- Toggle the dispatcher between board and map layouts in the UI bridge
- Update side panel logic to hide contract tabs in dispatch map mode
This commit is contained in:
Jacob Schmidt 2026-03-30 21:39:39 -05:00
parent 1ca2499af7
commit 56b560edf2
21 changed files with 301 additions and 58 deletions

View File

@ -49,6 +49,14 @@ switch (_event) do {
GVAR(CADUIBridge) call ["setMode", [_mode]];
};
case "cad::dispatchView::set": {
private _dispatchView = "";
if (_data isEqualType createHashMap) then {
_dispatchView = _data getOrDefault ["dispatchView", ""];
};
GVAR(CADUIBridge) call ["setDispatchView", [_dispatchView]];
};
case "cad::refresh": {
GVAR(CADUIBridge) call ["requestHydrate", []];
};

View File

@ -31,6 +31,7 @@ GVAR(CADRepository) = createHashMapObject [[
_self set ["activity", []];
_self set ["session", createHashMap];
_self set ["mode", "operations"];
_self set ["dispatchView", "board"];
}],
["getHydratePayload", compileFinal {
createHashMapFromArray [
@ -39,7 +40,8 @@ GVAR(CADRepository) = createHashMapObject [[
["assignments", +(_self getOrDefault ["assignments", []])],
["activity", +(_self getOrDefault ["activity", []])],
["session", +(_self getOrDefault ["session", createHashMap])],
["mode", _self getOrDefault ["mode", "operations"]]
["mode", _self getOrDefault ["mode", "operations"]],
["dispatchView", _self getOrDefault ["dispatchView", "board"]]
]
}],
["getCurrentGroup", compileFinal {
@ -80,6 +82,16 @@ GVAR(CADRepository) = createHashMapObject [[
_self set ["mode", _mode];
_mode
}],
["setDispatchView", compileFinal {
params [["_dispatchView", "board", [""]]];
if !(_dispatchView in ["board", "map"]) then {
_dispatchView = "board";
};
_self set ["dispatchView", _dispatchView];
_dispatchView
}],
["setOpen", compileFinal {
params [["_isOpen", false, [false]]];
_self set ["isOpen", _isOpen];

View File

@ -82,17 +82,25 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [
} else {
GVAR(CADRepository) getOrDefault ["mode", "operations"]
};
private _dispatchView = if (isNil QGVAR(CADRepository)) then {
"board"
} else {
GVAR(CADRepository) getOrDefault ["dispatchView", "board"]
};
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"); };
private _showMapLayout = (_mode isEqualTo "operations") || { _mode isEqualTo "dispatch" && { _dispatchView isEqualTo "map" } };
if !(isNull _mapCtrl) then { _mapCtrl ctrlShow _showMapLayout; };
if !(isNull _bottomBarCtrl) then { _bottomBarCtrl ctrlShow true; };
if !(isNull _sidePanelCtrl) then { _sidePanelCtrl ctrlShow _showMapLayout; };
if !(isNull _dispatcherCtrl) then { _dispatcherCtrl ctrlShow (_mode isEqualTo "dispatch" && { _dispatchView isEqualTo "board" }); };
_self call ["refreshHydrate", []];
_self call ["refreshTopBarState", []];
_self call ["refreshDispatcher", []];
true
@ -112,6 +120,19 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [
};
GVAR(CADRepository) call ["setMode", [_targetMode]];
if (_targetMode isEqualTo "dispatch") then {
GVAR(CADRepository) call ["setDispatchView", ["board"]];
};
_self call ["applyLayout", []]
}],
["setDispatchView", compileFinal {
params [["_dispatchView", "board", [""]]];
if (isNil QGVAR(CADRepository)) exitWith { false };
if ((GVAR(CADRepository) getOrDefault ["mode", "operations"]) isNotEqualTo "dispatch") exitWith { false };
if !(_self call ["isDispatcher", []]) exitWith { false };
GVAR(CADRepository) call ["setDispatchView", [_dispatchView]];
_self call ["applyLayout", []]
}],
["refreshTopBarState", compileFinal {
@ -126,6 +147,7 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [
private _currentGroup = GVAR(CADRepository) call ["getCurrentGroup", []];
private _payload = createHashMapFromArray [
["mode", GVAR(CADRepository) getOrDefault ["mode", "operations"]],
["dispatchView", GVAR(CADRepository) getOrDefault ["dispatchView", "board"]],
["session", _session],
["currentGroup", _currentGroup]
];

File diff suppressed because one or more lines are too long

View File

@ -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}.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}
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}.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-accept-btn,.task-secondary-btn,.cad-select{color:#f3f6f9;background:#1e252be6;border:1px solid #fff3;width:100%;padding:8px 10px}.task-accept-btn,.task-secondary-btn{cursor:pointer}.task-accept-btn:hover,.task-secondary-btn:hover{background:#2e3942f2}.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

View File

@ -1 +1 @@
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}
body{background:0 0;grid-template-columns:auto minmax(0,1fr) auto 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[data-mode=operations]{grid-template-columns:auto minmax(0,1fr) auto auto}body[data-mode=dispatch]{grid-template-columns:auto minmax(0,1fr) auto auto auto}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}.dispatch-view-controls{justify-self:end;align-items:center;gap:6px;display:flex}.dispatch-view-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}.btn-dispatch-view{text-transform:uppercase;letter-spacing:.08em;min-width:66px;padding:6px 10px;font-size:10px}.btn-icon{justify-content:center;align-items:center;width:34px;min-width:34px;height:30px;padding:0;font-size:16px;line-height:1;display:inline-flex}.btn-refresh{width:40px;min-width:40px;font-size:17px;font-weight:600}.btn-dispatch-view.is-active{color:var(--accent);background:#0f283af5;border-color:#5bbbff6b}.btn-close{font-size:14px}body{pointer-events:none}body .logo,body .title-block,body .operator-strip,body .operator-controls,body .mode-controls,body .dispatch-view-controls,body .controls,body .mode-switch,body .mode-switch *,body button,body select,body label{pointer-events:auto}

View File

@ -1 +1 @@
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();
window.cadTopbar={mode:"operations",dispatchView:"board",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("dispatchRefreshBtn").addEventListener("click",()=>{window.mapUI.sendEvent("cad::refresh",{})}),document.getElementById("dispatchBoardBtn").addEventListener("click",()=>{window.mapUI.sendEvent("cad::dispatchView::set",{dispatchView:"board"})}),document.getElementById("dispatchMapBtn").addEventListener("click",()=>{window.mapUI.sendEvent("cad::dispatchView::set",{dispatchView:"map"})}),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.dispatchView=e&&"string"==typeof e.dispatchView?e.dispatchView:"board",this.currentGroup=e&&e.currentGroup&&"object"==typeof e.currentGroup?e.currentGroup:null;const t=document.getElementById("modeControls"),o=!!this.session.isDispatcher,s=!(!this.currentGroup||!this.session.isLeader&&!this.session.isDispatcher),n=document.getElementById("operatorStrip"),d=document.getElementById("operatorControls"),r=document.getElementById("dispatchViewControls"),i=document.getElementById("dispatchRefreshBtn"),a=document.getElementById("dispatchBoardBtn"),c=document.getElementById("dispatchMapBtn");t.classList.toggle("is-hidden",!o),r.classList.toggle("is-hidden",!o||"dispatch"!==this.mode),n.classList.toggle("is-hidden","operations"!==this.mode||!this.currentGroup),d.classList.toggle("is-hidden",!s),document.body.dataset.mode=this.mode,document.body.dataset.dispatcher=o?"true":"false",document.getElementById("modeToggle").checked="dispatch"===this.mode,a.classList.toggle("is-active","board"===this.dispatchView),c.classList.toggle("is-active","map"===this.dispatchView),i.title="dispatch"===this.mode?"Refresh dispatch board":"Refresh CAD",i.setAttribute("aria-label","dispatch"===this.mode?"Refresh dispatch board":"Refresh CAD"),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();

View File

@ -1 +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>
<!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></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>

View File

@ -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="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>
<!doctype html><html><head><meta charset="UTF-8"></head><body><div class="panel-header"><h3>CAD System</h3></div><div class="panel-content"><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>

View File

@ -1 +1 @@
<!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>
<!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 id="dispatchViewControls" class="dispatch-view-controls is-hidden"><button id="dispatchBoardBtn" class="btn btn-dispatch-view is-active" type="button">Board</button> <button id="dispatchMapBtn" class="btn btn-dispatch-view" type="button">Map</button></div><div class="controls"><button id="dispatchRefreshBtn" class="btn btn-icon btn-refresh" type="button" aria-label="Refresh board" title="Refresh board"></button> <button id="btnClose" class="btn btn-icon 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>

View File

@ -10,9 +10,6 @@
<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>

View File

@ -15,13 +15,6 @@ window.cadDispatcher = {
],
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", () => {

View File

@ -8,9 +8,6 @@
<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

View File

@ -4,6 +4,7 @@ window.cadTasks = {
activity: [],
session: {},
mode: "operations",
dispatchView: "board",
activeTab: "contracts",
statuses: [
"available",
@ -16,11 +17,6 @@ window.cadTasks = {
],
roles: ["infantry", "recon", "armor", "air", "logistics", "support"],
init() {
const refreshBtn = document.getElementById("refreshCadBtn");
if (refreshBtn) {
refreshBtn.addEventListener("click", () => this.refresh());
}
document.querySelectorAll(".cad-tab").forEach((tab) => {
tab.addEventListener("click", () => {
this.setActiveTab(tab.dataset.tab || "contracts");
@ -58,6 +54,52 @@ window.cadTasks = {
);
});
},
syncLayoutState() {
const tabsEl = document.querySelector(".cad-tabs");
const contractsPanel = document.getElementById("contractsPanel");
const rosterPanel = document.getElementById("rosterPanel");
const activityPanel = document.getElementById("activityPanel");
const rosterHeader = rosterPanel?.querySelector(".cad-section-header");
if (this.isDispatchMapMode()) {
if (tabsEl) {
tabsEl.style.display = "none";
}
if (contractsPanel) {
contractsPanel.classList.remove("is-active");
contractsPanel.style.display = "none";
}
if (activityPanel) {
activityPanel.classList.remove("is-active");
activityPanel.style.display = "none";
}
if (rosterPanel) {
rosterPanel.classList.add("is-active");
rosterPanel.style.display = "block";
}
if (rosterHeader) {
rosterHeader.textContent = "Active Groups";
}
this.activeTab = "roster";
return;
}
if (tabsEl) {
tabsEl.style.display = "";
}
if (contractsPanel) {
contractsPanel.style.display = "";
}
if (activityPanel) {
activityPanel.style.display = "";
}
if (rosterPanel) {
rosterPanel.style.display = "";
}
if (rosterHeader) {
rosterHeader.textContent = "Roster";
}
},
setHydratePayload(payload) {
this.contracts = Array.isArray(payload.contracts)
? payload.contracts
@ -72,6 +114,10 @@ window.cadTasks = {
payload && typeof payload.mode === "string"
? payload.mode
: "operations";
this.dispatchView =
payload && typeof payload.dispatchView === "string"
? payload.dispatchView
: "board";
const statusEl = document.getElementById("cadStatusMessage");
if (
@ -81,6 +127,10 @@ window.cadTasks = {
this.setStatus("", "");
}
if (this.mode === "dispatch" && this.dispatchView === "map") {
this.activeTab = "roster";
}
this.render();
},
setStatus(message, type) {
@ -99,10 +149,6 @@ window.cadTasks = {
success ? "success" : "error",
);
},
refresh() {
this.setStatus("Refreshing board...", "info");
window.mapUI.sendEvent("cad::refresh", {});
},
acknowledgeTask(taskID) {
this.setStatus("Acknowledging contract...", "info");
window.mapUI.sendEvent("cad::tasks::acknowledge", { taskID: taskID });
@ -152,6 +198,9 @@ window.cadTasks = {
isDispatchMode() {
return this.mode === "dispatch";
},
isDispatchMapMode() {
return this.mode === "dispatch" && this.dispatchView === "map";
},
isLeader() {
return !!this.session.isLeader;
},
@ -161,6 +210,12 @@ window.cadTasks = {
return;
}
if (this.isDispatchMapMode()) {
listEl.innerHTML =
'<div class="placeholder-message"><p>Use the dispatch board view to assign and review contracts.</p></div>';
return;
}
const currentGroupId = this.getPlayerGroupId();
const visibleContracts = this.contracts.filter(
(task) => (task.assignedGroupId || "") === currentGroupId,
@ -216,6 +271,36 @@ window.cadTasks = {
return;
}
if (this.isDispatchMapMode()) {
if (!this.groups.length) {
listEl.innerHTML =
'<div class="placeholder-message"><p>No active groups are currently available.</p></div>';
return;
}
listEl.innerHTML = this.groups
.map((group) => {
return `
<div class="task-card roster-member-card" data-group-id="${group.groupId || ""}">
<div class="task-card-header">
<strong>${group.callsign || group.groupId || "Unknown Group"}</strong>
<span class="task-type">${group.role || "group"}</span>
</div>
<div class="task-meta">
<span>Leader: ${group.leaderName || "Unknown"}</span>
<span>Status: ${group.status || "unknown"}</span>
</div>
<div class="task-meta">
<span>Members: ${this.normalizeCollection(group.members).length}</span>
<span>Task: ${group.currentTaskId || "None"}</span>
</div>
</div>
`;
})
.join("");
return;
}
const currentGroup = this.getCurrentGroup();
if (!currentGroup) {
listEl.innerHTML =
@ -301,10 +386,13 @@ window.cadTasks = {
.join("");
},
render() {
this.syncLayoutState();
this.renderContracts();
this.renderRoster();
this.renderActivity();
this.setActiveTab(this.activeTab);
if (!this.isDispatchMapMode()) {
this.setActiveTab(this.activeTab);
}
},
};

View File

@ -55,10 +55,6 @@ body {
font-style: italic;
}
.task-toolbar {
margin-bottom: 10px;
}
.cad-tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
@ -109,7 +105,6 @@ body {
letter-spacing: 0.08em;
}
.task-toolbar button,
.task-accept-btn,
.task-secondary-btn,
.cad-select {
@ -120,19 +115,16 @@ body {
color: #f3f6f9;
}
.task-toolbar button,
.task-accept-btn,
.task-secondary-btn {
cursor: pointer;
}
.task-toolbar button:hover,
.task-accept-btn:hover,
.task-secondary-btn:hover {
background: rgba(46, 57, 66, 0.95);
}
.task-toolbar button:disabled,
.task-accept-btn:disabled,
.task-secondary-btn:disabled {
opacity: 0.55;

View File

@ -5,7 +5,7 @@ body {
right: 0;
height: 60px;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto auto;
grid-template-columns: auto minmax(0, 1fr) auto auto auto;
align-items: center;
column-gap: 16px;
padding: 0 16px;
@ -13,6 +13,14 @@ body {
overflow: visible;
}
body[data-mode="operations"] {
grid-template-columns: auto minmax(0, 1fr) auto auto;
}
body[data-mode="dispatch"] {
grid-template-columns: auto minmax(0, 1fr) auto auto auto;
}
body::before {
content: "";
position: absolute;
@ -142,6 +150,17 @@ body > * {
display: none;
}
.dispatch-view-controls {
display: flex;
align-items: center;
gap: 6px;
justify-self: end;
}
.dispatch-view-controls.is-hidden {
display: none;
}
.controls {
display: flex;
gap: 8px;
@ -220,20 +239,58 @@ body > * {
min-width: 42px;
}
body[data-mode="operations"] {
.btn-dispatch-view {
min-width: 66px;
padding: 6px 10px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.btn-icon {
min-width: 34px;
width: 34px;
height: 30px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
font-size: 16px;
line-height: 1;
}
.btn-refresh {
min-width: 40px;
width: 40px;
font-size: 17px;
font-weight: 600;
}
.btn-dispatch-view.is-active {
border-color: rgba(91, 187, 255, 0.42);
background: rgba(15, 40, 58, 0.96);
color: var(--accent);
}
.btn-close {
font-size: 14px;
}
body {
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 {
body .logo,
body .title-block,
body .operator-strip,
body .operator-controls,
body .mode-controls,
body .dispatch-view-controls,
body .controls,
body .mode-switch,
body .mode-switch *,
body button,
body select,
body label {
pointer-events: auto;
}

View File

@ -62,8 +62,33 @@
</label>
<span class="mode-text">Dispatch</span>
</div>
<div id="dispatchViewControls" class="dispatch-view-controls is-hidden">
<button
id="dispatchBoardBtn"
class="btn btn-dispatch-view is-active"
type="button"
>
Board
</button>
<button
id="dispatchMapBtn"
class="btn btn-dispatch-view"
type="button"
>
Map
</button>
</div>
<div class="controls">
<button id="btnClose" class="btn btn-close">X</button>
<button
id="dispatchRefreshBtn"
class="btn btn-icon btn-refresh"
type="button"
aria-label="Refresh board"
title="Refresh board"
>
</button>
<button id="btnClose" class="btn btn-icon btn-close">X</button>
</div>
<script>

View File

@ -1,5 +1,6 @@
window.cadTopbar = {
mode: "operations",
dispatchView: "board",
currentGroup: null,
session: {},
init() {
@ -15,6 +16,28 @@ window.cadTopbar = {
});
});
document
.getElementById("dispatchRefreshBtn")
.addEventListener("click", () => {
window.mapUI.sendEvent("cad::refresh", {});
});
document
.getElementById("dispatchBoardBtn")
.addEventListener("click", () => {
window.mapUI.sendEvent("cad::dispatchView::set", {
dispatchView: "board",
});
});
document
.getElementById("dispatchMapBtn")
.addEventListener("click", () => {
window.mapUI.sendEvent("cad::dispatchView::set", {
dispatchView: "map",
});
});
document
.getElementById("operatorRoleBtn")
.addEventListener("click", () => {
@ -63,6 +86,10 @@ window.cadTopbar = {
payload && typeof payload.mode === "string"
? payload.mode
: "operations";
this.dispatchView =
payload && typeof payload.dispatchView === "string"
? payload.dispatchView
: "board";
this.currentGroup =
payload &&
payload.currentGroup &&
@ -77,8 +104,19 @@ window.cadTopbar = {
(!!this.session.isLeader || !!this.session.isDispatcher);
const operatorStrip = document.getElementById("operatorStrip");
const operatorControls = document.getElementById("operatorControls");
const dispatchViewControls = document.getElementById(
"dispatchViewControls",
);
const dispatchRefreshBtn =
document.getElementById("dispatchRefreshBtn");
const dispatchBoardBtn = document.getElementById("dispatchBoardBtn");
const dispatchMapBtn = document.getElementById("dispatchMapBtn");
modeControls.classList.toggle("is-hidden", !canDispatch);
dispatchViewControls.classList.toggle(
"is-hidden",
!canDispatch || this.mode !== "dispatch",
);
operatorStrip.classList.toggle(
"is-hidden",
this.mode !== "operations" || !this.currentGroup,
@ -90,6 +128,20 @@ window.cadTopbar = {
document.getElementById("modeToggle").checked =
this.mode === "dispatch";
dispatchBoardBtn.classList.toggle(
"is-active",
this.dispatchView === "board",
);
dispatchMapBtn.classList.toggle(
"is-active",
this.dispatchView === "map",
);
dispatchRefreshBtn.title =
this.mode === "dispatch" ? "Refresh dispatch board" : "Refresh CAD";
dispatchRefreshBtn.setAttribute(
"aria-label",
this.mode === "dispatch" ? "Refresh dispatch board" : "Refresh CAD",
);
document.getElementById("operatorGroupName").textContent = this
.currentGroup

BIN
arma/client/addons/phone.7z Normal file

Binary file not shown.

BIN
arma/server/addons/phone.7z Normal file

Binary file not shown.