-
- Filter Teams
+
+ Teams
-
- Filter Incidents
+
+
+ Incidents
+
@@ -1305,7 +1314,7 @@
Page Configuration
+ data-bind="click: config.saveAndCloseAndLoad, disable: tokenLoading">
Save & Close
@@ -1442,56 +1451,48 @@
Contact Types
-
-
+
Contact Methods
-
-
+
Entry Purpose
-
-
+
Action Items
-
-
+
From 85d65ece6388bcb1d44028ef06675ed7ba2d0303 Mon Sep 17 00:00:00 2001
From: Tim Dykes
Date: Tue, 18 Nov 2025 23:15:02 +1100
Subject: [PATCH 04/61] Tdykes.tasking.wip (#223)
* boostrap upgrade broke team summary.
This restores bootstrap 4 and adds an alias for bootstrap 5
* first push - new tasking dashboard
* New features
* New features
* moved map to vm
made a new vm just for map
* refactored out the mapvm
* map popups have their own view models now
* starting to flesh out the popups UI
new UI for popups. refactor all the models
* Map with a capital M
wtf is mac doing with this
* Add fit bound button to job pop
* removed leaflet-responsive-popup
back to normal popups
* missed a bad import
* ffs and another
* redoing the setup screen. this one isnt horrible
unit search at the top, both results boxes below
* new config page with dropdown for HQ search
* All sorts of new stuff
New pulse marker for unclaimed
New icons
New refacactor of colours
New incident menu
* linter fix
* UX Overlays
Legend updates
Notification overlays
* linter fix
* small refactor
* only untask warn on active
* extension for opsLog for single entry
* ICEMS unack alerts and fixed legend
* layer draw prototype
* started making transport NSW api code
* lots of code cleanup
* team and job list sorting on column header press
* opslog filters. sticky row headers on jobs and teams
* draggable row fixes. focus on job button
* linter fix
* only deploy on dev branch
* correct filtering of job and team status. added image icon
* Core OpsLog Creation Functionality + New Radio Log UI (#219)
* NewOpsLogEntry Core + Instant Radio Team on Job
Created:
- BeaconClient for creating (POST) New OpsLogEntries.
- Foundation for creating OpsLogEntries via Modal.
- Radio Log button for Teams on Incidents (Taskings).
* Added Team Radio Log Button
Added:
- Team Radio Log (Job agnostic) button & functions.
- Added new status update button to Incidents -> Tasking & Teams -> Taskings (Task list icons)
- Updated Radio Log button to Microphone (inline with other Lighthouse Instant Radio Log icons)
- Started work on new OpsLogEntry code.
* Removing incorrectly duplicated data-bind
* unfucked the map for jobs
unfucked the map for jobs
* Draft New Ops Log Modal
* Create enum.js
* map in some enums correctly, refresh job tasking on expand
* clicking a job calls focus and expands in the job table
* new asset popup
has protection for clicking jobs that are not in filter.
new job ifFiltered logic so it wont update if its out of view
* show situation on scene
* bad refresh bug
refresh was stuck in a loop updating every 30 seconds
* wrong icon
* restrict row drag to just the first cell
* fixed the double toggle load
* new jobs list layout
* Bug Fixes
Teams had no filters on their obvervables
jobs and assets had no disposal methods
teams had no JSON update function
jobs popup had no protection for non asset teams or team out of view
* team list has the new cards list. tasking status badges have consistent color
* job status filters logic and new UI in the menu
* safe defaults in panel layout
* New Ops Log Functionality
-Completed New Ops Log Functionality.
-Slight rework of how the NewOpsLogModalVM handles tags.
-Added error catching for missing required fields in New Ops Logs.
* Config share code + fix for fetching jobs without auth
* UX tweaks on share config
* hooked up real url
* slightly better filter handling
* bugfix - filter boxes were not applying
* bug fix: handler for config checkboxes had back click handling
caused the config to save before the values had been updated.
* Separated Create Ops Log & Create Radio Log
Moved RadioLog into it's own VM (CreateRadioLogModalVM) - also renamed NewOpsLogModalVM to CreateOpsLogModalVM.
* job timeline magic
* tidy up after merge
---------
Co-authored-by: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
---
src/pages/tasking/components/job_popup.js | 5 +
src/pages/tasking/main.js | 105 ++++-
src/pages/tasking/models/HistoryEntry.js | 56 +++
src/pages/tasking/models/Job.js | 51 ++-
src/pages/tasking/viewmodels/JobPopUp.js | 4 +
src/pages/tasking/viewmodels/JobTimeline.js | 402 ++++++++++++++++++
src/pages/tasking/viewmodels/OpsLogModalVM.js | 3 +-
.../tasking/viewmodels/RadioLogModalVM.js | 196 +++++++++
src/shared/BeaconClient/job.js | 17 +
static/pages/tasking.html | 328 ++++++++++++--
styles/pages/tasking.css | 143 ++++++-
11 files changed, 1248 insertions(+), 62 deletions(-)
create mode 100644 src/pages/tasking/models/HistoryEntry.js
create mode 100644 src/pages/tasking/viewmodels/JobTimeline.js
create mode 100644 src/pages/tasking/viewmodels/RadioLogModalVM.js
diff --git a/src/pages/tasking/components/job_popup.js b/src/pages/tasking/components/job_popup.js
index bf8ac3fb..48a6685c 100644
--- a/src/pages/tasking/components/job_popup.js
+++ b/src/pages/tasking/components/job_popup.js
@@ -34,6 +34,11 @@ export function buildJobPopupKO() {
data-bind="click: $root.displayOpsLogsForJob, clickBubble: false">
+
+
+
diff --git a/src/pages/tasking/main.js b/src/pages/tasking/main.js
index 4574333e..fba8f5d2 100644
--- a/src/pages/tasking/main.js
+++ b/src/pages/tasking/main.js
@@ -13,7 +13,11 @@ import { addOrUpdateJobMarker, removeJobMarker } from './markers/jobMarker.js';
import { attachAssetMarker, detachAssetMarker } from './markers/assetMarker.js';
import { MapVM } from './viewmodels/Map.js';
import { OpsLogModalVM } from "./viewmodels/OpsLogModalVM.js";
-import { NewOpsLogModalVM } from "./viewmodels/OpsLogModalVM.js";
+
+import { JobTimeline } from "./viewmodels/JobTimeline.js";
+
+import { CreateOpsLogModalVM } from "./viewmodels/OpsLogModalVM.js";
+import { CreateRadioLogModalVM } from "./viewmodels/RadioLogModalVM.js";
import { getAllStaticTags, Tag } from "./models/Tag.js";
import { installAlerts } from './components/alerts.js';
@@ -258,12 +262,15 @@ function VM() {
self.allTags = ko.observableArray(
getAllStaticTags().map(t => new Tag(t))
);
-
+
///opslog short cuts
self.opsLogModalVM = new OpsLogModalVM(self);
- self.NewOpsLogModalVM = new NewOpsLogModalVM(self);
+ self.CreateOpsLogModalVM = new CreateOpsLogModalVM(self);
+ self.CreateRadioLogModalVM = new CreateRadioLogModalVM(self);
self.selectedJob = ko.observable(null);
+ self.jobTimelineVM = new JobTimeline(self);
+
// --- TABLE SORTING MAGIC ---
self.teamSortKey = ko.observable('callsign');
@@ -499,6 +506,10 @@ function VM() {
self.attachNewOpsLogModal(job);
},
+ attachAndFillTimelineModal: (job) => {
+ self.attachJobTimelineModal(job);
+ },
+
fetchUnacknowledgedJobNotifications: (job) => {
self.fetchUnacknowledgedJobNotifications(job);
}
@@ -551,11 +562,18 @@ function VM() {
modal.show();
};
+ self.attachJobTimelineModal = function (job) {
+ const modalEl = document.getElementById('jobTimelineModal');
+ const modal = new bootstrap.Modal(modalEl);
+ self.jobTimelineVM.openForJob(job);
+ modal.show();
+ }
+
self.attachJobRadioLogModal = function (tasking) {
const modalEl = document.getElementById('RadioLogModal');
const modal = new bootstrap.Modal(modalEl);
- const vm = self.NewOpsLogModalVM;
+ const vm = self.CreateRadioLogModalVM;
vm.modalInstance = modal;
@@ -567,7 +585,7 @@ function VM() {
const modalEl = document.getElementById('RadioLogModal');
const modal = new bootstrap.Modal(modalEl);
- const vm = self.NewOpsLogModalVM;
+ const vm = self.CreateRadioLogModalVM;
vm.modalInstance = modal;
@@ -576,10 +594,10 @@ function VM() {
};
self.attachNewOpsLogModal = function (job) {
- const modalEl = document.getElementById('NewOpsLogModal');
+ const modalEl = document.getElementById('CreateOpsLogModal');
const modal = new bootstrap.Modal(modalEl);
- const vm = self.NewOpsLogModalVM;
+ const vm = self.CreateOpsLogModalVM;
vm.modalInstance = modal;
@@ -855,6 +873,15 @@ function VM() {
});
}
+ self.fetchHistoryForJob = function (jobId, cb) {
+ BeaconClient.job.getHistory(jobId, apiHost, params.userId, token, function (data) {
+ cb(data || []);
+ }, function (err) {
+ console.error("Failed to fetch history for job:", err);
+ cb([]);
+ });
+ }
+
self.createOpsLogEntry = function (payload, cb) {
const form = BeaconClient.toFormUrlEncoded(payload);
BeaconClient.operationslog.create(apiHost, form, token, function (data) {
@@ -981,6 +1008,34 @@ function VM() {
startAssetDataRefreshTimer()
+ // ---------------- REFRESH TIMER FOR JOBS + TEAMS -----------------
+
+ let jobsTeamsTimer = null;
+
+ function startJobsTeamsTimer() {
+ // clear old timer
+ if (jobsTeamsTimer) clearInterval(jobsTeamsTimer);
+
+ // interval in seconds → ms
+ const interval = Number(self.config.refreshInterval() || 60) * 1000;
+
+ jobsTeamsTimer = setInterval(() => {
+ self.fetchAllJobsData();
+ self.fetchAllTeamData();
+ }, interval);
+
+ console.log("Jobs/Teams refresh timer started:", interval, "ms");
+ }
+
+ // run once on startup
+ startJobsTeamsTimer();
+
+ // re-arm timer when refreshInterval changes
+ self.config.refreshInterval.subscribe(() => {
+ console.log("refreshInterval changed → restarting timer");
+ startJobsTeamsTimer();
+ });
+
}
@@ -1021,14 +1076,15 @@ document.addEventListener('DOMContentLoaded', function () {
};
// status filters: maintain arrays of *ignored* status names
- function makeStatusFilterBinding(listName) {
+ function makeStatusFilterBinding(listName, onChanged) {
return {
init: function (element, valueAccessor, _allBindings, _vm, ctx) {
const status = ko.unwrap(valueAccessor());
const cfg = ctx.$root.config;
+ const vm = ctx.$root;
if (!cfg || typeof cfg[listName] !== 'function') return;
- // initial state
+ // initial checkbox state
element.checked = cfg[listName]().includes(status);
element.addEventListener('change', function () {
@@ -1040,21 +1096,52 @@ document.addEventListener('DOMContentLoaded', function () {
} else {
arr.remove(status);
}
+
+ // now data is updated → run callback
+ if (typeof onChanged === 'function') {
+ onChanged(vm, cfg);
+ }
});
},
update: function (element, valueAccessor, _allBindings, _vm, ctx) {
const status = ko.unwrap(valueAccessor());
const cfg = ctx.$root.config;
if (!cfg || typeof cfg[listName] !== 'function') return;
+
element.checked = cfg[listName]().includes(status);
}
};
}
+
ko.bindingHandlers.teamStatusFilter = makeStatusFilterBinding('teamStatusFilter');
ko.bindingHandlers.jobStatusFilter = makeStatusFilterBinding('jobStatusFilter');
ko.bindingHandlers.incidentTypeFilter = makeStatusFilterBinding('incidentTypeFilter');
+ ko.bindingHandlers.teamStatusFilterAndFetch = makeStatusFilterBinding(
+ 'teamStatusFilter',
+ (vm, cfg) => {
+ cfg.save(); // or cfg.saveAndLoadTeamData() if you prefer
+ vm.fetchAllTeamData();
+ }
+ );
+
+ ko.bindingHandlers.jobStatusFilterAndFetch = makeStatusFilterBinding(
+ 'jobStatusFilter',
+ (vm, cfg) => {
+ cfg.save();
+ vm.fetchAllJobsData();
+ }
+ );
+
+ ko.bindingHandlers.incidentTypeFilterAndFetch = makeStatusFilterBinding(
+ 'incidentTypeFilter',
+ (vm, cfg) => {
+ cfg.save();
+ vm.fetchAllJobsData();
+ }
+ );
+
ko.bindingHandlers.trVisible = {
update: function (element, valueAccessor) {
const value = ko.unwrap(valueAccessor());
diff --git a/src/pages/tasking/models/HistoryEntry.js b/src/pages/tasking/models/HistoryEntry.js
new file mode 100644
index 00000000..80eddfb3
--- /dev/null
+++ b/src/pages/tasking/models/HistoryEntry.js
@@ -0,0 +1,56 @@
+// HistoryEntry.js
+
+/* eslint-disable @typescript-eslint/no-this-alias */
+import ko from "knockout";
+import moment from "moment";
+
+export function HistoryEntry(data = {}) {
+ const self = this;
+
+ // --- core fields ---
+ self.name = ko.observable(data.Name || "");
+ self.description = ko.observable(data.Description || "");
+
+ self.timeLoggedRaw = ko.observable(data.TimeLogged || null);
+ self.timeStampRaw = ko.observable(data.TimeStamp || null);
+
+ // derived — formatted times
+ self.timeLogged = ko.pureComputed(() => {
+ const v = self.timeLoggedRaw();
+ return v ? moment(v).format("DD/MM/YYYY HH:mm:ss") : "";
+ });
+
+ self.timeStamp = ko.pureComputed(() => {
+ const v = self.timeStampRaw();
+ return v ? moment(v).format("DD/MM/YYYY HH:mm:ss") : "";
+ });
+
+ // "time ago" label
+ self.timeStampAgo = ko.pureComputed(() => {
+ const v = self.timeStampRaw();
+ return v ? moment(v).fromNow() : "";
+ });
+
+ // "time ago" label
+ self.timeLoggedAgo = ko.pureComputed(() => {
+ const v = self.timeLoggedRaw();
+ return v ? moment(v).fromNow() : "";
+ });
+
+ // --- CreatedBy object ---
+ const created = data.CreatedBy || {};
+
+ self.createdBy = {
+ id: ko.observable(created.Id || null),
+ firstName: ko.observable(created.FirstName || ""),
+ lastName: ko.observable(created.LastName || ""),
+ fullName: ko.observable(created.FullName || ""),
+ gender: ko.observable(created.Gender || null),
+ registrationNumber: ko.observable(created.RegistrationNumber || "")
+ };
+
+ // convenience computed
+ self.createdByDisplay = ko.pureComputed(() => {
+ return self.createdBy.fullName();
+ });
+}
diff --git a/src/pages/tasking/models/Job.js b/src/pages/tasking/models/Job.js
index 23fa2c28..002c4c6b 100644
--- a/src/pages/tasking/models/Job.js
+++ b/src/pages/tasking/models/Job.js
@@ -19,6 +19,7 @@ export function Job(data = {}, deps = {}) {
fetchJobById = (_jobId, cb) => cb(null),
flyToJob = (_job) => {/* noop */ },
attachAndFillOpsLogModal = (_jobId) => ([]),
+ attachAndFillTimelineModal = (_job) => { /* noop */ },
fetchUnacknowledgedJobNotifications = async (_job) => ([]),
} = deps;
@@ -143,9 +144,6 @@ export function Job(data = {}, deps = {}) {
}
});
-
-
-
self.toggleAndLoad = function () {
if (!self.expanded()) {
self.fetchTasking();
@@ -167,6 +165,11 @@ export function Job(data = {}, deps = {}) {
attachAndFillOpsLogModal(self)
}
+ self.attachAndFillTimelineModal = function () {
+ console.log("Fetching timeline entries for job", self.id());
+ attachAndFillTimelineModal(self)
+ }
+
self.openRadioLogModal = function (tasking) {
deps.openRadioLogModal(tasking)
};
@@ -383,6 +386,48 @@ export function Job(data = {}, deps = {}) {
flyToJob(self);
};
+ self.toggleAndExpand = function () {
+ self.toggleAndLoad();
+ if (!self.expanded()) {
+ self.fetchTasking();
+ self.refreshData();
+ }
+ requestAnimationFrame(() => {
+ // find the row for this job
+ const row = document.querySelector(`tr.job-row[data-job-id="${self.id()}"]`);
+ if (!row) return;
+
+ // find the scroll container (the table wrapper in the bottom pane)
+ const container = row.closest('.pane--bottom .table-responsive')
+ || row.closest('.table-responsive')
+ || row.parentElement?.parentElement; // fallback
+
+ if (!container) {
+ // fallback to normal scrollIntoView if we can't find a container
+ row.scrollIntoView({ behavior: "smooth", block: "start" });
+ return;
+ }
+
+ // sticky header height
+ const table = row.closest('table');
+ const thead = table ? table.querySelector('thead') : null;
+ const headerHeight = thead ? thead.getBoundingClientRect().height : 0;
+
+ // compute how far we need to move the container's scrollTop
+ const containerRect = container.getBoundingClientRect();
+ const rowRect = row.getBoundingClientRect();
+
+ // desired: row just under header, with a tiny padding
+ const padding = 2;
+ const delta = (rowRect.top - containerRect.top) - headerHeight - padding;
+
+ container.scrollTo({
+ top: container.scrollTop + delta,
+ behavior: "smooth"
+ });
+ });
+ }
+
self.focusAndExpandInList = function () {
// expand the job row
self.expand();
diff --git a/src/pages/tasking/viewmodels/JobPopUp.js b/src/pages/tasking/viewmodels/JobPopUp.js
index aebb315a..58659a9d 100644
--- a/src/pages/tasking/viewmodels/JobPopUp.js
+++ b/src/pages/tasking/viewmodels/JobPopUp.js
@@ -100,6 +100,10 @@ export class JobPopupViewModel {
this.job.attachAndFillOpsLogModal(this.job);
}
+ displayTimelineForJob = () => {
+ this.job.attachAndFillTimelineModal(this.job);
+ }
+
fitBoundsWithAsset = (tasking) => {
const bounds = L.latLngBounds([
tasking.getTeamLatLng(),
diff --git a/src/pages/tasking/viewmodels/JobTimeline.js b/src/pages/tasking/viewmodels/JobTimeline.js
new file mode 100644
index 00000000..85b76962
--- /dev/null
+++ b/src/pages/tasking/viewmodels/JobTimeline.js
@@ -0,0 +1,402 @@
+// JobHistory.js
+
+/* eslint-disable @typescript-eslint/no-this-alias */
+import ko from "knockout";
+import moment from "moment";
+import { HistoryEntry } from "../models/HistoryEntry.js";
+import { OpsLogEntry } from "../models/OpsLogEntry.js";
+
+export function JobTimeline(parentVm) {
+ const self = this;
+
+ self.job = ko.observable(null);
+
+ self.historyEntries = ko.observableArray([]);
+ self.opsLogEntries = ko.observableArray([]);
+
+ self.loading = ko.observable(false);
+ self.jobIdentifier = ko.observable();
+
+ self.selectedTags = ko.observableArray([]);
+
+ // lane view mode: "both" | "history" | "ops"
+ self.laneViewMode = ko.observable("both");
+
+ self.showHistoryLane = ko.pureComputed(function () {
+ var m = self.laneViewMode();
+ return m === "both" || m === "history";
+ });
+
+ self.showOpsLane = ko.pureComputed(function () {
+ var m = self.laneViewMode();
+ return m === "both" || m === "ops";
+ });
+
+ self.showMiddleAxis = ko.pureComputed(function () {
+ return self.showHistoryLane() && self.showOpsLane();
+ });
+
+ self.opsColClass = ko.pureComputed(function () {
+ if (!self.showOpsLane()) {
+ return "d-none";
+ }
+ if (self.showHistoryLane()) {
+ // both lanes
+ return "col-5";
+ }
+ // ops only
+ return "col-12";
+ });
+
+ self.historyColClass = ko.pureComputed(function () {
+ if (!self.showHistoryLane()) {
+ return "d-none";
+ }
+ if (self.showOpsLane()) {
+ // both lanes
+ return "col-5";
+ }
+ // history only
+ return "col-12";
+ });
+
+ self.middleColClass = ko.pureComputed(function () {
+ if (!self.showMiddleAxis()) {
+ return "d-none";
+ }
+ // keep the existing centre alignment
+ return "col-2 d-flex flex-column align-items-center";
+ });
+
+ // button classes for the view mode toggle (KSB-safe: used via attr: { class: ... })
+ self.laneBtnBothClass = ko.pureComputed(function () {
+ var cls = "btn btn-sm btn-outline-secondary me-1";
+ if (self.laneViewMode() === "both") {
+ cls += " active";
+ }
+ return cls;
+ });
+
+ self.laneBtnHistoryClass = ko.pureComputed(function () {
+ var cls = "btn btn-sm btn-outline-secondary me-1";
+ if (self.laneViewMode() === "history") {
+ cls += " active";
+ }
+ return cls;
+ });
+
+ self.laneBtnOpsClass = ko.pureComputed(function () {
+ var cls = "btn btn-sm btn-outline-secondary";
+ if (self.laneViewMode() === "ops") {
+ cls += " active";
+ }
+ return cls;
+ });
+
+ // click handlers (no params; KSB-safe)
+ self.setLaneViewBoth = function () {
+ self.laneViewMode("both");
+ };
+
+ self.setLaneViewHistory = function () {
+ self.laneViewMode("history");
+ };
+
+ self.setLaneViewOps = function () {
+ self.laneViewMode("ops");
+ };
+
+
+ self.isTagSelected = function (tag) {
+ return self.selectedTags.indexOf(tag) >= 0;
+ };
+
+ self.isIcems = function (tag) {
+ return tag && tag.toLowerCase() === "icems";
+ };
+
+ self.tagButtonCss = function (tag) {
+ return {
+ "btn-outline-secondary": !self.isTagSelected(tag),
+ "btn-secondary": self.isTagSelected(tag),
+ "active": self.isTagSelected(tag),
+ "jobhistory-tag-icems": self.isIcems(tag)
+ };
+ };
+
+ self.tagFilterItems = ko.observableArray([]);
+
+ self.rebuildTagFilterItems = function () {
+ const existing = self.tagFilterItems();
+ const map = new Map();
+
+ // keep existing objects so selection state is preserved
+ existing.forEach(item => {
+ map.set(item.name().toLowerCase(), item);
+ });
+
+ // collect tags from OpsLog entries
+ const neededKeys = new Set();
+
+ self.opsLogEntries().forEach(o => {
+ const tags = o.tags ? o.tags() : [];
+ tags.forEach(t => {
+ const n = (t.name?.() || "").trim();
+ if (!n) return;
+ neededKeys.add(n);
+ });
+ });
+
+ const newItems = [];
+ neededKeys.forEach(key => {
+ let item = map.get(key);
+ if (!item) {
+ item = new TagFilterItem(key);
+ }
+ newItems.push(item);
+ });
+
+ // ICEMS first, then alpha
+ newItems.sort((a, b) => {
+ const al = (a.name() || "").toLowerCase();
+ const bl = (b.name() || "").toLowerCase();
+ if (al === "icems") return -1;
+ if (bl === "icems") return 1;
+ if (al < bl) return -1;
+ if (al > bl) return 1;
+ return 0;
+ });
+
+ self.tagFilterItems(newItems);
+ };
+
+ // rebuild tags whenever ops log entries change
+ self.opsLogEntries.subscribe(() => {
+ self.rebuildTagFilterItems();
+ });
+
+ // toggle when a button is clicked
+ self.toggleTagFilter = function (tagItem) {
+ console.log("Toggling tag filter:", tagItem);
+ if (!tagItem || !tagItem.isSelected) return;
+ tagItem.isSelected(!tagItem.isSelected());
+ };
+
+ function activeTagNames() {
+ return self.tagFilterItems()
+ .filter(t => t.isSelected())
+ .map(t => (t.name() || ""));
+ }
+
+ function opsEntryMatchesTags(entry) {
+ const selected = activeTagNames();
+ if (!selected.length) return true; // no filter
+
+ if (!entry || !entry.tags) return false;
+ const tags = entry.tags();
+ if (!Array.isArray(tags)) return false;
+
+ const names = tags
+ .map(t => (t.name?.() || ""))
+ .filter(Boolean);
+
+ // AND logic: every selected tag must be present
+ return selected.every(tag => names.indexOf(tag) !== -1);
+ }
+
+ self.availableTags = ko.pureComputed(function () {
+ var map = {}; // key = lowercase, value = display name
+
+ if (self.opsLogEntries) {
+ self.opsLogEntries().forEach(function (o) {
+ if (!o.tags) return;
+ var tags = o.tags();
+ if (!Array.isArray(tags)) return;
+
+ tags.forEach(function (t) {
+ var name = t.name ? t.name() : "";
+ if (!name) return;
+ var key = name;
+ if (!map[key]) {
+ map[key] = name; // preserve original case
+ }
+ });
+ });
+ }
+
+ var list = Object.keys(map).map(function (k) { return map[k]; });
+
+ // ICEMS first, then alpha
+ list.sort(function (a, b) {
+ var al = a.toLowerCase();
+ var bl = b.toLowerCase();
+ if (al === "icems") return -1;
+ if (bl === "icems") return 1;
+ if (al < bl) return -1;
+ if (al > bl) return 1;
+ return 0;
+ });
+
+ return list;
+ });
+
+
+ const parseDate = (v) => {
+ if (!v) return null;
+ const m = moment(v, [
+ moment.ISO_8601,
+ "YYYY-MM-DDTHH:mm:ss",
+ "YYYY-MM-DD HH:mm:ss",
+ "YYYY-MM-DD HH:mm",
+ "YYYY-MM-DDTHH:mm"
+ ], true);
+ return m.isValid() ? m : null;
+ };
+
+ // still useful if you want separate views anywhere
+ self.sortedHistoryEntries = ko.pureComputed(() => {
+ return self.historyEntries()
+ .slice()
+ .sort((a, b) => {
+ const am = parseDate(a.timeStampRaw && a.timeStampRaw() || a.timeLoggedRaw && a.timeLoggedRaw());
+ const bm = parseDate(b.timeStampRaw && b.timeStampRaw() || b.timeLoggedRaw && b.timeLoggedRaw());
+ const aT = am ? am.valueOf() : 0;
+ const bT = bm ? bm.valueOf() : 0;
+ return bT - aT; // newest first
+ });
+ });
+
+ self.sortedOpsLogEntries = ko.pureComputed(() => {
+ return self.opsLogEntries()
+ .slice()
+ .sort((a, b) => {
+ const am = parseDate(a.timeLogged && a.timeLogged() || a.createdOn && a.createdOn());
+ const bm = parseDate(b.timeLogged && b.timeLogged() || b.createdOn && b.createdOn());
+ const aT = am ? am.valueOf() : 0;
+ const bT = bm ? bm.valueOf() : 0;
+ return bT - aT; // newest first
+ });
+ });
+
+ // --- combined “two-lane” timeline ---
+ // --- combined “two-lane” timeline ---
+ self.timelineBuckets = ko.pureComputed(() => {
+ const map = new Map();
+
+ const add = (kind, entry, raw) => {
+ const m = parseDate(raw);
+ if (!m) return;
+
+ // *** group by minute ***
+ const minute = m.clone().seconds(0).milliseconds(0);
+
+ const key = minute.valueOf();
+ let bucket = map.get(key);
+ if (!bucket) {
+ bucket = {
+ key,
+ // label based on minute start, not raw time
+ label: minute.format("DD/MM/YYYY HH:mm"),
+ rel: minute.fromNow(),
+ ops: [],
+ history: []
+ };
+ map.set(key, bucket);
+ }
+
+ if (kind === "ops") {
+ bucket.ops.push(entry);
+ } else {
+ bucket.history.push(entry);
+ }
+ };
+
+ // history: prefer TimeStamp, fallback TimeLogged
+ self.historyEntries().forEach(h => {
+ const raw =
+ (h.timeStampRaw && h.timeStampRaw()) ||
+ (h.timeLoggedRaw && h.timeLoggedRaw());
+ add("history", h, raw);
+ });
+
+ // ops: prefer TimeLogged, fallback CreatedOn
+ self.opsLogEntries().forEach(o => {
+ if (!opsEntryMatchesTags(o)) return;
+
+ const raw =
+ (o.timeLogged && o.timeLogged()) ||
+ (o.createdOn && o.createdOn());
+ add("ops", o, raw);
+ });
+
+ // drop buckets that ended up empty
+ var buckets = Array.from(map.values()).filter(function (b) {
+ return b.ops.length > 0 || b.history.length > 0;
+ });
+
+ // newest minute first
+ buckets.sort(function (a, b) { return b.key - a.key; });
+ return buckets;
+ });
+
+ self.openForJob = async (job) => {
+ self.laneViewMode("both"); //this is just better. force people to like it
+ self.jobIdentifier(job.identifier() || "");
+ self.job(job);
+ self.historyEntries([]);
+ self.opsLogEntries([]);
+ self.loading(true);
+
+ const historyResults = new Promise((resolve, reject) => {
+ parentVm.fetchHistoryForJob(
+ job.id(),
+ function (res) {
+ self.historyEntries((res || []).map((e) => new HistoryEntry(e)));
+ resolve(res);
+ },
+ reject
+ );
+ });
+
+ const opsLogResults = new Promise((resolve, reject) => {
+ parentVm.fetchOpsLogForJob(
+ job.id(),
+ function (res) {
+ self.opsLogEntries((res || []).map((e) => new OpsLogEntry(e)));
+ resolve(res);
+ },
+ reject
+ );
+ });
+
+ try {
+ await Promise.all([historyResults, opsLogResults]);
+ } catch (e) {
+ console.error("Error loading job history or ops log:", e);
+ } finally {
+ self.loading(false);
+ }
+ };
+}
+
+
+function TagFilterItem(name) {
+ const self = this;
+
+ self.name = ko.observable(name);
+ self.isSelected = ko.observable(false);
+
+ self.isIcems = ko.pureComputed(() => {
+ const n = (self.name() || "").toLowerCase();
+ return n === "icems";
+ });
+
+ // Used directly by css: cssClasses
+ self.cssClasses = ko.pureComputed(() => ({
+ "jobhistory-tag-btn": true,
+ "btn-outline-secondary": !self.isSelected(),
+ "btn-secondary": self.isSelected(),
+ "active": self.isSelected(),
+ "jobhistory-tag-icems": self.isIcems()
+ }));
+}
\ No newline at end of file
diff --git a/src/pages/tasking/viewmodels/OpsLogModalVM.js b/src/pages/tasking/viewmodels/OpsLogModalVM.js
index 94bbfee3..813fc383 100644
--- a/src/pages/tasking/viewmodels/OpsLogModalVM.js
+++ b/src/pages/tasking/viewmodels/OpsLogModalVM.js
@@ -135,7 +135,7 @@ export function OpsLogModalVM(parentVm) {
};
}
-export function NewOpsLogModalVM(parentVM) {
+export function CreateOpsLogModalVM(parentVM) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
@@ -227,7 +227,6 @@ export function NewOpsLogModalVM(parentVM) {
self.openForNewJobLog = async (job) => {
self.resetFields();
self.jobId(job.id() || "");
- console.log(job);
self.initTags();
self.headerLabel(`New Ops Log for ${job.identifier() || ""}`);
}
diff --git a/src/pages/tasking/viewmodels/RadioLogModalVM.js b/src/pages/tasking/viewmodels/RadioLogModalVM.js
new file mode 100644
index 00000000..97eb7a6a
--- /dev/null
+++ b/src/pages/tasking/viewmodels/RadioLogModalVM.js
@@ -0,0 +1,196 @@
+/* eslint-disable @typescript-eslint/no-this-alias */
+import ko from "knockout";
+
+export function CreateRadioLogModalVM(parentVM) {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const self = this;
+
+ self.entityId = ko.observable(null);
+ self.jobId = ko.observable(null);
+ self.eventId = ko.observable(null);
+ self.talkgroupId = ko.observable(null);
+ self.talkgroupRequestId = ko.observable(null);
+
+ self.subject = ko.observable("");
+ self.text = ko.observable("");
+ self.position = ko.observable(null);
+ self.personFromId = ko.observable(null);
+ self.personTold = ko.observable(null);
+
+ self.important = ko.observable(false);
+ self.restricted = ko.observable(false);
+ self.actionRequired = ko.observable(false);
+ self.actionReminder = ko.observable(null);
+
+ self.tagIds = ko.observableArray([]);
+ self.timeLogged = ko.observable(null);
+
+ // UI
+ self.headerLabel = ko.observable("");
+ self.tagsByGroup = {};
+ self.errorMessage = ko.observable("");
+ self.showError = ko.observable(false);
+
+ self.uiTags = ko.observableArray([]);
+
+ self.initTags = () => {
+ const tags = parentVM.allTags ? parentVM.allTags() : [];
+ self.uiTags(tags.map(tag => createUiTag(tag)));
+ };
+
+ self.tagIds = ko.computed(() =>
+ self.uiTags()
+ .filter(t => t.selected())
+ .map(t => t.id())
+ );
+
+ [2, 3, 4, 27].forEach(gid => {
+ self.tagsByGroup[gid] = ko.pureComputed(() =>
+ self.uiTags().filter(t => t.groupId() === gid)
+ );
+ });
+
+ function preselectTag(idToSelect) {
+ self.uiTags().forEach(t => {
+ t.selected(t.id() === idToSelect);
+ });
+ }
+
+ self.tagIds = ko.computed(() =>
+ self.uiTags()
+ .filter(t => t.selected())
+ .map(t => t.id())
+ );
+
+ self.ActionRequiredCheck = ko.computed(() => {
+ const anyActionTagSelected = self.uiTags().some(
+ t => t.selected() && t.groupId() === 27
+ );
+
+ self.actionRequired(anyActionTagSelected);
+ });
+
+ self.openForTasking = async (tasking) => {
+ self.resetFields();
+ self.jobId(tasking.job.id() || "");
+ self.subject(tasking.teamCallsign() || "");
+ self.initTags();
+ preselectTag(6);
+
+ self.headerLabel(`New Radio Log for ${tasking.teamCallsign?.() || ""} on ${tasking.job.identifier() || ""}`);
+ }
+
+ self.openForTeam = async (team) => {
+ // NEED TO COME BACK AND ADD HQ ONCE TIM HAS SETUP THE CONFIG PAGE TO USE IT.
+ self.resetFields();
+ self.subject(team.callsign() || "");
+ self.initTags();
+ preselectTag(6);
+
+ self.headerLabel(`New Radio Log for ${team.callsign?.() || ""}`);
+ }
+
+ self.toPayload = function () {
+ return {
+ EntityId: self.entityId(),
+ JobId: self.jobId(),
+ EventId: self.eventId(),
+ TalkgroupId: self.talkgroupId(),
+ TalkgroupRequestId: self.talkgroupRequestId(),
+
+ Subject: self.subject(),
+ Text: self.text(),
+ Position: self.position(),
+ PersonFromId: self.personFromId(),
+ PersonTold: self.personTold(),
+
+ Important: self.important(),
+ Restricted: self.restricted(),
+ ActionRequired: self.actionRequired(),
+ ActionReminder: self.actionReminder(),
+
+ TagIds: self.tagIds(),
+ TimeLogged: self.timeLogged()
+ };
+ };
+
+ function validate() {
+ self.showError(false);
+ self.errorMessage("");
+
+ // No text
+ if (!self.text() || self.text().trim().length === 0) {
+ self.errorMessage("Text is required.");
+ self.showError(true);
+ return false;
+ }
+
+ // No tags
+ if (self.tagIds().length === 0) {
+ self.errorMessage("You must select at least one tag.");
+ self.showError(true);
+ return false;
+ }
+
+ return true;
+ }
+
+ self.submit = function () {
+ if (!validate()) {
+ return;
+ }
+ const payload = self.toPayload();
+
+ parentVM.createOpsLogEntry(payload, function (result) {
+
+ if (!result) {
+ console.error("Ops Log submit failed");
+ return;
+ }
+
+ if (self.modalInstance) {
+ self.modalInstance.hide();
+ }
+ });
+ };
+
+ self.resetFields = function () {
+ self.entityId(null);
+ self.jobId(null);
+ self.eventId(null);
+ self.talkgroupId(null);
+ self.talkgroupRequestId(null);
+
+ self.subject("");
+ self.text("");
+ self.position(null);
+ self.personFromId(null);
+ self.personTold(null);
+
+ self.important(false);
+ self.restricted(false);
+ self.actionRequired(false);
+ self.actionReminder(null);
+
+ self.timeLogged(null);
+
+ self.uiTags([]);
+ self.headerLabel("");
+ };
+
+ function createUiTag(tag) {
+ return {
+ model: tag,
+
+ id: tag.id,
+ name: tag.name,
+ groupId: tag.tagGroupId,
+
+ selected: ko.observable(false),
+
+ toggle() {
+ this.selected(!this.selected());
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/shared/BeaconClient/job.js b/src/shared/BeaconClient/job.js
index 22a12e2b..15d33b4e 100644
--- a/src/shared/BeaconClient/job.js
+++ b/src/shared/BeaconClient/job.js
@@ -187,4 +187,21 @@ export function searchwithFilter(unit, host, StartDate, EndDate, userId = 'notPa
}
);
+}
+
+export function getHistory(id, host, userId = 'notPassed', token, callback) {
+ $.ajax({
+ type: 'GET',
+ url: host + "/Api/v1/Jobs/" + id + "/History/?LighthouseFunction=getHistory&userId=" + userId,
+ beforeSend: function (n) {
+ n.setRequestHeader("Authorization", "Bearer " + token)
+ },
+ cache: false,
+ dataType: 'json',
+ complete: function (response, textStatus) {
+ if (textStatus == 'success') {
+ callback(response.responseJSON);
+ }
+ }
+ });
}
\ No newline at end of file
diff --git a/static/pages/tasking.html b/static/pages/tasking.html
index 2889f285..ce626e40 100644
--- a/static/pages/tasking.html
+++ b/static/pages/tasking.html
@@ -26,21 +26,74 @@
Focus
- Ops Log
+ data-bind="click: j.attachAndFillTimelineModal">
+ Time Line
@@ -1402,11 +1455,200 @@
-
+
+
+
+
+
+
+ Incident History —
+
+
+
+
+
+
+
+
+ Tag filter (AND):
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No history or ops log entries have been recorded for this incident yet.
+
+
+
+
+
+
+
+
+
+
+
Ops Log
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ •
+
+
+
+
+
+
+
+
+
+
+ Important
+ ICEMS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Incident History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ •
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -1432,11 +1674,11 @@
-
+
-
+
-
+
diff --git a/styles/pages/tasking.css b/styles/pages/tasking.css
index 73be910e..c48e2c13 100644
--- a/styles/pages/tasking.css
+++ b/styles/pages/tasking.css
@@ -876,14 +876,14 @@ tr.job:hover {
margin: 0;
}
-#NewOpsLogModal h6 {
+#CreateOpsLogModal h6 {
margin-top: 0.75rem !important;
margin-bottom: 0.35rem !important;
font-weight: 600;
}
-#NewOpsLogModal input[type="text"],
-#NewOpsLogModal textarea {
+#CreateOpsLogModal input[type="text"],
+#CreateOpsLogModal textarea {
width: 100%;
border-radius: 6px;
border: 1px solid #ccc;
@@ -893,8 +893,8 @@ tr.job:hover {
}
/* Focus state */
-#NewOpsLogModal input[type="text"]:focus,
-#NewOpsLogModal textarea:focus {
+#CreateOpsLogModal input[type="text"]:focus,
+#CreateOpsLogModal textarea:focus {
border-color: #1e8740; /* theme colour? */
box-shadow: 0 0 0 2px rgba(30, 135, 64, 0.2);
outline: none;
@@ -1090,4 +1090,137 @@ tr.job:hover {
font-family: monospace;
font-size: 1.3rem;
letter-spacing: 0.05rem;
+}
+
+
+
+
+/* === Job History / Ops Log two-lane timeline === */
+
+.job-history-timeline {
+ margin-top: 0.5rem;
+}
+
+.job-history-row {
+ position: relative;
+}
+
+/* centre rail (time axis) */
+.job-history-rail {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ min-height: 3rem;
+}
+
+.job-history-dot {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background-color: var(--bs-primary);
+ border: 2px solid #fff;
+ box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.25);
+ z-index: 1;
+}
+
+.job-history-line {
+ flex: 1;
+ width: 2px;
+ background-color: #dee2e6;
+ margin-top: 2px;
+}
+
+.jobhistory-tag-btn {
+ font-size: 0.75rem;
+ padding: 0.1rem 0.4rem;
+ vertical-align: middle; /* THIS FIXES THE FLOATING */
+ line-height: 1.2; /* Helps even out height across browsers */
+}
+
+/* Legend container */
+.jobhistory-legend {
+ color: #6c757d; /* muted */
+}
+
+/* Swatch shape: little bar matching card borders */
+.jobhistory-legend-swatch {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ border-radius: 2px;
+ background-color: #fff;
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
+ border-left-width: 4px;
+ border-left-style: solid;
+}
+
+/* Match your card border colours */
+
+.jobhistory-legend-history {
+ border-left-color: var(--bs-success); /* .job-history-card-history */
+}
+
+.jobhistory-legend-ops-normal {
+ border-left-color: var(--bs-primary); /* .job-history-card-ops default */
+}
+
+.jobhistory-legend-ops-important {
+ border-left-color: var(--bs-warning); /* .job-history-card-flagged */
+}
+
+/* ICEMS: hint at double border */
+.jobhistory-legend-ops-icems {
+ border-left-color: var(--bs-primary);
+ border-left-style: double;
+}
+
+/* cards */
+
+.job-history-card {
+ border-radius: 0.5rem;
+ border: 1px solid #dee2e6;
+ border-left-width: 4px;
+ background-color: #fff;
+}
+
+.job-history-card-ops {
+ border-left-color: var(--bs-primary);
+}
+
+.job-history-card-history {
+ border-left-color: var(--bs-success);
+}
+.job-history-card-flagged {
+ border-left-color: var(--bs-warning);
+ border-left-style: solid !important;
+}
+
+.job-history-card-icems {
+ border-left-style: double;
+}
+
+/* common hover */
+.job-history-card-ops:hover,
+.job-history-card-history:hover {
+ box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.08);
+}
+
+/* tighten spacing in the modal */
+#jobHistoryModal .modal-body {
+ padding-top: 0.75rem;
+}
+
+.jobhistory-tag-btn {
+ font-size: 0.75rem;
+ padding: 0.1rem 0.4rem;
+}
+
+.jobhistory-tag-btn.jobhistory-tag-icems {
+ border-color: var(--bs-secondary);
+}
+
+/* when ICEMS is selected (btn-secondary active already applied) */
+.jobhistory-tag-btn.jobhistory-tag-icems.btn-secondary {
+ background-color: var(--bs-secondary);
}
\ No newline at end of file
From 12bb64d08355d3fa2ace489773562e33b27f48d4 Mon Sep 17 00:00:00 2001
From: Tim Dykes
Date: Mon, 24 Nov 2025 12:14:51 +1100
Subject: [PATCH 05/61] Tdykes.tasking.wip (#224)
* boostrap upgrade broke team summary.
This restores bootstrap 4 and adds an alias for bootstrap 5
* first push - new tasking dashboard
* New features
* New features
* moved map to vm
made a new vm just for map
* refactored out the mapvm
* map popups have their own view models now
* starting to flesh out the popups UI
new UI for popups. refactor all the models
* Map with a capital M
wtf is mac doing with this
* Add fit bound button to job pop
* removed leaflet-responsive-popup
back to normal popups
* missed a bad import
* ffs and another
* redoing the setup screen. this one isnt horrible
unit search at the top, both results boxes below
* new config page with dropdown for HQ search
* All sorts of new stuff
New pulse marker for unclaimed
New icons
New refacactor of colours
New incident menu
* linter fix
* UX Overlays
Legend updates
Notification overlays
* linter fix
* small refactor
* only untask warn on active
* extension for opsLog for single entry
* ICEMS unack alerts and fixed legend
* layer draw prototype
* started making transport NSW api code
* lots of code cleanup
* team and job list sorting on column header press
* opslog filters. sticky row headers on jobs and teams
* draggable row fixes. focus on job button
* linter fix
* only deploy on dev branch
* correct filtering of job and team status. added image icon
* Core OpsLog Creation Functionality + New Radio Log UI (#219)
* NewOpsLogEntry Core + Instant Radio Team on Job
Created:
- BeaconClient for creating (POST) New OpsLogEntries.
- Foundation for creating OpsLogEntries via Modal.
- Radio Log button for Teams on Incidents (Taskings).
* Added Team Radio Log Button
Added:
- Team Radio Log (Job agnostic) button & functions.
- Added new status update button to Incidents -> Tasking & Teams -> Taskings (Task list icons)
- Updated Radio Log button to Microphone (inline with other Lighthouse Instant Radio Log icons)
- Started work on new OpsLogEntry code.
* Removing incorrectly duplicated data-bind
* unfucked the map for jobs
unfucked the map for jobs
* Draft New Ops Log Modal
* Create enum.js
* map in some enums correctly, refresh job tasking on expand
* clicking a job calls focus and expands in the job table
* new asset popup
has protection for clicking jobs that are not in filter.
new job ifFiltered logic so it wont update if its out of view
* show situation on scene
* bad refresh bug
refresh was stuck in a loop updating every 30 seconds
* wrong icon
* restrict row drag to just the first cell
* fixed the double toggle load
* new jobs list layout
* Bug Fixes
Teams had no filters on their obvervables
jobs and assets had no disposal methods
teams had no JSON update function
jobs popup had no protection for non asset teams or team out of view
* team list has the new cards list. tasking status badges have consistent color
* job status filters logic and new UI in the menu
* safe defaults in panel layout
* New Ops Log Functionality
-Completed New Ops Log Functionality.
-Slight rework of how the NewOpsLogModalVM handles tags.
-Added error catching for missing required fields in New Ops Logs.
* Config share code + fix for fetching jobs without auth
* UX tweaks on share config
* hooked up real url
* slightly better filter handling
* bugfix - filter boxes were not applying
* bug fix: handler for config checkboxes had back click handling
caused the config to save before the values had been updated.
* Separated Create Ops Log & Create Radio Log
Moved RadioLog into it's own VM (CreateRadioLogModalVM) - also renamed NewOpsLogModalVM to CreateOpsLogModalVM.
* job timeline magic
* tidy up after merge
* new job details thingy
* popup smoothing. new range circles. style updates
* smooth the page load so that it fades in once the dom loads
---------
Co-authored-by: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
---
src/pages/tasking/components/job_popup.js | 14 +-
src/pages/tasking/main.js | 17 +-
src/pages/tasking/models/Job.js | 85 +++-
src/pages/tasking/viewmodels/JobPopUp.js | 88 +++-
src/pages/tasking/viewmodels/JobTimeline.js | 25 +-
src/pages/tasking/viewmodels/Map.js | 155 ++++++-
static/pages/tasking.html | 458 ++++++++++++--------
styles/pages/tasking.css | 48 +-
8 files changed, 655 insertions(+), 235 deletions(-)
diff --git a/src/pages/tasking/components/job_popup.js b/src/pages/tasking/components/job_popup.js
index 48a6685c..ddb8a61c 100644
--- a/src/pages/tasking/components/job_popup.js
+++ b/src/pages/tasking/components/job_popup.js
@@ -60,7 +60,7 @@ export function buildJobPopupKO() {
- Save & Close
+ data-bind="click: config.saveAndCloseAndLoad, disable: pageIsLoading">
+ Save & Close
- Loading...
+ data-bind="visible: pageIsLoading">
+ Loading...
From a429dea6422c58d0b554fddd842839ae23464766 Mon Sep 17 00:00:00 2001
From: Tim Dykes
Date: Thu, 27 Nov 2025 14:48:42 +1100
Subject: [PATCH 10/61] Tdykes.tasking.wip (#229)
* boostrap upgrade broke team summary.
This restores bootstrap 4 and adds an alias for bootstrap 5
* first push - new tasking dashboard
* New features
* New features
* moved map to vm
made a new vm just for map
* refactored out the mapvm
* map popups have their own view models now
* starting to flesh out the popups UI
new UI for popups. refactor all the models
* Map with a capital M
wtf is mac doing with this
* Add fit bound button to job pop
* removed leaflet-responsive-popup
back to normal popups
* missed a bad import
* ffs and another
* redoing the setup screen. this one isnt horrible
unit search at the top, both results boxes below
* new config page with dropdown for HQ search
* All sorts of new stuff
New pulse marker for unclaimed
New icons
New refacactor of colours
New incident menu
* linter fix
* UX Overlays
Legend updates
Notification overlays
* linter fix
* small refactor
* only untask warn on active
* extension for opsLog for single entry
* ICEMS unack alerts and fixed legend
* layer draw prototype
* started making transport NSW api code
* lots of code cleanup
* team and job list sorting on column header press
* opslog filters. sticky row headers on jobs and teams
* draggable row fixes. focus on job button
* linter fix
* only deploy on dev branch
* correct filtering of job and team status. added image icon
* Core OpsLog Creation Functionality + New Radio Log UI (#219)
* NewOpsLogEntry Core + Instant Radio Team on Job
Created:
- BeaconClient for creating (POST) New OpsLogEntries.
- Foundation for creating OpsLogEntries via Modal.
- Radio Log button for Teams on Incidents (Taskings).
* Added Team Radio Log Button
Added:
- Team Radio Log (Job agnostic) button & functions.
- Added new status update button to Incidents -> Tasking & Teams -> Taskings (Task list icons)
- Updated Radio Log button to Microphone (inline with other Lighthouse Instant Radio Log icons)
- Started work on new OpsLogEntry code.
* Removing incorrectly duplicated data-bind
* unfucked the map for jobs
unfucked the map for jobs
* Draft New Ops Log Modal
* Create enum.js
* map in some enums correctly, refresh job tasking on expand
* clicking a job calls focus and expands in the job table
* new asset popup
has protection for clicking jobs that are not in filter.
new job ifFiltered logic so it wont update if its out of view
* show situation on scene
* bad refresh bug
refresh was stuck in a loop updating every 30 seconds
* wrong icon
* restrict row drag to just the first cell
* fixed the double toggle load
* new jobs list layout
* Bug Fixes
Teams had no filters on their obvervables
jobs and assets had no disposal methods
teams had no JSON update function
jobs popup had no protection for non asset teams or team out of view
* team list has the new cards list. tasking status badges have consistent color
* job status filters logic and new UI in the menu
* safe defaults in panel layout
* New Ops Log Functionality
-Completed New Ops Log Functionality.
-Slight rework of how the NewOpsLogModalVM handles tags.
-Added error catching for missing required fields in New Ops Logs.
* Config share code + fix for fetching jobs without auth
* UX tweaks on share config
* hooked up real url
* slightly better filter handling
* bugfix - filter boxes were not applying
* bug fix: handler for config checkboxes had back click handling
caused the config to save before the values had been updated.
* Separated Create Ops Log & Create Radio Log
Moved RadioLog into it's own VM (CreateRadioLogModalVM) - also renamed NewOpsLogModalVM to CreateOpsLogModalVM.
* job timeline magic
* tidy up after merge
* new job details thingy
* popup smoothing. new range circles. style updates
* smooth the page load so that it fades in once the dom loads
* linter fix
* Daily
Added mini map
Added geoservices common code and map layer for unit boundaries
* better layer menu
kinda?
* map layers refactored out into their own files
* fixup of something that has nothing to do with this WIP
* Team Status Update Functionality
Team Status functionality added & tested. UI is still a bit janky but we'll come back to that.
* Got rid of defunct team status update button
* tag code
* bad import
* turn down the debug
* fixed stupid map zoom bug on unit boundary
* rewrite of the scroll into view stuff
---------
Co-authored-by: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
---
src/pages/tasking/markers/assetMarker.js | 11 ++-
src/pages/tasking/markers/jobMarker.js | 2 +
src/pages/tasking/models/Job.js | 87 +++++++++---------------
src/pages/tasking/models/Team.js | 85 ++++++++++++++++++++++-
src/pages/tasking/viewmodels/JobPopUp.js | 4 +-
static/pages/tasking.html | 5 +-
6 files changed, 131 insertions(+), 63 deletions(-)
diff --git a/src/pages/tasking/markers/assetMarker.js b/src/pages/tasking/markers/assetMarker.js
index 7963e383..e8947c96 100644
--- a/src/pages/tasking/markers/assetMarker.js
+++ b/src/pages/tasking/markers/assetMarker.js
@@ -76,6 +76,13 @@ export function attachAssetMarker(ko, map, viewModel, asset) {
m.addTo(layer);
asset.marker = m;
+ // Register with spiderfier
+ if (window.oms) {
+ window.oms.addMarker(m);
+ m.on('mouseover', () => window.oms.spiderfy(m));
+ m.on('mouseout', () => window.oms.unspiderfy());
+ }
+
// Ask MapVM to create the AssetPopupViewModel for this asset:
const popupVm = viewModel.mapVM.makeAssetPopupVM(asset);
bindPopupWithKO(ko, asset.marker, viewModel, asset, popupVm);
@@ -152,6 +159,8 @@ function bindPopupWithKO(ko, marker, vm, asset, popupVm) {
const el = e.popup.getContent(); // our stable node
vm.mapVM.setOpen?.('asset', asset);
bindKoToPopup(ko, popupVm, el);
+ asset.matchingTeams()?.length !==0 && asset.matchingTeams()[0].onPopupOpen()
+ popupVm.updatePopup?.();
deferPopupUpdate(e.popup);
};
const closeHandler = (e) => {
@@ -160,7 +169,7 @@ function bindPopupWithKO(ko, marker, vm, asset, popupVm) {
vm.mapVM.clearRoutes?.();
vm.mapVM.clearOpen?.();
unbindKoFromPopup(ko, el);
-
+ asset.matchingTeams()?.length !==0 && asset.matchingTeams()[0].onPopupClose()
};
marker._koWired = true;
marker.on('popupopen', openHandler);
diff --git a/src/pages/tasking/markers/jobMarker.js b/src/pages/tasking/markers/jobMarker.js
index d8510619..73c477f3 100644
--- a/src/pages/tasking/markers/jobMarker.js
+++ b/src/pages/tasking/markers/jobMarker.js
@@ -172,6 +172,8 @@ function safeMove(marker, job) {
if (Number.isFinite(lat) && Number.isFinite(lng)) marker.setLatLng([lat, lng]);
}
+
+
function wireKoForPopup(ko, marker, job, vm, popupVM) {
if (marker._koWired) return;
marker.on('popupopen', e => {
diff --git a/src/pages/tasking/models/Job.js b/src/pages/tasking/models/Job.js
index f2d19ee6..8f2cb960 100644
--- a/src/pages/tasking/models/Job.js
+++ b/src/pages/tasking/models/Job.js
@@ -461,81 +461,58 @@ export function Job(data = {}, deps = {}) {
self.fetchTasking();
self.refreshData();
}
- requestAnimationFrame(() => {
- // find the row for this job
- const row = document.querySelector(`tr.job-row[data-job-id="${self.id()}"]`);
- if (!row) return;
-
- // find the scroll container (the table wrapper in the bottom pane)
- const container = row.closest('.pane--bottom .table-responsive')
- || row.closest('.table-responsive')
- || row.parentElement?.parentElement; // fallback
-
- if (!container) {
- // fallback to normal scrollIntoView if we can't find a container
- row.scrollIntoView({ behavior: "smooth", block: "start" });
- return;
- }
-
- // sticky header height
- const table = row.closest('table');
- const thead = table ? table.querySelector('thead') : null;
- const headerHeight = thead ? thead.getBoundingClientRect().height : 0;
-
- // compute how far we need to move the container's scrollTop
- const containerRect = container.getBoundingClientRect();
- const rowRect = row.getBoundingClientRect();
-
- // desired: row just under header, with a tiny padding
- const padding = 2;
- const delta = (rowRect.top - containerRect.top) - headerHeight - padding;
-
- container.scrollTo({
- top: container.scrollTop + delta,
- behavior: "smooth"
- });
- });
+ scrollToThisInTable();
}
self.focusAndExpandInList = function () {
// expand the job row
self.expand();
- requestAnimationFrame(() => {
- // find the row for this job
- const row = document.querySelector(`tr.job-row[data-job-id="${self.id()}"]`);
- if (!row) return;
+ scrollToThisInTable();
+ };
+
- // find the scroll container (the table wrapper in the bottom pane)
- const container = row.closest('.pane--bottom .table-responsive')
- || row.closest('.table-responsive')
- || row.parentElement?.parentElement; // fallback
+ function scrollToThisInTable() {
+ setTimeout(() => {
+ const row = document.querySelector(
+ `tr.job-row[data-job-id="${self.id()}"]`
+ );
+ if (!row) return;
+ // Scroll container is the top pane
+ const container = document.querySelector('#paneBottom .table-responsive');
if (!container) {
- // fallback to normal scrollIntoView if we can't find a container
row.scrollIntoView({ behavior: "smooth", block: "start" });
return;
}
- // sticky header height
- const table = row.closest('table');
- const thead = table ? table.querySelector('thead') : null;
- const headerHeight = thead ? thead.getBoundingClientRect().height : 0;
+ // Sticky header height
+ const table = row.closest("table");
+ const thead = table ? table.querySelector("thead") : null;
+ const headerHeight = thead
+ ? thead.getBoundingClientRect().height
+ : 0;
- // compute how far we need to move the container's scrollTop
const containerRect = container.getBoundingClientRect();
const rowRect = row.getBoundingClientRect();
-
- // desired: row just under header, with a tiny padding
const padding = 2;
- const delta = (rowRect.top - containerRect.top) - headerHeight - padding;
+
+ // Where we *want* the row: just under the header
+ let target =
+ container.scrollTop +
+ (rowRect.top - containerRect.top) -
+ headerHeight -
+ padding;
+
+ // Only clamp to >= 0; don't clamp to maxScroll here
+ if (target < 0) target = 0;
container.scrollTo({
- top: container.scrollTop + delta,
- behavior: "smooth"
+ top: target,
+ behavior: "smooth",
});
- });
- };
+ }, 150);
+ }
self.rowHasFocus = ko.observable(false);
diff --git a/src/pages/tasking/models/Team.js b/src/pages/tasking/models/Team.js
index 80ce1bb6..7059af22 100644
--- a/src/pages/tasking/models/Team.js
+++ b/src/pages/tasking/models/Team.js
@@ -38,6 +38,84 @@ export function Team(data = {}, deps = {}) {
self.trackableAssets = ko.observableArray([]);
+ self.toggleAndExpand = function () {
+ const wasExpanded = self.expanded();
+ self.expanded(!wasExpanded);
+
+ // If we just collapsed, no scroll
+ if (wasExpanded) return;
+
+ scrollToThisInTable();
+
+ };
+
+
+
+
+
+
+
+ self.focusAndExpandInList = function () {
+ self.expand();
+
+
+ scrollToThisInTable();
+ };
+
+
+ function scrollToThisInTable() {
+ setTimeout(() => {
+ const row = document.querySelector(
+ `tr.team-row[data-team-id="${self.id()}"]`
+ );
+ if (!row) return;
+
+ // Scroll container is the top pane
+ const container = document.querySelector('#paneTop .table-responsive');
+ if (!container) {
+ row.scrollIntoView({ behavior: "smooth", block: "start" });
+ return;
+ }
+
+ // Sticky header height
+ const table = row.closest("table");
+ const thead = table ? table.querySelector("thead") : null;
+ const headerHeight = thead
+ ? thead.getBoundingClientRect().height
+ : 0;
+
+ const containerRect = container.getBoundingClientRect();
+ const rowRect = row.getBoundingClientRect();
+ const padding = 2;
+
+ // Where we *want* the row: just under the header
+ let target =
+ container.scrollTop +
+ (rowRect.top - containerRect.top) -
+ headerHeight -
+ padding;
+
+ // Only clamp to >= 0; don't clamp to maxScroll here
+ if (target < 0) target = 0;
+
+
+ container.scrollTo({
+ top: target,
+ behavior: "smooth",
+ });
+ }, 150);
+ }
+
+
+ self.onPopupOpen = function () {
+ self.focusAndExpandInList();
+ };
+
+ self.onPopupClose = function () {
+ self.collapse();
+ };
+
+
// ---- DATA REFRESH CHECK ----
const dataRefreshInterval = makeFilteredInterval(async () => {
const now = Date.now();
@@ -106,7 +184,7 @@ export function Team(data = {}, deps = {}) {
return self.filteredTaskings() && self.filteredTaskings().length || 0;
})
-
+
self.currentTaskingSummary = ko.pureComputed(() => {
const typeCounts = {};
@@ -149,7 +227,7 @@ export function Team(data = {}, deps = {}) {
});
self.taskingRowColour = ko.pureComputed(() => {
- if (self.activeTaskingsCount() === 0) {
+ if (self.taskedJobCount() === 0) {
return '#d4edda'; // light green
}
})
@@ -218,12 +296,13 @@ export function Team(data = {}, deps = {}) {
//only handle single asset for now
self.markerFocus = function () {
+ if (self.isFilteredIn() === false) return;
const a = self.trackableAssets()[0];
if (!a) return;
flyToAsset(a); // map logic stays out of the model
};
- self.openRadioLogModal = function () {
+ self.openRadioLogModal = function () {
deps.openRadioLogModal(self);
};
diff --git a/src/pages/tasking/viewmodels/JobPopUp.js b/src/pages/tasking/viewmodels/JobPopUp.js
index 4730c15f..3ec7e78f 100644
--- a/src/pages/tasking/viewmodels/JobPopUp.js
+++ b/src/pages/tasking/viewmodels/JobPopUp.js
@@ -79,7 +79,6 @@ export class JobPopupViewModel {
drawCrowsFliesToAssetPassedTeam = (team) => {
- console.log(team)
// clear any existing one first
this.api.clearCrowFliesLine();
if (!team) return;
@@ -111,6 +110,9 @@ export class JobPopupViewModel {
if (tasking.job.isFilteredIn() === false) {
return;
}
+ if (tasking.team.isFilteredIn() === false) {
+ return;
+ }
// clear any existing one first
this.api.clearCrowFliesLine();
if (!tasking) return;
diff --git a/static/pages/tasking.html b/static/pages/tasking.html
index fe5c6812..bbbd2000 100644
--- a/static/pages/tasking.html
+++ b/static/pages/tasking.html
@@ -202,7 +202,6 @@
From 5d2308720e783d510129ccc44fe5857dec3475a8 Mon Sep 17 00:00:00 2001
From: Tim Dykes
Date: Fri, 28 Nov 2025 14:29:13 +1100
Subject: [PATCH 15/61] Tdykes.tasking.wip (#234)
* boostrap upgrade broke team summary.
This restores bootstrap 4 and adds an alias for bootstrap 5
* first push - new tasking dashboard
* New features
* New features
* moved map to vm
made a new vm just for map
* refactored out the mapvm
* map popups have their own view models now
* starting to flesh out the popups UI
new UI for popups. refactor all the models
* Map with a capital M
wtf is mac doing with this
* Add fit bound button to job pop
* removed leaflet-responsive-popup
back to normal popups
* missed a bad import
* ffs and another
* redoing the setup screen. this one isnt horrible
unit search at the top, both results boxes below
* new config page with dropdown for HQ search
* All sorts of new stuff
New pulse marker for unclaimed
New icons
New refacactor of colours
New incident menu
* linter fix
* UX Overlays
Legend updates
Notification overlays
* linter fix
* small refactor
* only untask warn on active
* extension for opsLog for single entry
* ICEMS unack alerts and fixed legend
* layer draw prototype
* started making transport NSW api code
* lots of code cleanup
* team and job list sorting on column header press
* opslog filters. sticky row headers on jobs and teams
* draggable row fixes. focus on job button
* linter fix
* only deploy on dev branch
* correct filtering of job and team status. added image icon
* Core OpsLog Creation Functionality + New Radio Log UI (#219)
* NewOpsLogEntry Core + Instant Radio Team on Job
Created:
- BeaconClient for creating (POST) New OpsLogEntries.
- Foundation for creating OpsLogEntries via Modal.
- Radio Log button for Teams on Incidents (Taskings).
* Added Team Radio Log Button
Added:
- Team Radio Log (Job agnostic) button & functions.
- Added new status update button to Incidents -> Tasking & Teams -> Taskings (Task list icons)
- Updated Radio Log button to Microphone (inline with other Lighthouse Instant Radio Log icons)
- Started work on new OpsLogEntry code.
* Removing incorrectly duplicated data-bind
* unfucked the map for jobs
unfucked the map for jobs
* Draft New Ops Log Modal
* Create enum.js
* map in some enums correctly, refresh job tasking on expand
* clicking a job calls focus and expands in the job table
* new asset popup
has protection for clicking jobs that are not in filter.
new job ifFiltered logic so it wont update if its out of view
* show situation on scene
* bad refresh bug
refresh was stuck in a loop updating every 30 seconds
* wrong icon
* restrict row drag to just the first cell
* fixed the double toggle load
* new jobs list layout
* Bug Fixes
Teams had no filters on their obvervables
jobs and assets had no disposal methods
teams had no JSON update function
jobs popup had no protection for non asset teams or team out of view
* team list has the new cards list. tasking status badges have consistent color
* job status filters logic and new UI in the menu
* safe defaults in panel layout
* New Ops Log Functionality
-Completed New Ops Log Functionality.
-Slight rework of how the NewOpsLogModalVM handles tags.
-Added error catching for missing required fields in New Ops Logs.
* Config share code + fix for fetching jobs without auth
* UX tweaks on share config
* hooked up real url
* slightly better filter handling
* bugfix - filter boxes were not applying
* bug fix: handler for config checkboxes had back click handling
caused the config to save before the values had been updated.
* Separated Create Ops Log & Create Radio Log
Moved RadioLog into it's own VM (CreateRadioLogModalVM) - also renamed NewOpsLogModalVM to CreateOpsLogModalVM.
* job timeline magic
* tidy up after merge
* new job details thingy
* popup smoothing. new range circles. style updates
* smooth the page load so that it fades in once the dom loads
* linter fix
* Daily
Added mini map
Added geoservices common code and map layer for unit boundaries
* better layer menu
kinda?
* map layers refactored out into their own files
* fixup of something that has nothing to do with this WIP
* Team Status Update Functionality
Team Status functionality added & tested. UI is still a bit janky but we'll come back to that.
* Got rid of defunct team status update button
* tag code
* bad import
* turn down the debug
* fixed stupid map zoom bug on unit boundary
* rewrite of the scroll into view stuff
* remove jobs and teams that are not in search results
* bug fixes
* error out if the remote tab command tries to open the same tab
* remote control on self bug
* warn when a job is removed if it has focus
* warning when things are filtered out
* added bootstrap alerts wrapper
* less debug
---------
Co-authored-by: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
---
src/pages/tasking/models/Team.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/pages/tasking/models/Team.js b/src/pages/tasking/models/Team.js
index 6ad080bb..2c064049 100644
--- a/src/pages/tasking/models/Team.js
+++ b/src/pages/tasking/models/Team.js
@@ -322,7 +322,6 @@ export function Team(data = {}, deps = {}) {
};
Team.prototype.updateFromJson = function (d = {}) {
- console.log("Updating team from json:", 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);
From 4e5098e9fbaefdce264d83a1ee24e41d7ad1f239 Mon Sep 17 00:00:00 2001
From: Tim Dykes
Date: Sat, 29 Nov 2025 11:58:55 +1100
Subject: [PATCH 16/61] Tdykes.tasking.wip (#235)
* boostrap upgrade broke team summary.
This restores bootstrap 4 and adds an alias for bootstrap 5
* first push - new tasking dashboard
* New features
* New features
* moved map to vm
made a new vm just for map
* refactored out the mapvm
* map popups have their own view models now
* starting to flesh out the popups UI
new UI for popups. refactor all the models
* Map with a capital M
wtf is mac doing with this
* Add fit bound button to job pop
* removed leaflet-responsive-popup
back to normal popups
* missed a bad import
* ffs and another
* redoing the setup screen. this one isnt horrible
unit search at the top, both results boxes below
* new config page with dropdown for HQ search
* All sorts of new stuff
New pulse marker for unclaimed
New icons
New refacactor of colours
New incident menu
* linter fix
* UX Overlays
Legend updates
Notification overlays
* linter fix
* small refactor
* only untask warn on active
* extension for opsLog for single entry
* ICEMS unack alerts and fixed legend
* layer draw prototype
* started making transport NSW api code
* lots of code cleanup
* team and job list sorting on column header press
* opslog filters. sticky row headers on jobs and teams
* draggable row fixes. focus on job button
* linter fix
* only deploy on dev branch
* correct filtering of job and team status. added image icon
* Core OpsLog Creation Functionality + New Radio Log UI (#219)
* NewOpsLogEntry Core + Instant Radio Team on Job
Created:
- BeaconClient for creating (POST) New OpsLogEntries.
- Foundation for creating OpsLogEntries via Modal.
- Radio Log button for Teams on Incidents (Taskings).
* Added Team Radio Log Button
Added:
- Team Radio Log (Job agnostic) button & functions.
- Added new status update button to Incidents -> Tasking & Teams -> Taskings (Task list icons)
- Updated Radio Log button to Microphone (inline with other Lighthouse Instant Radio Log icons)
- Started work on new OpsLogEntry code.
* Removing incorrectly duplicated data-bind
* unfucked the map for jobs
unfucked the map for jobs
* Draft New Ops Log Modal
* Create enum.js
* map in some enums correctly, refresh job tasking on expand
* clicking a job calls focus and expands in the job table
* new asset popup
has protection for clicking jobs that are not in filter.
new job ifFiltered logic so it wont update if its out of view
* show situation on scene
* bad refresh bug
refresh was stuck in a loop updating every 30 seconds
* wrong icon
* restrict row drag to just the first cell
* fixed the double toggle load
* new jobs list layout
* Bug Fixes
Teams had no filters on their obvervables
jobs and assets had no disposal methods
teams had no JSON update function
jobs popup had no protection for non asset teams or team out of view
* team list has the new cards list. tasking status badges have consistent color
* job status filters logic and new UI in the menu
* safe defaults in panel layout
* New Ops Log Functionality
-Completed New Ops Log Functionality.
-Slight rework of how the NewOpsLogModalVM handles tags.
-Added error catching for missing required fields in New Ops Logs.
* Config share code + fix for fetching jobs without auth
* UX tweaks on share config
* hooked up real url
* slightly better filter handling
* bugfix - filter boxes were not applying
* bug fix: handler for config checkboxes had back click handling
caused the config to save before the values had been updated.
* Separated Create Ops Log & Create Radio Log
Moved RadioLog into it's own VM (CreateRadioLogModalVM) - also renamed NewOpsLogModalVM to CreateOpsLogModalVM.
* job timeline magic
* tidy up after merge
* new job details thingy
* popup smoothing. new range circles. style updates
* smooth the page load so that it fades in once the dom loads
* linter fix
* Daily
Added mini map
Added geoservices common code and map layer for unit boundaries
* better layer menu
kinda?
* map layers refactored out into their own files
* fixup of something that has nothing to do with this WIP
* Team Status Update Functionality
Team Status functionality added & tested. UI is still a bit janky but we'll come back to that.
* Got rid of defunct team status update button
* tag code
* bad import
* turn down the debug
* fixed stupid map zoom bug on unit boundary
* rewrite of the scroll into view stuff
* remove jobs and teams that are not in search results
* bug fixes
* error out if the remote tab command tries to open the same tab
* remote control on self bug
* warn when a job is removed if it has focus
* warning when things are filtered out
* added bootstrap alerts wrapper
* less debug
* fixing the drop downs not appearing over the top of the dom overflow
* new body font
* Updates to Team Status Dropdown
- UI Cleanups
- Fixed issue with ETC/ETA not be nullified before API.
- Clicking button whilst open now closes.
* Fuck it
removed opslog
fixed asset bug
* linter fix
---------
Co-authored-by: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
---
src/pages/tasking/components/job_popup.js | 5 -
src/pages/tasking/main.js | 70 +++++----
src/pages/tasking/models/Job.js | 7 -
src/pages/tasking/viewmodels/OpsLogModalVM.js | 134 ------------------
.../tasking/viewmodels/SendSMSModalVM.js | 10 ++
.../viewmodels/UpdateTeamStatusDropdownVM.js | 88 +++++++++---
static/pages/tasking.html | 67 +++++----
styles/pages/tasking.css | 63 +++++++-
8 files changed, 205 insertions(+), 239 deletions(-)
create mode 100644 src/pages/tasking/viewmodels/SendSMSModalVM.js
diff --git a/src/pages/tasking/components/job_popup.js b/src/pages/tasking/components/job_popup.js
index ddb8a61c..1c292d1f 100644
--- a/src/pages/tasking/components/job_popup.js
+++ b/src/pages/tasking/components/job_popup.js
@@ -29,11 +29,6 @@ export function buildJobPopupKO() {
-
-
-
diff --git a/src/pages/tasking/main.js b/src/pages/tasking/main.js
index 542993f0..0330bb10 100644
--- a/src/pages/tasking/main.js
+++ b/src/pages/tasking/main.js
@@ -14,7 +14,6 @@ import { ResizeDividers } from './resize.js';
import { addOrUpdateJobMarker, removeJobMarker } from './markers/jobMarker.js';
import { attachAssetMarker, detachAssetMarker } from './markers/assetMarker.js';
import { MapVM } from './viewmodels/Map.js';
-import { OpsLogModalVM } from "./viewmodels/OpsLogModalVM.js";
import { JobTimeline } from "./viewmodels/JobTimeline.js";
@@ -315,7 +314,6 @@ function VM() {
self.allTags = ko.observableArray([]);
///opslog short cuts
- self.opsLogModalVM = new OpsLogModalVM(self);
self.CreateOpsLogModalVM = new CreateOpsLogModalVM(self);
self.CreateRadioLogModalVM = new CreateRadioLogModalVM(self);
self.selectedJob = ko.observable(null);
@@ -527,16 +525,26 @@ function VM() {
// No teams? Return nothing
if (!self.trackableAssets) return [];
-
-
return ko.utils.arrayFilter(self.trackableAssets() || [], a => {
const teams = ko.unwrap(a.matchingTeams);
if (!Array.isArray(teams) || teams.length === 0) return false;
+ const allowed = self.config.teamStatusFilter(); // allow-list
// Return true if at least one team's status() is not in ignoreList
return teams.some(t => {
- const status = ko.unwrap(t.status);
- return status && !self.config.teamStatusFilter().includes(status) && t.isFilteredIn();
+ const status = t.teamStatusType()?.Name;
+ const hqMatch = self.config.teamFilters().length === 0 || self.config.teamFilters().some((f) => f.id == t.assignedTo().id());
+ // If allow-list non-empty, only show teams whose status is in it
+ if (allowed.length > 0 && !allowed.includes(status)) {
+ return false;
+ }
+ //must match HQ filter
+ if (!hqMatch) {
+ return false;
+ }
+ if (t.isFilteredIn == null) return false;
+
+ return true;
});
});
}).extend({ trackArrayChanges: true, rateLimit: 50 });
@@ -562,10 +570,6 @@ function VM() {
}
},
- attachAndFillOpsLogModal: (job) => {
- self.attachOpsLogModal(job);
- },
-
openRadioLogModal: (tasking) => {
self.attachJobRadioLogModal(tasking);
},
@@ -636,15 +640,6 @@ function VM() {
return team;
};
- self.attachOpsLogModal = function (job) {
- const modalEl = document.getElementById('opsLogModal');
- const modal = new bootstrap.Modal(modalEl);
-
- self.selectedJob(job);
- self.opsLogModalVM.openForJob(job);
- modal.show();
- };
-
self.attachJobTimelineModal = function (job) {
const modalEl = document.getElementById('jobTimelineModal');
const modal = new bootstrap.Modal(modalEl);
@@ -742,7 +737,6 @@ function VM() {
self._refreshTeamTrackableAssets = function (team) {
if (!team || typeof team.trackableAssets !== 'function') return;
const list = team.trackableAssets();
-
(self.trackableAssets() || []).forEach(a => {
const has = list.find(x => x.id() === a.id())
const match = self._assetMatchesTeam(a, team);
@@ -891,7 +885,6 @@ function VM() {
} else if (ch.status === 'deleted') {
if (ch.value.expanded() || ch.value.popUpIsOpen()) {
showAlert("The team you were viewing has been refreshed and filtered out based on the current filters", "warning", 4000);
-
}
ch.value.isFilteredIn(false);
}
@@ -958,16 +951,6 @@ function VM() {
});
};
- self.attachAndFillOpsLogModal = async function (jobId, cb) {
- const t = await getToken(); // blocks here until token is ready
- BeaconClient.operationslog.search(jobId, apiHost, params.userId, t, function (data) {
- cb(data?.Results || []);
- }, function (err) {
- console.error("Failed to fetch ops log for job:", err);
- cb([]);
- });
- }
-
self.fetchOpsLogForJob = async function (jobId, cb) {
const t = await getToken(); // blocks here until token is ready
BeaconClient.operationslog.search(jobId, apiHost, params.userId, t, function (data) {
@@ -999,8 +982,9 @@ function VM() {
});
}
- self.updateTeamStatus = function (taskingId, status, payload, cb) {
- BeaconClient.tasking.updateTeamStatus(apiHost, taskingId, status, payload, token, function (data) {
+ self.updateTeamStatus = function (tasking, status, payload, cb) {
+ BeaconClient.tasking.updateTeamStatus(apiHost, tasking.id(), status, payload, token, function (data) {
+ tasking.job.fetchTasking();
cb(data || []);
}, function (err) {
console.error("Failed to update team status:", err);
@@ -1008,8 +992,9 @@ function VM() {
});
}
- self.callOffTeam = function (taskingId, payload, cb) {
- BeaconClient.tasking.callOffTeam(apiHost, taskingId, payload, token, function (data) {
+ self.callOffTeam = function (tasking, payload, cb) {
+ BeaconClient.tasking.callOffTeam(apiHost, tasking.id(), payload, token, function (data) {
+ tasking.job.fetchTasking();
cb(data || []);
}, function (err) {
console.error("Failed to call off team:", err);
@@ -1017,9 +1002,10 @@ function VM() {
});
}
- self.untaskTeam = function (taskingId, payload, cb) {
+ self.untaskTeam = function (tasking, payload, cb) {
const form = BeaconClient.toFormUrlEncoded(payload);
- BeaconClient.tasking.untaskTeam(apiHost, taskingId, form, token, function (data) {
+ BeaconClient.tasking.untaskTeam(apiHost, tasking.id(), form, token, function (data) {
+ tasking.job.fetchTasking();
cb(data);
}, function (err) {
console.error("Failed to create ops log entry:", err);
@@ -1280,6 +1266,16 @@ window.addEventListener('resize', () => map.invalidateSize());
document.addEventListener('DOMContentLoaded', function () {
+
+ $('.dropdown-menu').on('show.bs.dropdown', function () {
+ $('body').append($('.dropdown-menu').css({
+ position: 'absolute',
+ left: $('.dropdown-menu').offset().left,
+ top: $('.dropdown-menu').offset().top
+ }).detach());
+ });
+
+
//get tokens
BeaconToken.fetchBeaconTokenAndKeepReturningValidTokens(
apiHost,
diff --git a/src/pages/tasking/models/Job.js b/src/pages/tasking/models/Job.js
index 5cd75e05..dfc6bca4 100644
--- a/src/pages/tasking/models/Job.js
+++ b/src/pages/tasking/models/Job.js
@@ -18,7 +18,6 @@ export function Job(data = {}, deps = {}) {
fetchJobTasking = (_jobId, cb) => cb(null),
fetchJobById = (_jobId, cb) => cb(null),
flyToJob = (_job) => {/* noop */ },
- attachAndFillOpsLogModal = (_jobId) => ([]),
attachAndFillTimelineModal = (_job) => { /* noop */ },
fetchUnacknowledgedJobNotifications = async (_job) => ([]),
drawJobTargetRing = (_job) => { /* noop */ },
@@ -196,12 +195,6 @@ export function Job(data = {}, deps = {}) {
return fullType.replace(/Evacuation/i, 'Evac').trim();
})
-
- self.attachAndFillOpsLogModal = function () {
- console.log("Fetching ops log entries for job", self.id());
- attachAndFillOpsLogModal(self)
- }
-
self.attachAndFillTimelineModal = function () {
console.log("Fetching timeline entries for job", self.id());
attachAndFillTimelineModal(self)
diff --git a/src/pages/tasking/viewmodels/OpsLogModalVM.js b/src/pages/tasking/viewmodels/OpsLogModalVM.js
index 813fc383..e1e64228 100644
--- a/src/pages/tasking/viewmodels/OpsLogModalVM.js
+++ b/src/pages/tasking/viewmodels/OpsLogModalVM.js
@@ -1,139 +1,5 @@
/* eslint-disable @typescript-eslint/no-this-alias */
import ko from "knockout";
-import { OpsLogEntry } from "../models/OpsLogEntry.js";
-
-function TagFilterItem(label, owner) {
- const self = this;
- self.label = label;
- self.owner = owner;
- self.isIcems = (label === "ICEMS");
-
- self.isActive = ko.pureComputed(function () {
- return owner.selectedTags().indexOf(self.label) !== -1;
- });
-
- self.select = function () {
- var tags = owner.selectedTags().slice(0);
- var idx = tags.indexOf(self.label);
- if (idx === -1) {
- tags.push(self.label); // add
- } else {
- tags.splice(idx, 1); // remove
- }
- owner.selectedTags(tags);
- };
-
- self.btnClass = ko.pureComputed(function () {
- var base = "btn btn-xs me-1 mb-1 ";
- if (self.isIcems) {
- // ICEMS uses secondary colour
- return base + (self.isActive() ? "btn-info" : "btn-outline-info");
- }
- // Other tags use primary when active
- return base + (self.isActive() ? "btn-primary" : "btn-outline-secondary");
- });
-}
-
-export function OpsLogModalVM(parentVm) {
- // eslint-disable-next-line @typescript-eslint/no-this-alias
- const self = this;
-
- self.job = ko.observable(null);
- self.entries = ko.observableArray([]);
- self.loading = ko.observable(false);
- self.jobIdentifier = ko.observable();
-
- // --- TAG FILTER STATE (multi-select) ---
- self.selectedTags = ko.observableArray([]);
- self.tagItems = ko.observableArray([]);
-
- self.isTagFilterClear = ko.pureComputed(function () {
- return self.selectedTags().length === 0;
- });
-
- self.clearTagFilter = function () {
- self.selectedTags([]);
- };
-
- self.allTagButtonClass = ko.pureComputed(function () {
- return "btn btn-xs " + (self.isTagFilterClear() ? "btn-primary" : "btn-outline-secondary");
- });
- self.filteredEntries = ko.pureComputed(function () {
- var selected = self.selectedTags();
- if (!selected.length) {
- return self.entries(); // no filter → all entries
- }
-
- return self.entries().filter(function (entry) {
- var raw = entry.tagsCsv ? entry.tagsCsv() : "";
- if (!raw) {
- return false;
- }
-
- var tags = raw
- .split(",")
- .map(function (t) { return t.trim(); })
- .filter(function (t) { return t.length > 0; });
-
- // AND semantics: every selected tag must appear in entry.tags
- for (var i = 0; i < selected.length; i++) {
- if (tags.indexOf(selected[i]) === -1) {
- return false;
- }
- }
- return true;
- });
- });
-
- function rebuildTagItemsFromEntries() {
- var tagsSet = new Set();
-
- self.entries().forEach(function (entry) {
- var raw = entry.tagsCsv ? entry.tagsCsv() : "";
- if (!raw) {
- return;
- }
- raw
- .split(",")
- .map(function (t) { return t.trim(); })
- .filter(function (t) { return t.length > 0; })
- .forEach(function (t) { tagsSet.add(t); });
- });
-
- var items = Array.from(tagsSet)
- .sort(function (a, b) {
- // ICEMS always first
- if (a === "ICEMS" && b !== "ICEMS") return -1;
- if (b === "ICEMS" && a !== "ICEMS") return 1;
-
- return a.localeCompare(b); // normal alphabetical for the rest
- })
- .map(function (label) { return new TagFilterItem(label, self); });
-
- self.tagItems(items);
- }
-
- self.openForJob = async (job) => {
- self.jobIdentifier(job.identifier() || "");
- self.job(job);
- self.entries([]);
- self.selectedTags([]);
- self.tagItems([]);
- self.loading(true);
-
- try {
- const results = await new Promise((resolve, reject) => {
- parentVm.fetchOpsLogForJob(job.id(), function (res) { resolve(res); }, reject);
- });
- self.entries((results || []).map(function (e) { return new OpsLogEntry(e); }));
- rebuildTagItemsFromEntries();
- } catch (err) {
- console.error("Failed to fetch ops log entries:", err);
- } finally {
- self.loading(false);
- }
- };
-}
export function CreateOpsLogModalVM(parentVM) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
diff --git a/src/pages/tasking/viewmodels/SendSMSModalVM.js b/src/pages/tasking/viewmodels/SendSMSModalVM.js
new file mode 100644
index 00000000..eb07b709
--- /dev/null
+++ b/src/pages/tasking/viewmodels/SendSMSModalVM.js
@@ -0,0 +1,10 @@
+/* eslint-disable @typescript-eslint/no-this-alias */
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import ko from "knockout";
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export function CreateRadioLogModalVM(parentVM) {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias, @typescript-eslint/no-unused-vars
+ const self = this;
+
+};
\ No newline at end of file
diff --git a/src/pages/tasking/viewmodels/UpdateTeamStatusDropdownVM.js b/src/pages/tasking/viewmodels/UpdateTeamStatusDropdownVM.js
index ac49383b..ac8dee95 100644
--- a/src/pages/tasking/viewmodels/UpdateTeamStatusDropdownVM.js
+++ b/src/pages/tasking/viewmodels/UpdateTeamStatusDropdownVM.js
@@ -5,11 +5,12 @@ import moment from "moment";
export function UpdateTeamStatusDropdownVM(parentVM) {
const self = this;
+ self.openForTaskingId = ko.observable(null);
self.currentTasking = ko.observable(null);
self.isVisible = ko.observable(false);
self.top = ko.pureComputed(() => self.posY() + "px");
self.left = ko.pureComputed(() => self.posX() + "px");
- self.currentPage = ko.observable("selectStatus");
+ self.currentPage = ko.observable("closed");
self.currentStatus = ko.observable(null);
self.selectedStatus = ko.observable(null);
let scrollHandler = null;
@@ -109,27 +110,54 @@ export function UpdateTeamStatusDropdownVM(parentVM) {
// Open popup next to clicked button
self.openTeamStatusDropdown = function (tasking, anchorE1) {
+
+ if (self.openForTaskingId() === tasking.id()) {
+ if(self.currentPage() === "selectStatus" || self.currentPage() === "details") {
+ self.close();
+ return;
+ }
+ }
+
self.currentTasking(tasking);
+ self.openForTaskingId(tasking.id());
self.currentStatus(self.currentTasking().currentStatus());
+ self.currentPage("selectStatus");
const rect = anchorE1.getBoundingClientRect();
// Yes, this is very hacky code - need to come back and fix this - TODO
- const visibleCount =
- (self.canUntask() ? 1 : 0) +
- (self.canEnroute() ? 1 : 0) +
- (self.canOnsite() ? 1 : 0) +
- (self.canOffsite() ? 1 : 0) +
- (self.canUntask() ? 1 : 0) +
- (self.canCalloff() ? 1 : 0);
- const popupHeight = visibleCount * 45;
+ let popupHeight;
const spaceBelow = window.innerHeight - rect.bottom;
+ switch (self.currentStatus()) {
+ case "Tasked":
+ popupHeight = 228; //done
+ break;
+ case "Enroute":
+ popupHeight = 190; //done
+ break;
+ case "Onsite":
+ popupHeight = 155; //done
+ break;
+ case "Offsite":
+ popupHeight = 155; //done
+ break;
+ case "Untasked":
+ popupHeight = 45;
+ break;
+ case "Complete":
+ popupHeight = 45;
+ break;
+ case "CalledOff":
+ popupHeight = 45;
+ break;
+ }
+
let top;
if (spaceBelow >= popupHeight) {
top = rect.bottom; // Popup below the button - this is the preference if there is space to do it.
} else {
- top = rect.top - popupHeight - rect.height; // Popup above the button
+ top = rect.top - popupHeight; // Popup above the button
}
self.posX(rect.left);
@@ -210,13 +238,21 @@ export function UpdateTeamStatusDropdownVM(parentVM) {
// Construct Payload for En-Route
if (self.selectedStatus() === "En-Route") {
action = "Enroute";
- payload.estimatedCompletion = moment(self.eta()).format("YYYY-MM-DDTHH:mm:ssZ");
+ if (self.eta() && moment(self.eta()).isValid()) {
+ payload.estimatedCompletion = moment(self.eta()).format("YYYY-MM-DDTHH:mm:ssZ");
+ } else {
+ payload.estimatedCompletion = null;
+ }
}
// Construct Payload for On-Site
if (self.selectedStatus() === "On-Site") {
action = "Onsite";
- payload.estimatedCompletion = moment(self.etc()).format("YYYY-MM-DDTHH:mm:ssZ");
+ if (self.etc() && moment(self.etc()).isValid()) {
+ payload.estimatedCompletion = moment(self.etc()).format("YYYY-MM-DDTHH:mm:ssZ");
+ } else {
+ payload.estimatedCompletion = null;
+ }
}
// Construct Payload for Off-Site
@@ -267,10 +303,10 @@ export function UpdateTeamStatusDropdownVM(parentVM) {
}
// Send to ParentVM to make API Call
- // If it's a POST Request send it to the UpdateTeamStatus API - sends TaskingID, Action (what status we are changing to), Payload and Callback.
+ // If it's a POST Request send it to the UpdateTeamStatus API - sends Tasking, Action (what status we are changing to), Payload and Callback.
const UpdateTeamStatusAPI = ['Enroute','Onsite','Offsite']
if (UpdateTeamStatusAPI.includes(action)) {
- parentVM.updateTeamStatus(self.currentTasking().id(), action, payload, function (result) {
+ parentVM.updateTeamStatus(self.currentTasking(), action, payload, function (result) {
if (!result) {
console.error("Team Status Update failed");
@@ -279,9 +315,9 @@ export function UpdateTeamStatusDropdownVM(parentVM) {
});
}
- // If it's a PUT request to Call off we need to send it to the callOffTeam API - Sends TaskingID, Payload and Callback.
+ // If it's a PUT request to Call off we need to send it to the callOffTeam API - Sends Tasking, Payload and Callback.
if (action === "CallOff") {
- parentVM.callOffTeam(self.currentTasking().id(), payload, function (result) {
+ parentVM.callOffTeam(self.currentTasking(), payload, function (result) {
if (!result) {
console.error("Failed to CallOff Team");
@@ -290,9 +326,9 @@ export function UpdateTeamStatusDropdownVM(parentVM) {
});
}
- // If it's a DELETE request for Untask we need to send it to the untaskTeam API - sends TaskingID, Payload and Callback.
+ // If it's a DELETE request for Untask we need to send it to the untaskTeam API - sends Tasking, Payload and Callback.
if (action === "Untask") {
- parentVM.untaskTeam(self.currentTasking().id(), payload, function (result) {
+ parentVM.untaskTeam(self.currentTasking(), payload, function (result) {
if (!result) {
console.error("Failed to Untask Team");
@@ -315,10 +351,9 @@ export function UpdateTeamStatusDropdownVM(parentVM) {
// Close
self.close = function () {
- // ⭐ REPLACE isVisible(false) WITH hide()
self.hide();
detachOutsideClick();
- self.currentPage("selectStatus");
+ self.currentPage("closed");
self.selectedStatus(null);
self.time("");
self.eta("");
@@ -334,16 +369,23 @@ export function UpdateTeamStatusDropdownVM(parentVM) {
function attachOutsideClick() {
outsideHandler = (evt) => {
const dropdown = document.getElementById("TeamStatusDropdown");
- if (!dropdown.contains(evt.target)) {
+
+ // NEW: detect ANY of your team-status buttons
+ const isToggle = evt.target.closest(".team-status-button");
+
+ // Close only if click is outside both the dropdown AND the toggle button
+ if (!dropdown.contains(evt.target) && !isToggle) {
self.close();
}
};
- document.addEventListener("mousedown", outsideHandler);
+
+ // Use click, not mousedown
+ document.addEventListener("click", outsideHandler);
}
function detachOutsideClick() {
if (outsideHandler) {
- document.removeEventListener("mousedown", outsideHandler);
+ document.removeEventListener("click", outsideHandler);
outsideHandler = null;
}
}
diff --git a/static/pages/tasking.html b/static/pages/tasking.html
index db925c8e..79cc018b 100644
--- a/static/pages/tasking.html
+++ b/static/pages/tasking.html
@@ -16,20 +16,13 @@
diff --git a/styles/pages/tasking.css b/styles/pages/tasking.css
index 878a3299..2dc27fd0 100644
--- a/styles/pages/tasking.css
+++ b/styles/pages/tasking.css
@@ -12,9 +12,37 @@ html {
body {
height: 100%;
margin: 0;
- font-family: 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande',
- sans-serif;
- font-size: 13px;
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ font-size: 12px;
+ line-height: 1.35;
+}
+
+/* Ensure key UI elements follow the smaller base size */
+button,
+input,
+select,
+textarea,
+.table,
+.dropdown-menu,
+.input-group-text,
+.badge,
+.nav-link,
+.modal,
+.form-control {
+ font-family: inherit;
+ font-size: inherit;
+}
+
+/* Optional: slightly tighter tables (teams + jobs) */
+.teams,
+.jobs {
+ font-size: 12px; /* make tables a bit denser than rest */
+}
+
+/* Optional: make "small" really small for helper text */
+.small,
+.text-muted {
+ font-size: 11px;
}
td.chev[draggable="true"] {
@@ -1276,12 +1304,25 @@ tr.job:hover {
}
.team-status-button {
- border: none;
- padding: 4px 8px;
+ border: 1px solid #ccc;
+ background: #fff;
+ padding: 4px 24px 4px 8px; /* space for icon on the right */
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
line-height: 1.2;
+ position: relative;
+}
+
+.team-status-button::after {
+ content: "▾"; /* small dropdown arrow */
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 0.7rem;
+ pointer-events: none;
+ opacity: 0.7;
}
#TeamStatusDropdown {
@@ -1412,4 +1453,14 @@ tr.job:hover {
color: #6e663c;
background-color: #ffffff;
border-color: #6e663c
-}
\ No newline at end of file
+}
+
+/* Ensure team/job dropdown menus appear above pane overflow */
+.sidebar .dropdown-menu,
+.pane .dropdown-menu,
+.table-responsive .dropdown-menu {
+ position: fixed !important; /* escape overflow containers */
+ z-index: 3000 !important; /* above map, table scroll, etc. */
+ transform: none !important; /* disable BS placement transforms */
+ inset: auto !important; /* let JS set the coords */
+}
From b0cf70f13deffcb66039ca79e971a1746e72df6e Mon Sep 17 00:00:00 2001
From: Tim Dykes
Date: Sat, 29 Nov 2025 12:09:12 +1100
Subject: [PATCH 17/61] Tdykes.tasking.wip (#236)
* boostrap upgrade broke team summary.
This restores bootstrap 4 and adds an alias for bootstrap 5
* first push - new tasking dashboard
* New features
* New features
* moved map to vm
made a new vm just for map
* refactored out the mapvm
* map popups have their own view models now
* starting to flesh out the popups UI
new UI for popups. refactor all the models
* Map with a capital M
wtf is mac doing with this
* Add fit bound button to job pop
* removed leaflet-responsive-popup
back to normal popups
* missed a bad import
* ffs and another
* redoing the setup screen. this one isnt horrible
unit search at the top, both results boxes below
* new config page with dropdown for HQ search
* All sorts of new stuff
New pulse marker for unclaimed
New icons
New refacactor of colours
New incident menu
* linter fix
* UX Overlays
Legend updates
Notification overlays
* linter fix
* small refactor
* only untask warn on active
* extension for opsLog for single entry
* ICEMS unack alerts and fixed legend
* layer draw prototype
* started making transport NSW api code
* lots of code cleanup
* team and job list sorting on column header press
* opslog filters. sticky row headers on jobs and teams
* draggable row fixes. focus on job button
* linter fix
* only deploy on dev branch
* correct filtering of job and team status. added image icon
* Core OpsLog Creation Functionality + New Radio Log UI (#219)
* NewOpsLogEntry Core + Instant Radio Team on Job
Created:
- BeaconClient for creating (POST) New OpsLogEntries.
- Foundation for creating OpsLogEntries via Modal.
- Radio Log button for Teams on Incidents (Taskings).
* Added Team Radio Log Button
Added:
- Team Radio Log (Job agnostic) button & functions.
- Added new status update button to Incidents -> Tasking & Teams -> Taskings (Task list icons)
- Updated Radio Log button to Microphone (inline with other Lighthouse Instant Radio Log icons)
- Started work on new OpsLogEntry code.
* Removing incorrectly duplicated data-bind
* unfucked the map for jobs
unfucked the map for jobs
* Draft New Ops Log Modal
* Create enum.js
* map in some enums correctly, refresh job tasking on expand
* clicking a job calls focus and expands in the job table
* new asset popup
has protection for clicking jobs that are not in filter.
new job ifFiltered logic so it wont update if its out of view
* show situation on scene
* bad refresh bug
refresh was stuck in a loop updating every 30 seconds
* wrong icon
* restrict row drag to just the first cell
* fixed the double toggle load
* new jobs list layout
* Bug Fixes
Teams had no filters on their obvervables
jobs and assets had no disposal methods
teams had no JSON update function
jobs popup had no protection for non asset teams or team out of view
* team list has the new cards list. tasking status badges have consistent color
* job status filters logic and new UI in the menu
* safe defaults in panel layout
* New Ops Log Functionality
-Completed New Ops Log Functionality.
-Slight rework of how the NewOpsLogModalVM handles tags.
-Added error catching for missing required fields in New Ops Logs.
* Config share code + fix for fetching jobs without auth
* UX tweaks on share config
* hooked up real url
* slightly better filter handling
* bugfix - filter boxes were not applying
* bug fix: handler for config checkboxes had back click handling
caused the config to save before the values had been updated.
* Separated Create Ops Log & Create Radio Log
Moved RadioLog into it's own VM (CreateRadioLogModalVM) - also renamed NewOpsLogModalVM to CreateOpsLogModalVM.
* job timeline magic
* tidy up after merge
* new job details thingy
* popup smoothing. new range circles. style updates
* smooth the page load so that it fades in once the dom loads
* linter fix
* Daily
Added mini map
Added geoservices common code and map layer for unit boundaries
* better layer menu
kinda?
* map layers refactored out into their own files
* fixup of something that has nothing to do with this WIP
* Team Status Update Functionality
Team Status functionality added & tested. UI is still a bit janky but we'll come back to that.
* Got rid of defunct team status update button
* tag code
* bad import
* turn down the debug
* fixed stupid map zoom bug on unit boundary
* rewrite of the scroll into view stuff
* remove jobs and teams that are not in search results
* bug fixes
* error out if the remote tab command tries to open the same tab
* remote control on self bug
* warn when a job is removed if it has focus
* warning when things are filtered out
* added bootstrap alerts wrapper
* less debug
* fixing the drop downs not appearing over the top of the dom overflow
* new body font
* Updates to Team Status Dropdown
- UI Cleanups
- Fixed issue with ETC/ETA not be nullified before API.
- Clicking button whilst open now closes.
* Fuck it
removed opslog
fixed asset bug
* linter fix
---------
Co-authored-by: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
From 64f0cfdd0460f7e37993d12f62be22787b44b45b Mon Sep 17 00:00:00 2001
From: Tim Dykes
Date: Sun, 30 Nov 2025 21:02:17 +1100
Subject: [PATCH 18/61] Tdykes.tasking.wip (#237)
* boostrap upgrade broke team summary.
This restores bootstrap 4 and adds an alias for bootstrap 5
* first push - new tasking dashboard
* New features
* New features
* moved map to vm
made a new vm just for map
* refactored out the mapvm
* map popups have their own view models now
* starting to flesh out the popups UI
new UI for popups. refactor all the models
* Map with a capital M
wtf is mac doing with this
* Add fit bound button to job pop
* removed leaflet-responsive-popup
back to normal popups
* missed a bad import
* ffs and another
* redoing the setup screen. this one isnt horrible
unit search at the top, both results boxes below
* new config page with dropdown for HQ search
* All sorts of new stuff
New pulse marker for unclaimed
New icons
New refacactor of colours
New incident menu
* linter fix
* UX Overlays
Legend updates
Notification overlays
* linter fix
* small refactor
* only untask warn on active
* extension for opsLog for single entry
* ICEMS unack alerts and fixed legend
* layer draw prototype
* started making transport NSW api code
* lots of code cleanup
* team and job list sorting on column header press
* opslog filters. sticky row headers on jobs and teams
* draggable row fixes. focus on job button
* linter fix
* only deploy on dev branch
* correct filtering of job and team status. added image icon
* Core OpsLog Creation Functionality + New Radio Log UI (#219)
* NewOpsLogEntry Core + Instant Radio Team on Job
Created:
- BeaconClient for creating (POST) New OpsLogEntries.
- Foundation for creating OpsLogEntries via Modal.
- Radio Log button for Teams on Incidents (Taskings).
* Added Team Radio Log Button
Added:
- Team Radio Log (Job agnostic) button & functions.
- Added new status update button to Incidents -> Tasking & Teams -> Taskings (Task list icons)
- Updated Radio Log button to Microphone (inline with other Lighthouse Instant Radio Log icons)
- Started work on new OpsLogEntry code.
* Removing incorrectly duplicated data-bind
* unfucked the map for jobs
unfucked the map for jobs
* Draft New Ops Log Modal
* Create enum.js
* map in some enums correctly, refresh job tasking on expand
* clicking a job calls focus and expands in the job table
* new asset popup
has protection for clicking jobs that are not in filter.
new job ifFiltered logic so it wont update if its out of view
* show situation on scene
* bad refresh bug
refresh was stuck in a loop updating every 30 seconds
* wrong icon
* restrict row drag to just the first cell
* fixed the double toggle load
* new jobs list layout
* Bug Fixes
Teams had no filters on their obvervables
jobs and assets had no disposal methods
teams had no JSON update function
jobs popup had no protection for non asset teams or team out of view
* team list has the new cards list. tasking status badges have consistent color
* job status filters logic and new UI in the menu
* safe defaults in panel layout
* New Ops Log Functionality
-Completed New Ops Log Functionality.
-Slight rework of how the NewOpsLogModalVM handles tags.
-Added error catching for missing required fields in New Ops Logs.
* Config share code + fix for fetching jobs without auth
* UX tweaks on share config
* hooked up real url
* slightly better filter handling
* bugfix - filter boxes were not applying
* bug fix: handler for config checkboxes had back click handling
caused the config to save before the values had been updated.
* Separated Create Ops Log & Create Radio Log
Moved RadioLog into it's own VM (CreateRadioLogModalVM) - also renamed NewOpsLogModalVM to CreateOpsLogModalVM.
* job timeline magic
* tidy up after merge
* new job details thingy
* popup smoothing. new range circles. style updates
* smooth the page load so that it fades in once the dom loads
* linter fix
* Daily
Added mini map
Added geoservices common code and map layer for unit boundaries
* better layer menu
kinda?
* map layers refactored out into their own files
* fixup of something that has nothing to do with this WIP
* Team Status Update Functionality
Team Status functionality added & tested. UI is still a bit janky but we'll come back to that.
* Got rid of defunct team status update button
* tag code
* bad import
* turn down the debug
* fixed stupid map zoom bug on unit boundary
* rewrite of the scroll into view stuff
* remove jobs and teams that are not in search results
* bug fixes
* error out if the remote tab command tries to open the same tab
* remote control on self bug
* warn when a job is removed if it has focus
* warning when things are filtered out
* added bootstrap alerts wrapper
* less debug
* fixing the drop downs not appearing over the top of the dom overflow
* new body font
* Updates to Team Status Dropdown
- UI Cleanups
- Fixed issue with ETC/ETA not be nullified before API.
- Clicking button whilst open now closes.
* Fuck it
removed opslog
fixed asset bug
* linter fix
* dumb self calling function
* unsafe unwrap
* swap the order of config items around to be logical
* Tags on opslog messages now using the factory
* more tags in places using tag factory
* minor css and style tweaks for tags
* team row correctly disables button when team doesnt exist any more
* row hover code
* rewrite of popups css. added InstantTask VM.
---------
Co-authored-by: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
---
src/pages/tasking/components/asset_popup.js | 8 -
src/pages/tasking/components/job_popup.js | 80 +++++---
src/pages/tasking/main.js | 31 +--
src/pages/tasking/models/Job.js | 24 ++-
src/pages/tasking/models/Tag.js | 6 +-
src/pages/tasking/models/Team.js | 3 +-
src/pages/tasking/utils/tagFactory.js | 28 ++-
src/pages/tasking/viewmodels/InstantTask.js | 169 +++++++++++++++
src/pages/tasking/viewmodels/JobPopUp.js | 217 +-------------------
src/pages/tasking/viewmodels/Map.js | 96 +++++++--
static/pages/tasking.html | 209 +++++++++++++------
styles/pages/tasking.css | 45 ++--
12 files changed, 529 insertions(+), 387 deletions(-)
create mode 100644 src/pages/tasking/viewmodels/InstantTask.js
diff --git a/src/pages/tasking/components/asset_popup.js b/src/pages/tasking/components/asset_popup.js
index 6e68535e..025e9063 100644
--- a/src/pages/tasking/components/asset_popup.js
+++ b/src/pages/tasking/components/asset_popup.js
@@ -114,14 +114,6 @@ export function buildAssetPopupKO() {
clickBubble:false">
-
-
-
-
diff --git a/styles/pages/tasking.css b/styles/pages/tasking.css
index 69b644a6..75e26d30 100644
--- a/styles/pages/tasking.css
+++ b/styles/pages/tasking.css
@@ -1165,12 +1165,12 @@ tr.job:hover {
/* match thead/colgroup: 2,10,8,8,8,28,8,22 % */
grid-template-columns:
2% /* chev */
- 8% /* Id */
+ 7% /* Id */
12% /* Received */
8% /* HQ */
8% /* Type */
32% /* Situation */
- 8% /* Status */
+ 9% /* Status */
22%; /* Address */
align-items: center;
column-gap: 0; /* let colgroup control spacing */
From 576c777f98416be47d87d49b59db173ad01a192b Mon Sep 17 00:00:00 2001
From: Tim Dykes
Date: Tue, 9 Dec 2025 22:28:55 +1100
Subject: [PATCH 32/61] Tdykes.tasking.wip (#251)
* boostrap upgrade broke team summary.
This restores bootstrap 4 and adds an alias for bootstrap 5
* first push - new tasking dashboard
* New features
* New features
* moved map to vm
made a new vm just for map
* refactored out the mapvm
* map popups have their own view models now
* starting to flesh out the popups UI
new UI for popups. refactor all the models
* Map with a capital M
wtf is mac doing with this
* Add fit bound button to job pop
* removed leaflet-responsive-popup
back to normal popups
* missed a bad import
* ffs and another
* redoing the setup screen. this one isnt horrible
unit search at the top, both results boxes below
* new config page with dropdown for HQ search
* All sorts of new stuff
New pulse marker for unclaimed
New icons
New refacactor of colours
New incident menu
* linter fix
* UX Overlays
Legend updates
Notification overlays
* linter fix
* small refactor
* only untask warn on active
* extension for opsLog for single entry
* ICEMS unack alerts and fixed legend
* layer draw prototype
* started making transport NSW api code
* lots of code cleanup
* team and job list sorting on column header press
* opslog filters. sticky row headers on jobs and teams
* draggable row fixes. focus on job button
* linter fix
* only deploy on dev branch
* correct filtering of job and team status. added image icon
* Core OpsLog Creation Functionality + New Radio Log UI (#219)
* NewOpsLogEntry Core + Instant Radio Team on Job
Created:
- BeaconClient for creating (POST) New OpsLogEntries.
- Foundation for creating OpsLogEntries via Modal.
- Radio Log button for Teams on Incidents (Taskings).
* Added Team Radio Log Button
Added:
- Team Radio Log (Job agnostic) button & functions.
- Added new status update button to Incidents -> Tasking & Teams -> Taskings (Task list icons)
- Updated Radio Log button to Microphone (inline with other Lighthouse Instant Radio Log icons)
- Started work on new OpsLogEntry code.
* Removing incorrectly duplicated data-bind
* unfucked the map for jobs
unfucked the map for jobs
* Draft New Ops Log Modal
* Create enum.js
* map in some enums correctly, refresh job tasking on expand
* clicking a job calls focus and expands in the job table
* new asset popup
has protection for clicking jobs that are not in filter.
new job ifFiltered logic so it wont update if its out of view
* show situation on scene
* bad refresh bug
refresh was stuck in a loop updating every 30 seconds
* wrong icon
* restrict row drag to just the first cell
* fixed the double toggle load
* new jobs list layout
* Bug Fixes
Teams had no filters on their obvervables
jobs and assets had no disposal methods
teams had no JSON update function
jobs popup had no protection for non asset teams or team out of view
* team list has the new cards list. tasking status badges have consistent color
* job status filters logic and new UI in the menu
* safe defaults in panel layout
* New Ops Log Functionality
-Completed New Ops Log Functionality.
-Slight rework of how the NewOpsLogModalVM handles tags.
-Added error catching for missing required fields in New Ops Logs.
* Config share code + fix for fetching jobs without auth
* UX tweaks on share config
* hooked up real url
* slightly better filter handling
* bugfix - filter boxes were not applying
* bug fix: handler for config checkboxes had back click handling
caused the config to save before the values had been updated.
* Separated Create Ops Log & Create Radio Log
Moved RadioLog into it's own VM (CreateRadioLogModalVM) - also renamed NewOpsLogModalVM to CreateOpsLogModalVM.
* job timeline magic
* tidy up after merge
* new job details thingy
* popup smoothing. new range circles. style updates
* smooth the page load so that it fades in once the dom loads
* linter fix
* Daily
Added mini map
Added geoservices common code and map layer for unit boundaries
* better layer menu
kinda?
* map layers refactored out into their own files
* fixup of something that has nothing to do with this WIP
* Team Status Update Functionality
Team Status functionality added & tested. UI is still a bit janky but we'll come back to that.
* Got rid of defunct team status update button
* tag code
* bad import
* turn down the debug
* fixed stupid map zoom bug on unit boundary
* rewrite of the scroll into view stuff
* remove jobs and teams that are not in search results
* bug fixes
* error out if the remote tab command tries to open the same tab
* remote control on self bug
* warn when a job is removed if it has focus
* warning when things are filtered out
* added bootstrap alerts wrapper
* less debug
* fixing the drop downs not appearing over the top of the dom overflow
* new body font
* Updates to Team Status Dropdown
- UI Cleanups
- Fixed issue with ETC/ETA not be nullified before API.
- Clicking button whilst open now closes.
* Fuck it
removed opslog
fixed asset bug
* linter fix
* dumb self calling function
* unsafe unwrap
* swap the order of config items around to be logical
* Tags on opslog messages now using the factory
* more tags in places using tag factory
* minor css and style tweaks for tags
* team row correctly disables button when team doesnt exist any more
* row hover code
* rewrite of popups css. added InstantTask VM.
* legend has asset icons now
* Added support for SIX maps layers
* sector filter support
* can assign and unassign sectors now
* GEO map work for new layers
* FRAO layer
* linter fix
* new layer menu
* show on jobs if theres outstanding action items. collapsable alerts. task count next to status. team row colors
* green border in train. custom sort rolled back out
* train beacon banner
* Stuff
* obnoxious training banner
* minor bug fix and new wording
* new SMS code. bugfix to stop tasking updating active teams
* linter and doco
* Lint fix ffs
* fixup on how we display team tasking counts.
fixup on how we display team tasking counts. fix on not showing assigned HQ
* stop click bubbling on disabled buttons in the left UI
* prevent message send while loading receps
* pass operational correctly
* reset defalts
* less in your face
* dont collapse teams clicking in the DIV
---------
Co-authored-by: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
---
static/pages/tasking.html | 4 ++--
styles/pages/tasking.css | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/static/pages/tasking.html b/static/pages/tasking.html
index 3015154e..024cea90 100644
--- a/static/pages/tasking.html
+++ b/static/pages/tasking.html
@@ -247,11 +247,11 @@
attr:{'aria-expanded': t.expanded}, attr: { 'data-team-id': t.id }">
Recipients
diff --git a/styles/pages/tasking.css b/styles/pages/tasking.css
index 6559a427..ddda7fb0 100644
--- a/styles/pages/tasking.css
+++ b/styles/pages/tasking.css
@@ -1744,7 +1744,8 @@ tr.job:hover {
}
#SendSMSModal .sms-recipient-list {
- max-height: 140px;
+ max-height: 150;
+ min-height: 150px;
overflow-y: auto;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
@@ -1754,3 +1755,33 @@ tr.job:hover {
#SendSMSModal .form-check-sm {
font-size: 0.85rem;
}
+
+#smsRecipientSearchDropdown.dropdown-menu {
+ max-height: 220px;
+ overflow: auto;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1055; /* above modal body contents */
+ transform: none !important;
+ margin-top: 2px;
+}
+/* They wont fuck off. im putting all the browsers in and something will work */
+/* Clears the 'X' button in search inputs for Chrome, Safari, and newer browsers */
+input[type="search"]::-webkit-search-decoration,
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-results-button,
+input[type="search"]::-webkit-search-results-decoration {
+ display: none;
+ /* Additional styles to ensure no space is taken */
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+/* Clears the 'X' button in text or search inputs for Internet Explorer 10+ */
+input[type="text"]::-ms-clear,
+input[type="search"]::-ms-clear {
+ display: none;
+ width: 0;
+ height: 0;
+}
\ No newline at end of file
From d45390bc4e1afd510f3e5302600982c956b50ceb Mon Sep 17 00:00:00 2001
From: Tim Dykes
Date: Mon, 15 Dec 2025 15:12:02 +1100
Subject: [PATCH 34/61] Tdykes.tasking.wip (#253)
* boostrap upgrade broke team summary.
This restores bootstrap 4 and adds an alias for bootstrap 5
* first push - new tasking dashboard
* New features
* New features
* moved map to vm
made a new vm just for map
* refactored out the mapvm
* map popups have their own view models now
* starting to flesh out the popups UI
new UI for popups. refactor all the models
* Map with a capital M
wtf is mac doing with this
* Add fit bound button to job pop
* removed leaflet-responsive-popup
back to normal popups
* missed a bad import
* ffs and another
* redoing the setup screen. this one isnt horrible
unit search at the top, both results boxes below
* new config page with dropdown for HQ search
* All sorts of new stuff
New pulse marker for unclaimed
New icons
New refacactor of colours
New incident menu
* linter fix
* UX Overlays
Legend updates
Notification overlays
* linter fix
* small refactor
* only untask warn on active
* extension for opsLog for single entry
* ICEMS unack alerts and fixed legend
* layer draw prototype
* started making transport NSW api code
* lots of code cleanup
* team and job list sorting on column header press
* opslog filters. sticky row headers on jobs and teams
* draggable row fixes. focus on job button
* linter fix
* only deploy on dev branch
* correct filtering of job and team status. added image icon
* Core OpsLog Creation Functionality + New Radio Log UI (#219)
* NewOpsLogEntry Core + Instant Radio Team on Job
Created:
- BeaconClient for creating (POST) New OpsLogEntries.
- Foundation for creating OpsLogEntries via Modal.
- Radio Log button for Teams on Incidents (Taskings).
* Added Team Radio Log Button
Added:
- Team Radio Log (Job agnostic) button & functions.
- Added new status update button to Incidents -> Tasking & Teams -> Taskings (Task list icons)
- Updated Radio Log button to Microphone (inline with other Lighthouse Instant Radio Log icons)
- Started work on new OpsLogEntry code.
* Removing incorrectly duplicated data-bind
* unfucked the map for jobs
unfucked the map for jobs
* Draft New Ops Log Modal
* Create enum.js
* map in some enums correctly, refresh job tasking on expand
* clicking a job calls focus and expands in the job table
* new asset popup
has protection for clicking jobs that are not in filter.
new job ifFiltered logic so it wont update if its out of view
* show situation on scene
* bad refresh bug
refresh was stuck in a loop updating every 30 seconds
* wrong icon
* restrict row drag to just the first cell
* fixed the double toggle load
* new jobs list layout
* Bug Fixes
Teams had no filters on their obvervables
jobs and assets had no disposal methods
teams had no JSON update function
jobs popup had no protection for non asset teams or team out of view
* team list has the new cards list. tasking status badges have consistent color
* job status filters logic and new UI in the menu
* safe defaults in panel layout
* New Ops Log Functionality
-Completed New Ops Log Functionality.
-Slight rework of how the NewOpsLogModalVM handles tags.
-Added error catching for missing required fields in New Ops Logs.
* Config share code + fix for fetching jobs without auth
* UX tweaks on share config
* hooked up real url
* slightly better filter handling
* bugfix - filter boxes were not applying
* bug fix: handler for config checkboxes had back click handling
caused the config to save before the values had been updated.
* Separated Create Ops Log & Create Radio Log
Moved RadioLog into it's own VM (CreateRadioLogModalVM) - also renamed NewOpsLogModalVM to CreateOpsLogModalVM.
* job timeline magic
* tidy up after merge
* new job details thingy
* popup smoothing. new range circles. style updates
* smooth the page load so that it fades in once the dom loads
* linter fix
* Daily
Added mini map
Added geoservices common code and map layer for unit boundaries
* better layer menu
kinda?
* map layers refactored out into their own files
* fixup of something that has nothing to do with this WIP
* Team Status Update Functionality
Team Status functionality added & tested. UI is still a bit janky but we'll come back to that.
* Got rid of defunct team status update button
* tag code
* bad import
* turn down the debug
* fixed stupid map zoom bug on unit boundary
* rewrite of the scroll into view stuff
* remove jobs and teams that are not in search results
* bug fixes
* error out if the remote tab command tries to open the same tab
* remote control on self bug
* warn when a job is removed if it has focus
* warning when things are filtered out
* added bootstrap alerts wrapper
* less debug
* fixing the drop downs not appearing over the top of the dom overflow
* new body font
* Updates to Team Status Dropdown
- UI Cleanups
- Fixed issue with ETC/ETA not be nullified before API.
- Clicking button whilst open now closes.
* Fuck it
removed opslog
fixed asset bug
* linter fix
* dumb self calling function
* unsafe unwrap
* swap the order of config items around to be logical
* Tags on opslog messages now using the factory
* more tags in places using tag factory
* minor css and style tweaks for tags
* team row correctly disables button when team doesnt exist any more
* row hover code
* rewrite of popups css. added InstantTask VM.
* legend has asset icons now
* Added support for SIX maps layers
* sector filter support
* can assign and unassign sectors now
* GEO map work for new layers
* FRAO layer
* linter fix
* new layer menu
* show on jobs if theres outstanding action items. collapsable alerts. task count next to status. team row colors
* green border in train. custom sort rolled back out
* train beacon banner
* Stuff
* obnoxious training banner
* minor bug fix and new wording
* new SMS code. bugfix to stop tasking updating active teams
* linter and doco
* Lint fix ffs
* fixup on how we display team tasking counts.
fixup on how we display team tasking counts. fix on not showing assigned HQ
* stop click bubbling on disabled buttons in the left UI
* prevent message send while loading receps
* pass operational correctly
* reset defalts
* less in your face
* dont collapse teams clicking in the DIV
* cloudfront infront of FRAO query
* job filter fix. added contact add to sms
* check status date on active team filters
* add contact to message fixups
* UI fixups for searching
* collapse side menu UI
* cycle through matching assets if theres multiple for a team
* fixup button in asset pop to standard icon
* lots of small visual changes.
* code cleanup for team marker focus
* duplicate css
---------
Co-authored-by: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
---
src/pages/tasking/components/asset_icon.js | 6 +-
src/pages/tasking/components/asset_popup.js | 2 +-
src/pages/tasking/main.js | 101 +++++++++++++++--
src/pages/tasking/mapLayers/geoservices.js | 5 +
src/pages/tasking/markers/assetMarker.js | 113 ++++++++++++++------
src/pages/tasking/markers/jobMarker.js | 1 +
src/pages/tasking/models/Job.js | 4 -
src/pages/tasking/models/Team.js | 63 +++++++----
src/pages/tasking/viewmodels/Map.js | 45 +++++---
static/pages/tasking.html | 5 +-
styles/pages/tasking.css | 38 +++----
11 files changed, 278 insertions(+), 105 deletions(-)
diff --git a/src/pages/tasking/components/asset_icon.js b/src/pages/tasking/components/asset_icon.js
index 183680be..728c92a0 100644
--- a/src/pages/tasking/components/asset_icon.js
+++ b/src/pages/tasking/components/asset_icon.js
@@ -1,7 +1,7 @@
import L from 'leaflet';
import moment from 'moment';
-export function buildIcon(asset) {
+export function buildIcon(asset, matchStatus) {
const capabilityColors = {
'Bus': '#FFD600',
'Command': '#1565C0',
@@ -26,9 +26,11 @@ export function buildIcon(asset) {
} catch { return ''; }
})();
+ const matchTextColor = matchStatus === 'unmatched' ? 'grey' : 'white';
+
const html =
`
-
into a grid too */
+.jobs > thead > tr{
+ display: grid;
+ grid-template-columns: var(--job-cols);
+ align-items: center;
+}
+
+/* header cells behave like blocks inside the grid */
+.jobs > thead > th{
+ position: sticky;
+ display: block;
+ box-sizing: border-box;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-right: 4px;
+}
+
.job-card--expanded .job-summary {
background: #eef4ff;
}
@@ -1740,8 +1793,8 @@ tr.job:hover {
}
#SendSMSModal .sms-recipient-list {
- max-height: 150;
- min-height: 150px;
+ max-height: 140px;
+ min-height: 100px;
overflow-y: auto;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
@@ -1780,4 +1833,39 @@ input[type="search"]::-ms-clear {
display: none;
width: 0;
height: 0;
-}
\ No newline at end of file
+}
+
+
+.teams > thead,
+.jobs > thead {
+ display: block;
+ position: sticky;
+ top: 0;
+ z-index: 1050; /* above cards/rows */
+ background: var(--bs-light, #f8f9fa);
+}
+
+/* Keep your existing grid header row definitions */
+.teams > thead > tr,
+.jobs > thead > tr {
+ display: grid;
+}
+
+/* Header cells should just be normal blocks now */
+.teams > thead > th,
+.jobs > thead > th {
+ position: static !important; /* override your current sticky-on-th */
+ display: block;
+ background: transparent;
+}
+
+
+
+.team-details fieldset {
+ height: 100%;
+ border: 1px solid #e5e5e5;
+ border-radius: 4px;
+ padding: 0.5rem 0.75rem 0.25rem;
+ margin-bottom: 0.75rem;
+ background-color: #ffffff;
+}
From 6017a3063ee2a6f935f5aa2b2613dd6fa3c64435 Mon Sep 17 00:00:00 2001
From: Tim Dykes
Date: Wed, 17 Dec 2025 00:42:20 +1100
Subject: [PATCH 36/61] Tdykes.tasking.wip (#255)
* boostrap upgrade broke team summary.
This restores bootstrap 4 and adds an alias for bootstrap 5
* first push - new tasking dashboard
* New features
* New features
* moved map to vm
made a new vm just for map
* refactored out the mapvm
* map popups have their own view models now
* starting to flesh out the popups UI
new UI for popups. refactor all the models
* Map with a capital M
wtf is mac doing with this
* Add fit bound button to job pop
* removed leaflet-responsive-popup
back to normal popups
* missed a bad import
* ffs and another
* redoing the setup screen. this one isnt horrible
unit search at the top, both results boxes below
* new config page with dropdown for HQ search
* All sorts of new stuff
New pulse marker for unclaimed
New icons
New refacactor of colours
New incident menu
* linter fix
* UX Overlays
Legend updates
Notification overlays
* linter fix
* small refactor
* only untask warn on active
* extension for opsLog for single entry
* ICEMS unack alerts and fixed legend
* layer draw prototype
* started making transport NSW api code
* lots of code cleanup
* team and job list sorting on column header press
* opslog filters. sticky row headers on jobs and teams
* draggable row fixes. focus on job button
* linter fix
* only deploy on dev branch
* correct filtering of job and team status. added image icon
* Core OpsLog Creation Functionality + New Radio Log UI (#219)
* NewOpsLogEntry Core + Instant Radio Team on Job
Created:
- BeaconClient for creating (POST) New OpsLogEntries.
- Foundation for creating OpsLogEntries via Modal.
- Radio Log button for Teams on Incidents (Taskings).
* Added Team Radio Log Button
Added:
- Team Radio Log (Job agnostic) button & functions.
- Added new status update button to Incidents -> Tasking & Teams -> Taskings (Task list icons)
- Updated Radio Log button to Microphone (inline with other Lighthouse Instant Radio Log icons)
- Started work on new OpsLogEntry code.
* Removing incorrectly duplicated data-bind
* unfucked the map for jobs
unfucked the map for jobs
* Draft New Ops Log Modal
* Create enum.js
* map in some enums correctly, refresh job tasking on expand
* clicking a job calls focus and expands in the job table
* new asset popup
has protection for clicking jobs that are not in filter.
new job ifFiltered logic so it wont update if its out of view
* show situation on scene
* bad refresh bug
refresh was stuck in a loop updating every 30 seconds
* wrong icon
* restrict row drag to just the first cell
* fixed the double toggle load
* new jobs list layout
* Bug Fixes
Teams had no filters on their obvervables
jobs and assets had no disposal methods
teams had no JSON update function
jobs popup had no protection for non asset teams or team out of view
* team list has the new cards list. tasking status badges have consistent color
* job status filters logic and new UI in the menu
* safe defaults in panel layout
* New Ops Log Functionality
-Completed New Ops Log Functionality.
-Slight rework of how the NewOpsLogModalVM handles tags.
-Added error catching for missing required fields in New Ops Logs.
* Config share code + fix for fetching jobs without auth
* UX tweaks on share config
* hooked up real url
* slightly better filter handling
* bugfix - filter boxes were not applying
* bug fix: handler for config checkboxes had back click handling
caused the config to save before the values had been updated.
* Separated Create Ops Log & Create Radio Log
Moved RadioLog into it's own VM (CreateRadioLogModalVM) - also renamed NewOpsLogModalVM to CreateOpsLogModalVM.
* job timeline magic
* tidy up after merge
* new job details thingy
* popup smoothing. new range circles. style updates
* smooth the page load so that it fades in once the dom loads
* linter fix
* Daily
Added mini map
Added geoservices common code and map layer for unit boundaries
* better layer menu
kinda?
* map layers refactored out into their own files
* fixup of something that has nothing to do with this WIP
* Team Status Update Functionality
Team Status functionality added & tested. UI is still a bit janky but we'll come back to that.
* Got rid of defunct team status update button
* tag code
* bad import
* turn down the debug
* fixed stupid map zoom bug on unit boundary
* rewrite of the scroll into view stuff
* remove jobs and teams that are not in search results
* bug fixes
* error out if the remote tab command tries to open the same tab
* remote control on self bug
* warn when a job is removed if it has focus
* warning when things are filtered out
* added bootstrap alerts wrapper
* less debug
* fixing the drop downs not appearing over the top of the dom overflow
* new body font
* Updates to Team Status Dropdown
- UI Cleanups
- Fixed issue with ETC/ETA not be nullified before API.
- Clicking button whilst open now closes.
* Fuck it
removed opslog
fixed asset bug
* linter fix
* dumb self calling function
* unsafe unwrap
* swap the order of config items around to be logical
* Tags on opslog messages now using the factory
* more tags in places using tag factory
* minor css and style tweaks for tags
* team row correctly disables button when team doesnt exist any more
* row hover code
* rewrite of popups css. added InstantTask VM.
* legend has asset icons now
* Added support for SIX maps layers
* sector filter support
* can assign and unassign sectors now
* GEO map work for new layers
* FRAO layer
* linter fix
* new layer menu
* show on jobs if theres outstanding action items. collapsable alerts. task count next to status. team row colors
* green border in train. custom sort rolled back out
* train beacon banner
* Stuff
* obnoxious training banner
* minor bug fix and new wording
* new SMS code. bugfix to stop tasking updating active teams
* linter and doco
* Lint fix ffs
* fixup on how we display team tasking counts.
fixup on how we display team tasking counts. fix on not showing assigned HQ
* stop click bubbling on disabled buttons in the left UI
* prevent message send while loading receps
* pass operational correctly
* reset defalts
* less in your face
* dont collapse teams clicking in the DIV
* cloudfront infront of FRAO query
* job filter fix. added contact add to sms
* check status date on active team filters
* add contact to message fixups
* UI fixups for searching
* collapse side menu UI
* cycle through matching assets if theres multiple for a team
* fixup button in asset pop to standard icon
* lots of small visual changes.
* code cleanup for team marker focus
* duplicate css
* Bug fixes
* turn down the debug
* hotkeys for all modals
ctrl/cmd + enter to submit. esc to close
* smaller
* Update OpsLogModalVM.js
wasnt reseting its error correctly.
* error handling for all fetch failures
uses the error pops to pass the errors back to the user
* modal header coloured to environment
Co-Authored-By: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
* new table css to fix up the widths
Co-Authored-By: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
* Lots of visual fixups
Co-Authored-By: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
* swear to god CSS does this on purpose
Co-Authored-By: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
* better
Co-Authored-By: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
* case sensitive
---------
Co-authored-by: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
---
src/pages/tasking/main.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pages/tasking/main.js b/src/pages/tasking/main.js
index a006a9f9..34575c03 100644
--- a/src/pages/tasking/main.js
+++ b/src/pages/tasking/main.js
@@ -58,7 +58,7 @@ import { renderFRAOSLayer } from "./mapLayers/frao.js";
import { fetchHqDetailsSummary } from './utils/hqSummary.js';
-import { installModalHotkeys } from './components/modalHotkeys.js';
+import { installModalHotkeys } from './components/modalHotKeys.js';
From fd414af14c040c8b466bb15ff300656e380018f8 Mon Sep 17 00:00:00 2001
From: Tim Dykes
Date: Wed, 17 Dec 2025 17:29:09 +1100
Subject: [PATCH 37/61] Tdykes.tasking.wip (#256)
* boostrap upgrade broke team summary.
This restores bootstrap 4 and adds an alias for bootstrap 5
* first push - new tasking dashboard
* New features
* New features
* moved map to vm
made a new vm just for map
* refactored out the mapvm
* map popups have their own view models now
* starting to flesh out the popups UI
new UI for popups. refactor all the models
* Map with a capital M
wtf is mac doing with this
* Add fit bound button to job pop
* removed leaflet-responsive-popup
back to normal popups
* missed a bad import
* ffs and another
* redoing the setup screen. this one isnt horrible
unit search at the top, both results boxes below
* new config page with dropdown for HQ search
* All sorts of new stuff
New pulse marker for unclaimed
New icons
New refacactor of colours
New incident menu
* linter fix
* UX Overlays
Legend updates
Notification overlays
* linter fix
* small refactor
* only untask warn on active
* extension for opsLog for single entry
* ICEMS unack alerts and fixed legend
* layer draw prototype
* started making transport NSW api code
* lots of code cleanup
* team and job list sorting on column header press
* opslog filters. sticky row headers on jobs and teams
* draggable row fixes. focus on job button
* linter fix
* only deploy on dev branch
* correct filtering of job and team status. added image icon
* Core OpsLog Creation Functionality + New Radio Log UI (#219)
* NewOpsLogEntry Core + Instant Radio Team on Job
Created:
- BeaconClient for creating (POST) New OpsLogEntries.
- Foundation for creating OpsLogEntries via Modal.
- Radio Log button for Teams on Incidents (Taskings).
* Added Team Radio Log Button
Added:
- Team Radio Log (Job agnostic) button & functions.
- Added new status update button to Incidents -> Tasking & Teams -> Taskings (Task list icons)
- Updated Radio Log button to Microphone (inline with other Lighthouse Instant Radio Log icons)
- Started work on new OpsLogEntry code.
* Removing incorrectly duplicated data-bind
* unfucked the map for jobs
unfucked the map for jobs
* Draft New Ops Log Modal
* Create enum.js
* map in some enums correctly, refresh job tasking on expand
* clicking a job calls focus and expands in the job table
* new asset popup
has protection for clicking jobs that are not in filter.
new job ifFiltered logic so it wont update if its out of view
* show situation on scene
* bad refresh bug
refresh was stuck in a loop updating every 30 seconds
* wrong icon
* restrict row drag to just the first cell
* fixed the double toggle load
* new jobs list layout
* Bug Fixes
Teams had no filters on their obvervables
jobs and assets had no disposal methods
teams had no JSON update function
jobs popup had no protection for non asset teams or team out of view
* team list has the new cards list. tasking status badges have consistent color
* job status filters logic and new UI in the menu
* safe defaults in panel layout
* New Ops Log Functionality
-Completed New Ops Log Functionality.
-Slight rework of how the NewOpsLogModalVM handles tags.
-Added error catching for missing required fields in New Ops Logs.
* Config share code + fix for fetching jobs without auth
* UX tweaks on share config
* hooked up real url
* slightly better filter handling
* bugfix - filter boxes were not applying
* bug fix: handler for config checkboxes had back click handling
caused the config to save before the values had been updated.
* Separated Create Ops Log & Create Radio Log
Moved RadioLog into it's own VM (CreateRadioLogModalVM) - also renamed NewOpsLogModalVM to CreateOpsLogModalVM.
* job timeline magic
* tidy up after merge
* new job details thingy
* popup smoothing. new range circles. style updates
* smooth the page load so that it fades in once the dom loads
* linter fix
* Daily
Added mini map
Added geoservices common code and map layer for unit boundaries
* better layer menu
kinda?
* map layers refactored out into their own files
* fixup of something that has nothing to do with this WIP
* Team Status Update Functionality
Team Status functionality added & tested. UI is still a bit janky but we'll come back to that.
* Got rid of defunct team status update button
* tag code
* bad import
* turn down the debug
* fixed stupid map zoom bug on unit boundary
* rewrite of the scroll into view stuff
* remove jobs and teams that are not in search results
* bug fixes
* error out if the remote tab command tries to open the same tab
* remote control on self bug
* warn when a job is removed if it has focus
* warning when things are filtered out
* added bootstrap alerts wrapper
* less debug
* fixing the drop downs not appearing over the top of the dom overflow
* new body font
* Updates to Team Status Dropdown
- UI Cleanups
- Fixed issue with ETC/ETA not be nullified before API.
- Clicking button whilst open now closes.
* Fuck it
removed opslog
fixed asset bug
* linter fix
* dumb self calling function
* unsafe unwrap
* swap the order of config items around to be logical
* Tags on opslog messages now using the factory
* more tags in places using tag factory
* minor css and style tweaks for tags
* team row correctly disables button when team doesnt exist any more
* row hover code
* rewrite of popups css. added InstantTask VM.
* legend has asset icons now
* Added support for SIX maps layers
* sector filter support
* can assign and unassign sectors now
* GEO map work for new layers
* FRAO layer
* linter fix
* new layer menu
* show on jobs if theres outstanding action items. collapsable alerts. task count next to status. team row colors
* green border in train. custom sort rolled back out
* train beacon banner
* Stuff
* obnoxious training banner
* minor bug fix and new wording
* new SMS code. bugfix to stop tasking updating active teams
* linter and doco
* Lint fix ffs
* fixup on how we display team tasking counts.
fixup on how we display team tasking counts. fix on not showing assigned HQ
* stop click bubbling on disabled buttons in the left UI
* prevent message send while loading receps
* pass operational correctly
* reset defalts
* less in your face
* dont collapse teams clicking in the DIV
* cloudfront infront of FRAO query
* job filter fix. added contact add to sms
* check status date on active team filters
* add contact to message fixups
* UI fixups for searching
* collapse side menu UI
* cycle through matching assets if theres multiple for a team
* fixup button in asset pop to standard icon
* lots of small visual changes.
* code cleanup for team marker focus
* duplicate css
* Bug fixes
* turn down the debug
* hotkeys for all modals
ctrl/cmd + enter to submit. esc to close
* smaller
* Update OpsLogModalVM.js
wasnt reseting its error correctly.
* error handling for all fetch failures
uses the error pops to pass the errors back to the user
* modal header coloured to environment
Co-Authored-By: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
* new table css to fix up the widths
Co-Authored-By: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
* Lots of visual fixups
Co-Authored-By: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
* swear to god CSS does this on purpose
Co-Authored-By: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
* better
Co-Authored-By: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
* case sensitive
* added lhquickComplete url param to get the team complete modal to open
* canDo functions on job vm setup. NO UI
* bug with crow flies line from asset
* job status changing via a popup
* lint fix
---------
Co-authored-by: Rowantrek <12322201+Rowantrek@users.noreply.github.com>
---
src/injectscripts/jobs/view.js | 335 ++++++++++--------
src/pages/tasking/components/job_popup.js | 2 +-
src/pages/tasking/components/modalHotKeys.js | 2 -
src/pages/tasking/main.js | 83 ++++-
src/pages/tasking/models/Job.js | 90 ++++-
src/pages/tasking/models/Team.js | 2 +-
src/pages/tasking/utils/enum.js | 90 +++++
src/pages/tasking/viewmodels/AssetPopUp.js | 30 +-
.../viewmodels/JobStatusConfirmModalVM.js | 83 +++++
src/pages/tasking/viewmodels/Map.js | 4 +-
src/shared/BeaconClient.js | 5 +-
src/shared/BeaconClient/job.js | 92 ++++-
src/shared/BeaconClient/suppliers.js | 21 ++
static/pages/tasking.html | 315 ++++++++++------
styles/pages/tasking.css | 36 ++
15 files changed, 873 insertions(+), 317 deletions(-)
create mode 100644 src/pages/tasking/viewmodels/JobStatusConfirmModalVM.js
create mode 100644 src/shared/BeaconClient/suppliers.js
diff --git a/src/injectscripts/jobs/view.js b/src/injectscripts/jobs/view.js
index 2c3ecdbc..0b814086 100644
--- a/src/injectscripts/jobs/view.js
+++ b/src/injectscripts/jobs/view.js
@@ -21,10 +21,10 @@ console.log('Running inject script');
//track if shift is held down for the form submit shortcut in instant radio logs
var shiftKeyHeld = false;
-window.onkeyup = function(e) {
+window.onkeyup = function (e) {
shiftKeyHeld = e.shiftKey
}
-window.onkeydown = function(e) {
+window.onkeydown = function (e) {
shiftKeyHeld = e.shiftKey
}
///
@@ -139,73 +139,73 @@ function lighthouseETA() {
function lighthouseTasking() {
whenLighthouseIsReady(function () {
- console.log('Tasking Changes, adding buttons where needed per team');
- ///Horrible nasty code for untasking
-
- $("a[data-bind=\"attr: {href: '/Teams/' + Team.Id + '/Edit'}, text: Team.Callsign\"").each(function (k, v) {
- //for every dom that shows team name
- if ($(v).parent().find('#message').length == 0) {
- //check for an existing msg button
- let DOMCallsign = $(this)[0].href.split('/')[4];
- //for every tasked team
- $.each(masterViewModel.teamsViewModel.taskedTeams.peek(), function (k, vv) {
- //match against this DOM
- if (vv.Team.Id == DOMCallsign && vv.Team.Members.length) {
- //attach a sms button
- let msg = return_message_button();
- $(v).after(msg);
-
- //click function
- $(msg).click(function () {
- event.stopImmediatePropagation();
- var tightarray = [];
- vv.Team.Members.map(function (member) {
- tightarray.push(member.Person.Id);
+ console.log('Tasking Changes, adding buttons where needed per team');
+ ///Horrible nasty code for untasking
+
+ $("a[data-bind=\"attr: {href: '/Teams/' + Team.Id + '/Edit'}, text: Team.Callsign\"").each(function (k, v) {
+ //for every dom that shows team name
+ if ($(v).parent().find('#message').length == 0) {
+ //check for an existing msg button
+ let DOMCallsign = $(this)[0].href.split('/')[4];
+ //for every tasked team
+ $.each(masterViewModel.teamsViewModel.taskedTeams.peek(), function (k, vv) {
+ //match against this DOM
+ if (vv.Team.Id == DOMCallsign && vv.Team.Members.length) {
+ //attach a sms button
+ let msg = return_message_button();
+ $(v).after(msg);
+
+ //click function
+ $(msg).click(function () {
+ event.stopImmediatePropagation();
+ var tightarray = [];
+ vv.Team.Members.map(function (member) {
+ tightarray.push(member.Person.Id);
+ });
+ window.open(
+ '/Messages/Create?jobId=' + escape(jobId) + '&lhquickrecipient=' + escape(JSON.stringify(tightarray)),
+ '_blank',
+ );
});
- window.open(
- '/Messages/Create?jobId=' + escape(jobId) + '&lhquickrecipient=' + escape(JSON.stringify(tightarray)),
- '_blank',
- );
- });
- }
- });
- } else {
- console.log('already has a sms button');
- }
- });
+ }
+ });
+ } else {
+ console.log('already has a sms button');
+ }
+ });
- $('div.widget-content div.list-group div.list-group-item.clearfix div.col-xs-6.small.text-right').each(function (
- k,
- v,
- ) {
- //for every team dom
- if ($(v).parent().find('#untask').length == 0) {
- //check for an existing untask button
-
- //pull out the call sign and the status
- let DOMCallsign = $(this)[0].parentNode.children[0].children[0].href.split('/')[4];
- let DOMStatus = $(this)[0].parentNode.children[1].innerText.split(' ')[0];
- //for every tasked team
- $.each(masterViewModel.teamsViewModel.taskedTeams.peek(), function (k, vv) {
- //match against this DOM
- if (vv.Team.Id == DOMCallsign && vv.CurrentStatus == DOMStatus && vv.CurrentStatusId == 1) {
- //only show for tasking that can be deleted (tasked only)
- //attached a X button if its matched and deletable
- let untask = return_untask_button();
- $(v).append(untask);
-
- //click function
- $(untask).click(function () {
- event.stopImmediatePropagation();
- untaskTeamFromJob(vv.Team.Id, jobId, vv.Id); //untask it
- });
- }
- });
- } else {
- console.log('already has untask button');
- }
- });
-})
+ $('div.widget-content div.list-group div.list-group-item.clearfix div.col-xs-6.small.text-right').each(function (
+ k,
+ v,
+ ) {
+ //for every team dom
+ if ($(v).parent().find('#untask').length == 0) {
+ //check for an existing untask button
+
+ //pull out the call sign and the status
+ let DOMCallsign = $(this)[0].parentNode.children[0].children[0].href.split('/')[4];
+ let DOMStatus = $(this)[0].parentNode.children[1].innerText.split(' ')[0];
+ //for every tasked team
+ $.each(masterViewModel.teamsViewModel.taskedTeams.peek(), function (k, vv) {
+ //match against this DOM
+ if (vv.Team.Id == DOMCallsign && vv.CurrentStatus == DOMStatus && vv.CurrentStatusId == 1) {
+ //only show for tasking that can be deleted (tasked only)
+ //attached a X button if its matched and deletable
+ let untask = return_untask_button();
+ $(v).append(untask);
+
+ //click function
+ $(untask).click(function () {
+ event.stopImmediatePropagation();
+ untaskTeamFromJob(vv.Team.Id, jobId, vv.Id); //untask it
+ });
+ }
+ });
+ } else {
+ console.log('already has untask button');
+ }
+ });
+ })
}
//call on run
@@ -256,7 +256,7 @@ whenLighthouseIsReady(function () {
}
});
- if (typeof masterViewModel.geocodedAddress.peek() != 'undefined') {
+ if (typeof masterViewModel.geocodedAddress.peek() != 'undefined') {
if (
masterViewModel.geocodedAddress.peek().Latitude != null ||
masterViewModel.geocodedAddress.peek().Longitude != null
@@ -461,20 +461,20 @@ function renderNearestAssets({ teamFilter, activeOnly, resultsToDisplay, cb }) {
.find('#lhqContacts')
.append(
'