Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions src/pages/stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,6 @@ function RunForestRun(mp) {
unit = [];
console.log("passed array of units");
var hqsGiven = params.hq.split(",");
console.log(hqsGiven);
hqsGiven.forEach(function(d) {
BeaconClient.unit.getName(d, apiHost, params.userId, token, function(result, error) {
if (typeof error == 'undefined') {
Expand Down Expand Up @@ -812,9 +811,6 @@ function prepareCharts(jobs, start, end, firstRun) {
let zoneChartFilters = zoneChart.filters();
let sectorChartFilters = sectorChart.filters();

console.log(completionBellChart)


//remove the filters
statusChart.filter(null)
agencyChart.filter(null)
Expand Down
33 changes: 33 additions & 0 deletions src/pages/tasking/bindings/bsDropdownOpen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* bsDropdownOpen – Knockout custom binding
*
* Bridges Bootstrap 5 dropdown show/hide events to a KO observable.
* Apply to the parent element that contains the toggle button and the
* `.dropdown-menu`. The observable is set to `true` on `shown.bs.dropdown`
* and `false` on `hidden.bs.dropdown`.
*
* Usage:
* <div data-bind="bsDropdownOpen: myObservable"> … </div>
*
* @module bindings/bsDropdownOpen
*/

var ko = require('knockout');

ko.bindingHandlers.bsDropdownOpen = {
init(element, valueAccessor) {
const obs = valueAccessor();

const onShown = () => obs(true);
const onHidden = () => obs(false);

element.addEventListener('shown.bs.dropdown', onShown);
element.addEventListener('hidden.bs.dropdown', onHidden);

// Clean up listeners when the element is removed from the DOM
ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
element.removeEventListener('shown.bs.dropdown', onShown);
element.removeEventListener('hidden.bs.dropdown', onHidden);
});
},
};
79 changes: 79 additions & 0 deletions src/pages/tasking/bindings/fastTooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Knockout binding: fastTooltip
*
* Usage: data-bind="fastTooltip: someTextValue"
*
* Shows a fixed-position tooltip instantly on hover (0.1s fade).
* Works inside overflow:hidden / scroll containers.
*/
import ko from 'knockout';


let bubble = null;

function getBubble() {
if (!bubble) {
bubble = document.createElement('div');
bubble.className = 'fast-tooltip-bubble';
document.body.appendChild(bubble);
}
return bubble;
}

function showTip(el, text) {
if (!text) return;
const b = getBubble();
b.textContent = text;
b.classList.remove('show');

const rect = el.getBoundingClientRect();
// Position off-screen to measure without flicker
b.style.left = '-9999px';
b.style.top = '-9999px';
b.style.display = 'block';

// measure
void b.offsetWidth;
const bRect = b.getBoundingClientRect();

let top = rect.top - bRect.height - 6;
let left = rect.right - bRect.width;

if (top < 4) top = rect.bottom + 6;
if (left < 4) left = 4;
if (left + bRect.width > window.innerWidth - 4) {
left = window.innerWidth - bRect.width - 4;
}

b.style.top = top + 'px';
b.style.left = left + 'px';
// Force reflow then fade in via class (no inline opacity override)
void b.offsetWidth;
b.classList.add('show');
}

function hideTip() {
if (bubble) {
bubble.classList.remove('show');
bubble.style.display = 'none';
}
}

ko.bindingHandlers.fastTooltip = {
init: function (element, valueAccessor) {
element.addEventListener('mouseenter', function () {
const text = ko.unwrap(valueAccessor());
showTip(element, text);
});
element.addEventListener('mouseleave', function () {
hideTip();
});

ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
hideTip();
});
},
update: function () {
// text is read live on mouseenter, nothing to do here
}
};
45 changes: 38 additions & 7 deletions src/pages/tasking/components/alerts.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,23 @@ function createLeafletControl(L) {
function renderRules(container, rules, opts = {}) {
const allowCollapse = opts.allowCollapse !== false;
container.style.display = rules.length ? '' : 'none';
container.innerHTML = '';

// Build a set of active rule IDs so we can remove stale DOM elements
const activeIds = new Set(rules.map(r => r.id));

// Remove DOM elements for rules that are no longer active
container.querySelectorAll('[data-rule-id]').forEach(el => {
if (!activeIds.has(el.getAttribute('data-rule-id'))) {
el.remove();
}
});

for (const rule of rules) {
// --- restore / update state for this rule ---
// --- restore / initialise state for this rule ---
let state = ruleState.get(rule.id);
if (!state) {
state = { collapsed: false, open: false, lastCount: rule.count };
} else {
// if the count changed while collapsed, auto-expand
if (state.collapsed && rule.count !== state.lastCount) {
state.collapsed = false;
}
state.lastCount = rule.count;
}
if (!allowCollapse) {
Expand All @@ -60,7 +65,33 @@ function renderRules(container, rules, opts = {}) {
}
ruleState.set(rule.id, state);

const div = document.createElement('div');
// --- check if a DOM element already exists for this rule ---
let div = container.querySelector(`[data-rule-id="${rule.id}"]`);

if (div) {
// --- IN-PLACE UPDATE: only patch count + items, preserve all user state ---
const countEl = div.querySelector('.alerts__count');
if (countEl) countEl.textContent = rule.count;

const ul = div.querySelector('.alerts__list');
if (ul) {
ul.innerHTML = rule.items.map(it => `<li data-id="${it.id}">${it.label}</li>`).join('');
// Re-attach item click handlers
if (typeof rule.onClick === 'function') {
ul.querySelectorAll('li[data-id]').forEach(li => {
li.style.cursor = 'pointer';
li.addEventListener('mouseenter', () => li.style.textDecoration = 'underline');
li.addEventListener('mouseleave', () => li.style.textDecoration = '');
li.addEventListener('click', () => rule.onClick(li.getAttribute('data-id')));
});
}
}
continue; // skip full creation — DOM element (incl classes) stays as-is
}

// --- FIRST-TIME CREATION for this rule ---
div = document.createElement('div');
div.setAttribute('data-rule-id', rule.id);
var width = '280px'
div.className = `leaflet-control alerts alerts--${rule.level}`;
if (state.collapsed) {
Expand Down
18 changes: 15 additions & 3 deletions src/pages/tasking/components/job_popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ export function buildJobPopupKO() {
</div>
<!-- Assign to Team Dropdown -->
<div class="text-center mt-2">
<div class="dropdown d-inline-block">
<div class="dropdown d-inline-block" data-bind="bsDropdownOpen: $root.job.instantTask.dropdownOpen">
<button class="btn btn-small dropdown-toggle btn-outline-primary" type="button" id="assignTeamBtn"
title="Task Team"
data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-bind="disable: !canTaskJob()">
<i class="fa fa-solid fa-user-plus"></i>

Expand Down Expand Up @@ -82,16 +83,27 @@ export function buildJobPopupKO() {
class="dropdown-item text-start py-1"
data-bind="click: taskTeamToJobWithConfirm,
event: { mouseenter: mouseInTeamInInstantTaskPopup, mouseleave: mouseOutTeamInInstantTaskPopup }">
<div>
<strong
<div class="d-flex align-items-center">
<strong class="flex-grow-1"
data-bind="text: team.callsign() || ''"></strong>
<!-- ko if: isSuggested -->
<span class="fast-tooltip ms-2 flex-shrink-0" data-bind="fastTooltip: suggestionReason">
<i class="fa fa-robot text-primary"></i>
</span>
<!-- /ko -->
</div>
<div class="small text-muted"
data-bind="text: currentTaskingSummary()">
</div>
<div class="small text-muted"
data-bind="text: summaryLine">
</div>
<!-- ko if: routeLoading -->
<div class="small text-muted fst-italic">
<span class="spinner-border spinner-border-sm" style="width: 0.7em; height: 0.7em;" role="status"></span>
Calculating travel time…
</div>
<!-- /ko -->
</button>
<!-- /ko -->
<!-- /ko -->
Expand Down
Loading
Loading