var resgrid;
(function (resgrid) {
var dispatch;
(function (dispatch) {
var editcall;
(function (editcall) {
// The unit lists are built by resgrid.dispatch.nearestboard and count as drawn once they are loaded.
var personnelTable, groupsTable, rolesTable;
var totalTables = 4;
var drawnTables = {};
var dispatchedEntitiesApplied = false;
// Ticks what the call is already dispatched to once every list has drawn its rows. Only once: a later redraw (a
// search, a sort, the nearest board re-sorting closest first) must not tick again what the dispatcher has
// since unticked to take it off the call.
function onInitialDraw(table) {
drawnTables[table] = true;
if (!dispatchedEntitiesApplied && Object.keys(drawnTables).length >= totalTables) {
dispatchedEntitiesApplied = true;
resgrid.dispatch.editcall.updateDispatchedEntities();
}
}
function getText(key, fallback) {
return (resgrid.dispatch && typeof resgrid.dispatch.getText === 'function')
? resgrid.dispatch.getText(key, fallback)
: fallback;
}
function formatText(template) {
if (resgrid.dispatch && typeof resgrid.dispatch.formatText === 'function') {
return resgrid.dispatch.formatText.apply(null, arguments);
}
var args = Array.prototype.slice.call(arguments, 1);
return (template || '').replace(/\{(\d+)\}/g, function (match, index) {
return typeof args[index] !== 'undefined' ? args[index] : match;
});
}
editcall.protocolCount = 0;
editcall.protocolData = {};
$(document).ready(function () {
callMarker = null;
map = null;
resgrid.dispatch.nearestboard.init({
getMap: function () { return map; },
getPersonnelGrid: function () { return personnelTable; },
onUnitsLoaded: function () {
applyRecommendationSelections();
onInitialDraw('units');
}
});
// A typed (or previously saved) address always wins over a reverse-geocoded one; only an
// empty field is filled from the map pin.
userSuppliedAddress = jQuery.trim($("#Call_Address").val() || '') !== '';
let quillNature = new Quill('#nature-container', {
placeholder: '',
theme: 'snow'
});
// The department's new-call field policy can leave the note, the map and other fields off the
// form, so anything bound to one of them has to cope with it not being there.
let quillNotes = $('#note-container').length ? new Quill('#note-container', {
placeholder: '',
theme: 'snow'
}) : null;
$(document).on('submit', '#updateCallForm', function () {
if (quillNotes) {
$('#Call_Notes').val(quillNotes.root.innerHTML);
}
$('#Call_NatureOfCall').val(quillNature.root.innerHTML);
return true;
});
if (newCallFormData) {
let newCallForm = $('#fb-template').formRender({
dataType: 'json',
formData: newCallFormData
});
$("#saveNewCallFrom").click(function (evt) {
var data = JSON.stringify(newCallForm.userData);
$("#Call_CallFormData").val(data);
});
}
$('#PrimaryContact').select2();
$('#AdditionalContacts').select2();
$("#Call_Address").bind("keypress", function (event) {
if (event.keyCode == 13) {
$("#searchButton").click();
return false;
}
});
$("#Call_Address").on("input", function () {
userSuppliedAddress = jQuery.trim($(this).val() || '') !== '';
});
$("#What3Word").bind("keypress", function (event) {
if (event.keyCode == 13) {
$("#findw3wButton").click();
return false;
}
});
$("#CallPriority").change(function () {
checkForProtocols();
scheduleRecommendation();
});
$("#Call_Type").change(function () {
checkForProtocols();
scheduleRecommendation();
});
$("#selectLinkedCall").select2({
dropdownParent: $("#selectCallToLinkModal"),
ajax: {
url: resgrid.absoluteBaseUrl + '/User/Dispatch/GetCallsForSelectList',
dataType: 'json',
delay: 250,
data: function (params) {
return {
term: params.term
};
},
}
});
$.ajax({
url: resgrid.absoluteBaseUrl + '/User/Dispatch/GetMapDataForCall?callId=' + callId,
contentType: 'application/json; charset=utf-8',
type: 'GET'
}).done(function (result) {
// No map when GPS coordinates are hidden by the field policy.
if (result && document.getElementById('callMap')) {
var data = result;
// resgrid.maps: MapLibre or Leaflet by the department's Maps.MapLibreWeb flag (maplibre plan section 7).
resgrid.maps.create('callMap', { center: [Number(data.centerLat), Number(data.centerLon)], zoom: 11, scrollZoom: false, traffic: 'on' }).then(function (created) {
map = created;
resgrid.dispatch.nearestboard.drawBoundaries();
map.onClick(function (clicked) {
var e = { latlng: { lat: clicked[0], lng: clicked[1] } };
resgrid.dispatch.nearestboard.keepView(e.latlng.lat, e.latlng.lng);
resgrid.dispatch.editcall.setMarkerLocation(e.latlng.lat.toString(), e.latlng.lng.toString());
$("#Latitude").val(e.latlng.lat.toString());
$("#Longitude").val(e.latlng.lng.toString());
map.setView(clicked);
resgrid.dispatch.editcall.geocodeCoordinates(e.latlng.lat, e.latlng.lng);
});
resgrid.dispatch.editcall.setMarkerLocation(data.centerLat, data.centerLon);
});
}
});
$("#searchButton").click(function (evt) {
var where = jQuery.trim($("#Call_Address").val());
if (where.length < 1)
return;
fetch('/api/web-bff/api/v4/Geocoding/ForwardGeocode?address=' + encodeURIComponent(where))
.then(function(r) {
if (!r.ok) throw new Error('ForwardGeocode failed with HTTP status ' + r.status);
return r.json();
})
.then(function(result) {
if (result && result.Data && result.Data.Latitude != null && result.Data.Longitude != null) {
var lat = result.Data.Latitude;
var lng = result.Data.Longitude;
if (map) {
map.setView([Number(lat), Number(lng)], 16);
}
$("#Latitude").val(lat.toString());
$("#Longitude").val(lng.toString());
resgrid.dispatch.editcall.setMarkerLocation(lat.toString(), lng.toString());
} else {
console.log("Geocode returned no results for: " + where);
}
})
.catch(function(err) { console.error("Geocode error:", err); });
evt.preventDefault();
});
$("#findw3wButton").click(function (evt) {
var word = jQuery.trim($("#What3Word").val());
if (word.length < 1)
return;
$.ajax({
url: resgrid.absoluteBaseUrl + '/User/Dispatch/GetCoordinatesFromW3W?words=' + word,
contentType: 'application/json',
type: 'GET'
}).done(function (data) {
if (data && data.Latitude && data.Longitude) {
if (map) {
map.setView([Number(data.Latitude), Number(data.Longitude)], 16);
}
$("#Latitude").val(data.Latitude);
$("#Longitude").val(data.Longitude);
resgrid.dispatch.editcall.geocodeCoordinates(data.Latitude, data.Longitude);
resgrid.dispatch.editcall.setMarkerLocation(data.Latitude, data.Longitude);
}
else {
alert(getText('whatThreeWordsNotFound', 'What3Words was unable to find a location for those words. Ensure they are 3 words separated by periods.'));
}
});
evt.preventDefault();
});
$("#setPinButton").click(function (evt) {
var lat = parseFloat($("#Latitude").val());
var lng = parseFloat($("#Longitude").val());
if (isNaN(lat) || isNaN(lng)) {
alert(getText('invalidCoordinates', 'Please enter valid numeric latitude and longitude values.'));
return false;
}
if (map) {
map.setView([Number(lat), Number(lng)], 16);
}
resgrid.dispatch.editcall.setMarkerLocation(lat.toString(), lng.toString());
evt.preventDefault();
});
$('#protocolQuestionWindow').on('show.bs.modal', function (event) {
var protocolId = $(event.relatedTarget).data('protocolid');
var protocol = null;
for (var i = 0; i < resgrid.dispatch.editcall.protocolData.length; i++) {
if (resgrid.dispatch.editcall.protocolData[i].Id === protocolId) {
protocol = resgrid.dispatch.editcall.protocolData[i];
break;
}
}
var modal = $(this);
modal.find('.modal-title').text(formatText(getText('questionsFor', 'Questions for {0}'), protocol.Name));
var questionHtml = "";
for (var t = 0; t < protocol.Questions.length; t++) {
var question = protocol.Questions[t];
questionHtml = questionHtml + `
';
}
modal.find('.modal-body').empty();
modal.find('.modal-body').append(questionHtml);
$('#processQuestionAnswers').removeAttr("data-protocolid");
$('#processQuestionAnswers').attr('data-protocolid', protocol.Id);
});
$('#processQuestionAnswers').click(function () {
var buttonProtocolId = $('#processQuestionAnswers').attr('data-protocolid');
$('#protocolQuestionWindow').modal('hide');
var protocol = null;
for (var i = 0; i < resgrid.dispatch.editcall.protocolData.length; i++) {
if (resgrid.dispatch.editcall.protocolData[i].Id === Number(buttonProtocolId)) {
protocol = resgrid.dispatch.editcall.protocolData[i];
break;
}
}
var totalAnswerWeight = 0;
for (var t = 0; t < protocol.Questions.length; t++) {
var question = protocol.Questions[t];
var answerWeight = $(`#questionAnswer_${question.Id}`).val();
if (answerWeight) {
totalAnswerWeight = totalAnswerWeight + Number(answerWeight);
}
}
$(`#answerProcotolQuestions_${protocol.Id}`).removeClass("btn-warning btn-success btn-inverse");
if (totalAnswerWeight >= protocol.MinimumWeight) {
$(`#pendingProtocol_${protocol.Id}`).val('1');
$(`#answerProcotolQuestions_${protocol.Id}`).addClass("btn-success");
} else {
$(`#answerProcotolQuestions_${protocol.Id}`).addClass("btn-inverse");
}
});
$('#addNewLinkedCall').click(function (e) {
var data = $('#selectLinkedCall').select2('data');
// Nothing picked yet: keep the modal open (stopping propagation keeps Bootstrap's delegated
// data-dismiss handler from closing it) and open the picker instead.
if (!data || !data.length || !data[0].id) {
e.stopPropagation();
$('#selectLinkedCall').select2('open');
return;
}
var callId = Number(data[0].id);
var note = $('#selectCallNote').val() || '';
// Built with DOM calls, not an HTML string: the call name and note are user-entered text.
if ($('#linkedCall_' + callId).length === 0) {
var row = $('
')
.append($('')
.attr('data-original-title', getText('removeThisCallLink', 'Remove this call link'))
.on('click', function () { $(this).closest('tr').remove(); }))
.appendTo(row);
$('#linkedCalls tbody').first().append(row);
}
$('#selectCallNote').val('');
$('#selectLinkedCall').empty();
});
// Distance, ETA and position come from the nearest board (one ranked request) rather than a routed lookup per person.
personnelTable = $("#personnelGrid").DataTable(resgrid.dispatch.nearestboard.personnelTableOptions(resgrid.absoluteBaseUrl + '/User/Personnel/GetPersonnelForCallGrid'));
personnelTable.on('draw', function() {
applyRecommendationSelections();
onInitialDraw('personnel');
});
groupsTable = $("#groupsGrid").DataTable({
ajax: { url: resgrid.absoluteBaseUrl + '/User/Groups/GetGroupsForCallGrid', dataSrc: '' },
paging: false,
columns: [
{ data: 'GroupId', title: '', orderable: false, searchable: false, render: function(data) { return ''; } },
{ data: 'Name', title: getText('name', 'Name'), render: resgrid.textCell },
{ data: 'Count', title: getText('personnelCount', 'Personnel Count') }
]
});
groupsTable.on('draw', function() {
$('#groupsGrid thead th:first').html('');
onInitialDraw('groups');
});
rolesTable = $("#rolesGrid").DataTable({
ajax: { url: resgrid.absoluteBaseUrl + '/User/Personnel/GetRolesForCallGrid', dataSrc: '' },
paging: false,
columns: [
{ data: 'RoleId', title: '', orderable: false, searchable: false, render: function(data) { return ''; } },
{ data: 'Name', title: getText('name', 'Name'), render: resgrid.textCell },
{ data: 'Count', title: getText('personnelCount', 'Personnel Count') }
]
});
rolesTable.on('draw', function() {
$('#rolesGrid thead th:first').html('');
onInitialDraw('roles');
});
$('#personnelGrid').on('click', '#checkAllPersonnel', function () { $('#personnelGrid').find('tbody :checkbox').prop('checked', this.checked); });
$('#groupsGrid').on('click', '#checkAllGroups', function () { $('#groupsGrid').find('tbody :checkbox').prop('checked', this.checked); });
$('#rolesGrid').on('click', '#checkAllRoles', function () { $('#rolesGrid').find('tbody :checkbox').prop('checked', this.checked); });
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var targetTab = e.target && e.target.getAttribute('href');
if (targetTab === '#personnelTab') { personnelTable.columns.adjust(); }
else if (targetTab === '#groupsTab') { groupsTable.columns.adjust(); }
else if (targetTab === '#rolesTab') { rolesTable.columns.adjust(); }
});
// A recommended resource the dispatcher unticks stays unticked across grid redraws. A unit's box is the
// hidden dispatch box the unit tables' copies change.
$(document).on('change', 'input[name^="dispatchUnit_"], input[name^="dispatchUser_"]', function () {
if (this.checked) {
return;
}
var name = this.name || '';
if (name.indexOf('dispatchUnit_') === 0) {
appliedUnitIds = appliedUnitIds.filter(function (id) { return id !== name.substring('dispatchUnit_'.length); });
} else if (name.indexOf('dispatchUser_') === 0) {
appliedUserIds = appliedUserIds.filter(function (id) { return id !== name.substring('dispatchUser_'.length); });
}
});
$('#runCardPanel').on('click', '#runCardSelectRecommended', function (evt) {
evt.preventDefault();
selectRecommended();
});
$('#runCardPanel').on('click', '#runCardRefresh', function (evt) {
evt.preventDefault();
checkForRecommendation();
});
// Conditional run cards: walk the decision graph fresh instead of the decision pinned at save.
$('#runCardPanel').on('click', '#runCardReevaluate', function (evt) {
evt.preventDefault();
reevaluateNext = true;
checkForRecommendation();
});
$('#outstandingRequirementsPanel').on('click', '.dismiss-requirement', function (evt) {
evt.preventDefault();
dismissRequirement($(this).data('requirement-id'));
});
loadOutstandingRequirements();
checkForProtocols();
scheduleRecommendation();
});
// ── Run card recommendation for adding resources ──
// What the call's run card still needs at its current alarm level, after the units and people already on it.
// Advisory only: nothing is ticked until the dispatcher presses Select, so saving an unrelated edit never
// dispatches extra resources. The ids it ticked are kept so a grid redraw re-applies them, and a sequence number
// lets a slow earlier response be discarded instead of overwriting a newer one.
var recommendationSequence = 0;
var recommendationTimer = null;
var currentRecommendation = null;
var reevaluateNext = false;
var responseCodes = [];
var appliedUnitIds = [];
var appliedUserIds = [];
function prop(obj, name) {
if (!obj) return undefined;
if (obj[name] !== undefined) return obj[name];
var pascal = name.charAt(0).toUpperCase() + name.slice(1);
return obj[pascal];
}
function escapeText(text) {
return $('').text(text === null || text === undefined ? '' : String(text)).html();
}
function recommendedIds(result) {
return {
units: (prop(result, 'units') || []).map(function (u) { return String(prop(u, 'unitId')); }),
users: (prop(result, 'personnel') || []).map(function (p) { return String(prop(p, 'userId')); }).filter(function (id) { return id && id !== 'undefined'; })
};
}
function applyRecommendationSelections() {
appliedUnitIds.forEach(function (id) {
$('input[name="dispatchUnit_' + id + '"]').prop('checked', true);
});
appliedUserIds.forEach(function (id) {
$('input[name="dispatchUser_' + id + '"]').prop('checked', true);
});
resgrid.dispatch.nearestboard.syncPicks();
}
editcall.applyRecommendationSelections = applyRecommendationSelections;
function isRecommendationApplied(result) {
var ids = recommendedIds(result);
return (ids.units.length + ids.users.length) > 0 &&
ids.units.every(function (id) { return appliedUnitIds.indexOf(id) >= 0; }) &&
ids.users.every(function (id) { return appliedUserIds.indexOf(id) >= 0; });
}
function selectRecommended() {
if (!currentRecommendation) {
return;
}
var ids = recommendedIds(currentRecommendation);
ids.units.forEach(function (id) { if (appliedUnitIds.indexOf(id) < 0) appliedUnitIds.push(id); });
ids.users.forEach(function (id) { if (appliedUserIds.indexOf(id) < 0) appliedUserIds.push(id); });
applyRecommendationSelections();
renderRecommendation(currentRecommendation);
}
editcall.selectRecommended = selectRecommended;
function renderRecommendation(result) {
var units = prop(result, 'units') || [];
var personnel = prop(result, 'personnel') || [];
var shortfalls = prop(result, 'shortfalls') || [];
var notes = prop(result, 'notes') || [];
var level = prop(result, 'alarmLevel') || 1;
var html = '' + escapeText(prop(result, 'matchedRunCardName')) + '';
if (level > 1) {
html += ' ' + escapeText(formatText(getText('runCardAlarmLevel', 'Alarm {0}'), level)) + '';
}
if (!units.length && !personnel.length && !shortfalls.length) {
html += '
' + escapeText(formatText(getText('runCardCovered', 'The resources on this call already cover the run card (alarm {0}).'), level)) + '
';
}
var decision = prop(result, 'decision');
var decisionRenderer = resgrid.dispatch.runcarddecision;
if (decision && decisionRenderer) {
html += decisionRenderer.renderDecision(decision);
}
if (units.length) {
html += '