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
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/pages/tasking/components/spotlightSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export function SpotlightSearchVM({ rootVm, getTeams, getJobs }) {
const self = this;

self.query = ko.observable("");
self.clearQuery = () => self.query('');
self.activeIndex = ko.observable(0);
self.results = ko.observableArray([]);

Expand Down
222 changes: 167 additions & 55 deletions src/pages/tasking/main.js

Large diffs are not rendered by default.

67 changes: 66 additions & 1 deletion src/pages/tasking/markers/jobMarker.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ export function addOrUpdateJobMarker(ko, map, vm, job) {
);
}

// ensure we have a priority subscription exactly once
if (!m._prioritySub) {
m._prioritySub = job.jobPriorityType.subscribe(() => {
syncMarkerStyle(m, job, vm, pulseLayer);
});
(m._subs ||= []).push(m._prioritySub);
}

job.marker = m;
return m;
}
Expand All @@ -92,7 +100,6 @@ export function addOrUpdateJobMarker(ko, map, vm, job) {
marker._priorityColor = style.fill || '#6b7280';
if (targetLayer === vm.mapVM.jobClusterGroup) {
marker.addTo(targetLayer);
//vm.mapVM.safeAddToClusterGroup?.(marker);
} else {
marker.addTo(targetLayer);
}
Expand All @@ -116,10 +123,16 @@ export function addOrUpdateJobMarker(ko, map, vm, job) {
const popupVM = vm.mapVM.makeJobPopupVM(job);
wireKoForPopup(ko, marker, job, vm, popupVM);

// live priority updates — restyle icon when priority changes
marker._prioritySub = job.jobPriorityType.subscribe(() => {
syncMarkerStyle(marker, job, vm, pulseLayer);
});

// live position updates from KO observables
marker._subs = [
job.address.latitude.subscribe(() => safeMove(marker, job)),
job.address.longitude.subscribe(() => safeMove(marker, job)),
marker._prioritySub,
];

// Sync pulse ring visibility after adding
Expand Down Expand Up @@ -210,6 +223,58 @@ function upsertPulseRing(layerGroup, job, marker) {

// --- internals ---

/**
* Re-evaluate a marker's icon style after a priority (or category) change.
* Updates icon, _priorityColor, _isRescue and handles rescue-layer
* reassignment + cluster refresh as needed.
*/
function syncMarkerStyle(marker, job, vm, pulseLayer) {
const newStyle = styleForJob(job);
const newKey = JSON.stringify(newStyle);

// Update icon if the visual style actually changed
if (marker._styleKey !== newKey) {
marker.setIcon(makeShapeIcon(newStyle));
marker._styleKey = newKey;
}
marker._priorityColor = newStyle.fill || '#6b7280';

// Check whether rescue status flipped
const wasRescue = marker._isRescue;
const isRescue = (job.priorityName?.() || '').toLowerCase() === 'rescue';
marker._isRescue = isRescue;

if (wasRescue !== isRescue) {
const clusterRescue = !!vm.config?.clusterRescueJobs?.();
const clusteringOn = vm.mapVM.clusteringEnabled;

if (clusteringOn) {
if (isRescue && !clusterRescue) {
// Was normal, now rescue and rescues are unclustered
if (vm.mapVM.jobClusterGroup.hasLayer(marker)) {
vm.mapVM.jobClusterGroup.removeLayer(marker);
vm.mapVM.rescueJobLayer.addLayer(marker);
}
} else if (!isRescue || clusterRescue) {
// Was rescue (unclustered), now normal — put back into cluster group
if (vm.mapVM.rescueJobLayer.hasLayer(marker)) {
vm.mapVM.rescueJobLayer.removeLayer(marker);
vm.mapVM.jobClusterGroup.addLayer(marker);
}
}
}
}

// Refresh the pulse ring shape (it depends on style.shape)
upsertPulseRing(pulseLayer, job, marker);
vm.mapVM._syncPulseRings?.();

// Refresh ancestor cluster icons so the hex ring redraws with the new colour
if (vm.mapVM.clusteringEnabled && vm.mapVM.jobClusterGroup.hasLayer(marker)) {
vm.mapVM.jobClusterGroup.refreshClusters(marker);
}
}

function safeMove(marker, job) {
// Skip if the marker is currently spiderfied — moving it would break the
// spider layout. The real position is restored on unspiderfy.
Expand Down
50 changes: 17 additions & 33 deletions src/pages/tasking/models/Job.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,38 +258,16 @@ export function Job(data = {}, deps = {}) {

self.lastTaskingDataUpdate = new Date();


// ---- Tasking REFRESH CHECK ----
// ---- Because the tasking doesnt come down in the job search ----
const dataRefreshInterval = makeFilteredInterval(async () => {
const now = Date.now();
const last = self.lastTaskingDataUpdate?.getTime?.() ?? 0;
// only refresh if we haven't had an update in > 2 minutes
if (now - last > 120000) {
self.fetchTasking();
}
}, 30000, { runImmediately: false });

self.startDataRefreshCheck = function () {
dataRefreshInterval.start();
};

self.stopDataRefreshCheck = function () {
dataRefreshInterval.stop();
};
// Minimum cooldown (ms) between single-job tasking fetches.
// Bulk/batch refreshes update lastTaskingDataUpdate directly,
// so this gate also prevents a single fetch right after a batch.
const SINGLE_FETCH_COOLDOWN_MS = 10_000;

self.drawJobTargetRing = function () {
drawJobTargetRing(self);
};

// Start/stop with filter state
self.isFilteredIn.subscribe((flag) => {
if (flag) {
self.startDataRefreshCheck();
} else {
self.stopDataRefreshCheck();
}
});
// (Periodic tasking refresh is now handled in bulk by main.js)

self.expanded.subscribe((isExpanded) => {
self.instantTask.popupActive(isExpanded || self.popUpIsOpen());
Expand Down Expand Up @@ -409,7 +387,7 @@ export function Job(data = {}, deps = {}) {
self.incompleteTaskingsOnly = ko.pureComputed(() =>
self.taskings().filter(t => {
const status = t.currentStatus();
return status !== "Complete" && status !== "CalledOff";
return status !== "Complete" && status !== "CalledOff" && status !== "Untasked";
})
);

Expand Down Expand Up @@ -526,8 +504,6 @@ export function Job(data = {}, deps = {}) {

Job.prototype.updateFromJson = function (d = {}) {

this.startDataRefreshCheck(); // restart timer

// sector might be undefined or null. they mean different things
if (d.Sector !== undefined) { //sector present in payload
if (d.Sector === null) { //theres no sector assigned
Expand Down Expand Up @@ -655,7 +631,7 @@ export function Job(data = {}, deps = {}) {
};

self.toggleAndExpand = function () {
console.log("Toggling and expanding job", self.id());
console.log("Toggling job", self.id());
self.toggleAndLoad();
scrollToThisInTable();
}
Expand Down Expand Up @@ -737,12 +713,20 @@ export function Job(data = {}, deps = {}) {
self.refreshData();
}

self.fetchTasking = function () {
self.fetchTasking = function (opts = {}) {
const force = opts.force === true;
if (!force) {
const now = Date.now();
const last = self.lastTaskingDataUpdate?.getTime?.() ?? 0;
if (now - last < SINGLE_FETCH_COOLDOWN_MS) {
console.log("Skipping tasking fetch for job", self.id(), "due to cooldown");
return; // recently refreshed (single or batch), skip
}
}
self.taskingLoading(true);
fetchJobTasking(self.id(), () => {
self.taskingLoading(false);
});

};

self.refreshData = async function () {
Expand Down
112 changes: 53 additions & 59 deletions src/pages/tasking/models/Team.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,17 @@ export function Team(data = {}, deps = {}) {

// If we just collapsed, no scroll
if (wasExpanded) return;
self.refreshDataAndTasking();

// The expanded subscriber already calls fetchTasking(), so only
// refresh team data and shared-default mapping here.
self.refreshData();

if (_apiUrl && (self.trackableAssets?.() || []).length > 1) {
fetchSharedDefaults(_apiUrl, [String(self.id())])
.then(() => _defaultAssetTick(_defaultAssetTick() + 1))
.catch(() => {/* im not empty i promise */});
}

scrollToThisInTable();

};
Expand Down Expand Up @@ -346,54 +356,6 @@ export function Team(data = {}, deps = {}) {
self.collapse();
};


// ---- Tasking REFRESH CHECK ----
// ---- Because the tasking doesnt come down in the team search ----
const dataRefreshInterval = makeFilteredInterval(async () => {
const now = Date.now();
const last = self.lastTaskingDataUpdate?.getTime?.() ?? 0;
// only refresh if we haven't had an update in > 1 minute
if (now - last > 60000) {
self.fetchTasking();
}
}, 30000, { runImmediately: false });

self.startDataRefreshCheck = function () {
dataRefreshInterval.start();
};

self.stopDataRefreshCheck = function () {
dataRefreshInterval.stop();
};


// interval that only runs while team is filtered in
function makeFilteredInterval(fn, intervalMs, { runImmediately = false } = {}) {
let handle = null;

const tick = () => {
// global guard: only run if still filtered in
if (!self.isFilteredIn()) return;
fn();
};

const start = () => {
if (!self.isFilteredIn()) return; // don't start if already filtered out
if (handle) clearInterval(handle);
if (runImmediately) tick();
handle = setInterval(tick, intervalMs);
};

const stop = () => {
if (handle) {
clearInterval(handle);
handle = null;
}
};

return { start, stop };
}

self.trackableAndIsFiltered = ko.pureComputed(() => {
return self.isFilteredIn() && self.trackableAssets().length > 0;
});
Expand Down Expand Up @@ -555,7 +517,7 @@ export function Team(data = {}, deps = {}) {
self.expand = () => self.expanded(true);
self.collapse = () => self.expanded(false);
self.expanded.subscribe(function (isExpanded) {
if (isExpanded && !self.taskingLoading()) {
if (isExpanded) {
self.fetchTasking();
}
});
Expand All @@ -567,8 +529,22 @@ export function Team(data = {}, deps = {}) {
}
}

self.fetchTasking = function () {
self._lastFetchTaskingTime = 0;

self.fetchTasking = function (opts = {}) {
if (!getTeamTasking || !upsertTasking) return;
const force = opts.force === true;
const now = Date.now();
// Gate against both direct fetches AND bulk/batch updates
const lastFetch = self._lastFetchTaskingTime || 0;
const lastData = self.lastTaskingDataUpdate?.getTime?.() ?? 0;
const lastRefresh = Math.max(lastFetch, lastData);
if (!force && now - lastRefresh < 10000) {
console.log(`[Team ${self.id.peek()}] fetchTasking throttled — last refreshed ${now - lastRefresh}ms ago`);
self.taskingLoading(false); // clear loading state since data is already fresh
return;
}
self._lastFetchTaskingTime = now;
self.taskingLoading(true);
getTeamTasking(self.id.peek())
.then(tasking => {
Expand Down Expand Up @@ -641,20 +617,38 @@ export function Team(data = {}, deps = {}) {
}

Team.prototype.updateFromJson = function (d = {}) {
if (d.Id !== undefined) this.id(d.Id);
if (d.TaskedJobCount !== undefined) this.taskedJobCount(d.TaskedJobCount);
if (d.Callsign !== undefined) this.callsign(d.Callsign);
if (d.TeamStatusType !== undefined) this.teamStatusType(d.TeamStatusType);
if (d.Members !== undefined) this.members(d.Members);
if (d.AssignedTo !== undefined && d.CreatedAt !== undefined) this.assignedTo(new Entity(d.AssignedTo || d.CreatedAt)); //safety for beacon bug
if (d.Id !== undefined && d.Id !== this.id()) this.id(d.Id);
if (d.TaskedJobCount !== undefined && d.TaskedJobCount !== this.taskedJobCount()) this.taskedJobCount(d.TaskedJobCount);
if (d.Callsign !== undefined && d.Callsign !== this.callsign()) this.callsign(d.Callsign);
if (d.TeamStatusType !== undefined) {
const cur = this.teamStatusType();
if (!cur || cur.Id !== d.TeamStatusType?.Id) this.teamStatusType(d.TeamStatusType);
}
if (d.Members !== undefined) {
// Members is an array of objects — compare by length + leader/person ids
const prev = this.members() || [];
const next = d.Members || [];
const changed = prev.length !== next.length ||
next.some((m, i) => m.Person?.Id !== prev[i]?.Person?.Id || m.TeamLeader !== prev[i]?.TeamLeader);
if (changed) this.members(next);
}
if (d.AssignedTo !== undefined && d.CreatedAt !== undefined) {
const cur = this.assignedTo();
const src = d.AssignedTo || d.CreatedAt;
if (!cur || cur.id?.() !== src?.Id) this.assignedTo(new Entity(src));
}
if (d.Sector !== undefined) {
if (d.Sector === null) {
this.sector(new Sector({}));
if (this.sector().id?.()) this.sector(new Sector({}));
} else {
this.sector().updateFromJson(d.Sector);
}
}
if (d.statusId !== undefined) this.updateStatusById(d.statusId);
if (d.TeamStatusStartDate !== undefined) this.statusDate(new Date(d.TeamStatusStartDate));
if (d.TeamStatusStartDate !== undefined) {
const newDate = new Date(d.TeamStatusStartDate);
const cur = this.statusDate();
if (!cur || cur.getTime() !== newDate.getTime()) this.statusDate(newDate);
}
}
}
Loading
Loading