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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,438 changes: 1,013 additions & 1,425 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@
"xmldom": "^0.6.0"
},
"devDependencies": {
"@babel/core": "^7.16.5",
"@babel/plugin-transform-react-jsx": "^7.17.3",
"@babel/plugin-transform-runtime": "^7.16.5",
"@babel/preset-env": "^7.16.5",
"@babel/preset-typescript": "^7.16.5",
"@babel/core": "^8.0.1",
"@babel/plugin-transform-react-jsx": "^8.0.1",
"@babel/plugin-transform-runtime": "^8.0.1",
"@babel/preset-env": "^8.0.2",
"@babel/preset-typescript": "^8.0.1",
"@types/chrome": "^0.0.171",
"@typescript-eslint/eslint-plugin": "^5.8.0",
"@typescript-eslint/parser": "^5.8.0",
"babel-loader": "^9.1.3",
"babel-loader": "^10.1.1",
"copy-webpack-plugin": "^14.0.0",
"css-loader": "^6.7.1",
"dotenv-webpack": "^7.0.3",
Expand Down
25 changes: 17 additions & 8 deletions src/pages/tasking/components/alerts.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ function createLeafletControl(L) {

/**
* Render a list of active rules into the container. - RAW html no KO here yet
* Each rule: { id, level, title, items:[{id,label}], count, onClick? }
*/
/**
* Render a list of active rules into the container. - RAW html no KO here yet
* Each rule: { id, level, title, items:[{id,label}], count, onClick? }
* Each rule: { id, level, title, items:[{id,label}], count, onClick?, prominent? }
*
* prominent (boolean, optional) — when true the alert box draws extra attention:
* • opaque (rather than translucent) background for the level colour
* • 2 px solid border instead of 1 px semi-transparent
* • repeating pulse-glow box-shadow animation
* • enlarged hazard icon with a continuous ping ring
* • count wrapped in a solid filled badge
*/
function renderRules(container, rules, opts = {}) {
const allowCollapse = opts.allowCollapse !== false;
Expand Down Expand Up @@ -72,6 +75,8 @@ function renderRules(container, rules, opts = {}) {
// --- IN-PLACE UPDATE: only patch count + items, preserve all user state ---
const countEl = div.querySelector('.alerts__count');
if (countEl) countEl.textContent = rule.count;
// keep prominent class in sync
div.classList.toggle('alerts--prominent', !!rule.prominent);

const ul = div.querySelector('.alerts__list');
if (ul) {
Expand All @@ -94,9 +99,12 @@ function renderRules(container, rules, opts = {}) {
div.setAttribute('data-rule-id', rule.id);
var width = '280px'
div.className = `leaflet-control alerts alerts--${rule.level}`;
if (rule.prominent) {
div.classList.add('alerts--prominent');
}
if (state.collapsed) {
div.classList.add('alerts--collapsed');
width = "24px"
width = "30px"
}
if (!state.collapsed && state.open) {
div.classList.add('alerts--open');
Expand Down Expand Up @@ -160,10 +168,10 @@ function renderRules(container, rules, opts = {}) {
btn.setAttribute('aria-expanded', 'false');

div.querySelector('.alerts').animate(
[{ width: '280px' }, { width: '24px' }],
[{ width: '280px' }, { width: '30px' }],
{ duration: 300, easing: 'ease-in-out' }
).onfinish = () => {
div.querySelector('.alerts').style.width = '24px';
div.querySelector('.alerts').style.width = '30px';
};
div.classList.add('alerts--collapsed');
});
Expand Down Expand Up @@ -298,6 +306,7 @@ function buildDefaultRules(vm) {
id: 'new-jobs',
level: 'warning',
title: 'Unacknowledged incidents',
prominent: true,
active: newJobs.length > 0,
items: newJobs.slice(0, 10).map(asItem),
count: newJobs.length,
Expand Down
9 changes: 5 additions & 4 deletions src/pages/tasking/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -715,8 +715,8 @@ function VM() {
if (!lastToken) { self.jobSearchSuggestions([]); return; }

const escapeRx = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const rx = /^\d+$/.test(lastToken)
? null // numeric — use includes
const rx = (/^\d+$/.test(lastToken) || /[^a-z0-9]/i.test(lastToken))
? null // numeric or contains non-alphanumeric (e.g. "14-7570") — use includes
: new RegExp('\\b' + escapeRx(lastToken), 'i');

// Helper: escape HTML entities so label text is safe for innerHTML
Expand Down Expand Up @@ -911,8 +911,9 @@ function VM() {
// surprising (e.g. "123" inside "J-00123" should still match).
const escapeRx = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const termMatchers = terms.map(t => {
if (/^\d+$/.test(t)) {
// Numeric token — plain substring is more intuitive
if (/^\d+$/.test(t) || /[^a-z0-9]/i.test(t)) {
// Numeric token or token containing non-alphanumeric chars (e.g. "14-7570")
// — plain substring is more intuitive and avoids \b boundary failures
return (blob) => blob.includes(t);
}
const rx = new RegExp('\\b' + escapeRx(t), 'i');
Expand Down
11 changes: 9 additions & 2 deletions src/pages/tasking/models/Job.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,15 @@ export function Job(data = {}, deps = {}) {
self.statusNameAndCount = ko.pureComputed(() => {
const statusName = self.statusName();
if (statusName === "Active" || statusName === "Tasked") {
const taskingCount = self.taskings().length;
return `${statusName} (${taskingCount})`;
const activeOnly = deps.config?.taskingCountActiveOnly?.();
const taskings = self.taskings();
const count = activeOnly
? taskings.filter(t => {
const s = t.currentStatus?.();
return s === "Tasked" || s === "Enroute" || s === "Onsite";
}).length
: taskings.length;
return `${statusName} (${count})`;
}
return statusName;
});
Expand Down
9 changes: 9 additions & 0 deletions src/pages/tasking/viewmodels/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ export function ConfigVM(root, deps) {
self.clusterRadius = ko.observable(60); // maxClusterRadius in px (10–80)
self.clusterRescueJobs = ko.observable(true);
self.alertsCollapsibleRules = ko.observable(true);
self.taskingCountActiveOnly = ko.observable(false);

// pinned rows
self.pinnedTeamIds = ko.observableArray([]);
Expand Down Expand Up @@ -399,6 +400,7 @@ export function ConfigVM(root, deps) {
clusterRadius: Number(self.clusterRadius()) || 60,
clusterRescueJobs: !!self.clusterRescueJobs(),
alertsCollapsibleRules: !!self.alertsCollapsibleRules(),
taskingCountActiveOnly: !!self.taskingCountActiveOnly(),
suggestionEnabled: !!self.suggestionEnabled(),
rescueDistanceWeight: Number(self.rescueDistanceWeight()) || 0,
rescueTaskingWeight: Number(self.rescueTaskingWeight()) || 0,
Expand Down Expand Up @@ -713,6 +715,9 @@ export function ConfigVM(root, deps) {
if (typeof cfg.alertsCollapsibleRules === 'boolean') {
self.alertsCollapsibleRules(cfg.alertsCollapsibleRules);
}
if (typeof cfg.taskingCountActiveOnly === 'boolean') {
self.taskingCountActiveOnly(cfg.taskingCountActiveOnly);
}

// Instant Task Suggestion Engine weights
if (typeof cfg.suggestionEnabled === 'boolean') {
Expand Down Expand Up @@ -905,6 +910,10 @@ export function ConfigVM(root, deps) {
self.save();
})

self.taskingCountActiveOnly.subscribe(() => {
self.save();
})

// Auto-save suggestion engine settings
self.suggestionEnabled.subscribe(() => { self.save(); });
self.rescueDistanceWeight.subscribe(() => { self.save(); });
Expand Down
13 changes: 13 additions & 0 deletions static/pages/tasking.html
Original file line number Diff line number Diff line change
Expand Up @@ -2088,6 +2088,19 @@ <h2 class="accordion-header" id="headingGeneral">
</div>
<div class="form-text">Alerts will start collapsed.</div>
</div>
<div class="col-sm-6 col-md-3">
<label class="form-label">
<i class="fa fa-hashtag me-1"></i>Tasking Count
</label>
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" id="taskingCountActiveOnlyToggle"
data-bind="checked: config.taskingCountActiveOnly">
<label class="form-check-label" for="taskingCountActiveOnlyToggle">
Active taskings only
</label>
</div>
<div class="form-text">Status badge counts only Tasked, Enroute &amp; Onsite taskings (excludes Complete, CalledOff, Untasked).</div>
</div>
</div>
</div>
</div>
Expand Down
6 changes: 6 additions & 0 deletions styles/pages/darkmode.css
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,12 @@ body.dark-mode .leaflet-control.alerts.alerts--info {
border: 1px solid rgba(95, 157, 255, 0.88) !important;
}

/* Prominent modifier — dark mode */
body.dark-mode .alerts--warning.alerts--prominent { background: rgba(160,110,0,0.95) !important; border: 2px solid #ffc107 !important; }
body.dark-mode .alerts--danger.alerts--prominent { background: rgba(140,20,30,0.95) !important; border: 2px solid #dc3545 !important; }
body.dark-mode .alerts--caution.alerts--prominent { background: rgba(8,66,152,0.95) !important; border: 2px solid #4d9aff !important; }
body.dark-mode .alerts--info.alerts--prominent { background: rgba(22,58,109,0.95) !important; border: 2px solid #5f9dff !important; }

/* Custom map layers drawer */
body.dark-mode .layers-drawer .ld-toggle-btn {
background: #2d2d2d !important;
Expand Down
82 changes: 81 additions & 1 deletion styles/pages/tasking.css
Original file line number Diff line number Diff line change
Expand Up @@ -1531,6 +1531,81 @@ overflow: hidden;
}
}

/* ── Prominent modifier ──────────────────────────────────────────────────── *
* Add prominent: true to any rule to make the alert visually urgent.
* Combines: opaque background, heavier border, repeating pulse-glow,
* enlarged icon, and a highlighted count badge.
* -------------------------------------------------------------------------- */
.alerts--prominent {
border-width: 2px;
border-style: solid;
animation:
alerts-prominent-pulse 2s ease-in-out infinite,
alerts-prominent-bounce 1.2s cubic-bezier(0.36, 0.07, 0.19, 0.97) infinite;
}
/* Stop bouncing once the panel is open — keep the pulse-glow */
.alerts--prominent.alerts--open {
animation: alerts-prominent-pulse 2s ease-in-out infinite;
}
.alerts--prominent .alerts__icon {
width: 22px;
height: 22px;
flex: 0 0 22px;
}
.alerts--prominent .alerts__icon::after {
content: '';
position: absolute;
inset: -3px;
border-radius: 50%;
border: 2px solid currentColor;
opacity: 0;
animation: alerts-ping-loop 2s ease-out infinite;
}
.alerts--prominent .alerts__count {
padding: 1px 7px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
/* Per-level badge colours — explicit so they never rely on currentColor */
.alerts--warning.alerts--prominent .alerts__count { background: #7a4800; color: #fff; }
.alerts--danger.alerts--prominent .alerts__count { background: #6b0d17; color: #fff; }
.alerts--caution.alerts--prominent .alerts__count { background: #052c65; color: #fff; }
.alerts--info.alerts--prominent .alerts__count { background: #053d22; color: #fff; }
/* Override the count text colour so it reads white over the solid badge */
.alerts--prominent .alerts__count {
filter: none;
}
/* Make the badge background use the level accent colour */
.alerts--warning.alerts--prominent { background: rgba(255,193,7,0.92); border-color: #e6a800; }
.alerts--danger.alerts--prominent { background: rgba(220,53,69,0.92); border-color: #b02a37; }
.alerts--caution.alerts--prominent { background: rgba(13,110,253,0.88); border-color: #0a58ca; color: #fff; }
.alerts--info.alerts--prominent { background: rgba(25,135,84,0.88); border-color: #0f6848; color: #fff; }

/* Force text legibility for levels that go dark-background */
.alerts--caution.alerts--prominent .alerts__panel,
.alerts--info.alerts--prominent .alerts__panel { color: #fff; border-top-color: rgba(255,255,255,0.4); }

@keyframes alerts-prominent-pulse {
0%,100% { box-shadow: 0 0 6px 0 currentColor; }
50% { box-shadow: 0 0 18px 4px currentColor; }
}
@keyframes alerts-prominent-bounce {
0%,100% { transform: translateY(0); }
10% { transform: translateY(-8px); }
20% { transform: translateY(0); }
30% { transform: translateY(-5px); }
40% { transform: translateY(0); }
50% { transform: translateY(-2px); }
60%,99% { transform: translateY(0); }
}

@keyframes alerts-ping-loop {
0% { transform: scale(0.8); opacity: 0.8; }
70% { transform: scale(1.8); opacity: 0.2; }
100% { transform: scale(2.4); opacity: 0; }
}

/* Subtle ping from the hazard icon once */
.alerts__btn .alerts__icon {
position: relative;
Expand Down Expand Up @@ -2672,8 +2747,13 @@ overflow: hidden;
}

.leaflet-control.alerts.alerts--collapsed .alerts__btn {
padding: 4px;
padding: 0;
min-width: auto;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}

#SendSMSModal .sms-recipient-list {
Expand Down
Loading