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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ Lighthouse.zip
*.DS_Store
package-lock.json
lambda/myavailability-incident/
lambda/myavailability-unit-activations/
1 change: 1 addition & 0 deletions src/contentscripts/teams/create.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var inject = require('../../lib/inject.js');
require('../../styles/teams.create.css');

//inject our JS resource
inject('teams/create.js');
4 changes: 2 additions & 2 deletions src/injectscripts/jobs/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ function lighthouseResponseGems() {
? '<li><em>+' + (data.Names.length - MAX_NAMES_SHOWN) + ' more</em></li>'
: '') +
'</ul>'
: '<em>No responders</em>';
: '<em>No responses</em>';

var closedNote = isClosed ? '<div class="lighthouse-response-gem-closed-note"><em>Activation closed</em></div>' : '';

Expand Down Expand Up @@ -356,7 +356,7 @@ function buildCreateTeamPickerModal() {
function renderCreateTeamPickerList(listSelector, people, checked) {
var $list = $(listSelector);
if (!people || !people.length) {
$list.html('<li><em>No responders</em></li>');
$list.html('<li><em>No responses</em></li>');
return;
}
$list.html(_.map(people, function (person) {
Expand Down
277 changes: 276 additions & 1 deletion src/injectscripts/teams/create.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* global teamViewModel, $, _, ko */
/* global teamViewModel, $, _, ko, user, urls, lighthouseUrl */
//edit and create page.
// background js fiddles with create page to expose same viewmodel as OutageDisplayType

Expand Down Expand Up @@ -107,6 +107,281 @@ $(document).ready(function () {
}
});

// ---- Recent Activations fieldset ----
// Lets a team creator expand a recent activation for the assigned-to unit
// and click a name from its responses straight into the team, instead of
// only having the typeahead search. Built with plain jQuery (no data-bind)
// since this fieldset is inserted at runtime - it was never part of the
// page's own Knockout template, and knockout-secure-binding's restricted
// parser makes wiring ad-hoc bindings onto injected DOM more trouble than
// it's worth here.

// The myavailability Lambdas only have data for production Beacon
// (apibeacon.ses.nsw.gov.au) - trainbeacon/devbeacon unit names don't exist
// in that database. Gate on urls.Base so we never send a real request
// outside prod. Same check as jobs/view.js's isProductionBeaconApi().
function isProductionBeaconApi() {
return typeof urls !== 'undefined' && typeof urls.Base === 'string' &&
urls.Base.indexOf('apibeacon.ses.nsw.gov.au') !== -1;
}

// Only these three categories make sense to add to a team - mirrors the
// judgement call jobs/view.js already makes for its "Create Team" button
// (Unavailable/Unset responses aren't offered as a one-click add).
var ACTIVATION_PEOPLE_CATEGORIES = [
{ key: 'ActivationAccepted', label: 'Accepted', cssClass: 'lighthouse-activation-people-activationaccepted' },
{ key: 'Available', label: 'Available', cssClass: 'lighthouse-activation-people-available' },
{ key: 'Conditional', label: 'Conditional', cssClass: 'lighthouse-activation-people-conditional' },
];

function fetchUnitActivations(unitName, cb) {
$.ajax({
url: 'https://lambda.lighthouse-extension.com/myavailability/unit-activations',
method: 'GET',
data: { unitName: unitName },
beforeSend: function (xhr) { xhr.setRequestHeader('Authorization', 'Bearer ' + user.accessToken); },
dataType: 'json',
success: function (data) { cb(null, data); },
error: function (xhr, status, error) { cb({ status: xhr.status, error: error }); },
});
}

function fetchActivationResponses(activationId, cb) {
$.ajax({
url: 'https://lambda.lighthouse-extension.com/myavailability/incident',
method: 'GET',
data: { activationId: activationId },
beforeSend: function (xhr) { xhr.setRequestHeader('Authorization', 'Bearer ' + user.accessToken); },
dataType: 'json',
success: function (data) { cb(null, data); },
error: function (xhr, status, error) { cb({ status: xhr.status, error: error }); },
});
}

function formatActivationTime(iso) {
if (!iso) return '';
var d = new Date(iso);
if (isNaN(d.getTime())) return '';
return d.toLocaleString('en-AU', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' });
}

// entityAssignedTo().Name is just the town/suburb (e.g. "Parramatta") -
// mams's Unit.name includes the " Unit" suffix (e.g. "Parramatta Unit"), so
// the lookup has to add it back. Guarded in case an entity name ever
// already ends with "Unit" (e.g. some other suffix pattern), so this never
// sends a double "Unit Unit".
function toMamsUnitName(entityName) {
return /\bunit$/i.test(entityName.trim()) ? entityName.trim() : entityName.trim() + ' Unit';
}

function isMemberAlreadyInTeam(memberId) {
return _.some(teamViewModel.members.peek(), function (m) {
return m.Person && String(m.Person.RegistrationNumber) === String(memberId);
});
}

// Delegated (person rows are added long after this fires, inside an async
// panel render) - clicking adds via the same addTeamMemberById() the
// ?lhmembers= prefill path already uses, then checks teamViewModel.members
// afterwards to confirm it actually landed (addTeamMemberById swallows its
// own errors - logs and returns - so this is the only way to tell success
// from a silent lookup failure without changing that function).
$(document).off('click.lighthouseActivationPerson').on('click.lighthouseActivationPerson', '.lighthouse-activation-person', async function () {
var $person = $(this);
if ($person.hasClass('lighthouse-activation-person-added') || $person.hasClass('lighthouse-activation-person-adding')) return;

var memberId = $person.attr('data-member-id');
$person.addClass('lighthouse-activation-person-adding');

await addTeamMemberById(memberId);

$person.removeClass('lighthouse-activation-person-adding');
if (isMemberAlreadyInTeam(memberId)) {
$person.addClass('lighthouse-activation-person-added');
} else {
$person.addClass('lighthouse-activation-person-error').attr('title', 'Could not add this person - see console for details');
}
});

function renderActivationPeople($panel, activationId) {
$panel.html('<div class="lighthouse-activation-people-loading">Loading responses&hellip;</div>');

fetchActivationResponses(activationId, function (err, summary) {
if (err || !summary) {
$panel.html('<div class="lighthouse-activation-people-error">Could not load responses.</div>');
return;
}

var categories = summary.categories || {};
var $groups = $('<div class="lighthouse-activation-people-groups"></div>');

$.each(ACTIVATION_PEOPLE_CATEGORIES, function (i, cat) {
var data = categories[cat.key];
if (!data || !data.Names || !data.Names.length) return;

var $group = $('<div class="lighthouse-activation-people-group"></div>');
$group.append(
$('<div class="lighthouse-activation-people-group-label ' + cat.cssClass + '"></div>')
.text(cat.label + ' (' + data.Count + ')')
);

var $names = $('<ul class="lighthouse-activation-people-names"></ul>');
$.each(data.Names, function (j, person) {
var alreadyIn = isMemberAlreadyInTeam(person.MemberId);
var $li = $('<li class="lighthouse-activation-person"></li>')
.attr('data-member-id', person.MemberId)
.toggleClass('lighthouse-activation-person-added', alreadyIn)
.text(person.Name);
$names.append($li);
});
$group.append($names);
$groups.append($group);
});

if (!$groups.children().length) {
$groups = $('<div class="lighthouse-activation-people-empty">No accepted, available or conditional responses yet.</div>');
}

$panel.empty().append($groups);
});
}

function renderActivations($list, activations) {
$list.empty();

if (!activations || !activations.length) {
$list.append('<div class="lighthouse-recent-activations-empty">No recent activations for this unit.</div>');
return;
}

$.each(activations, function (i, activation) {
var $row = $('<div class="lighthouse-recent-activation"></div>');
var $header = $('<div class="lighthouse-recent-activation-header"></div>');

$header.append('<span class="lighthouse-recent-activation-toggle-icon fa fa-caret-right"></span>');
$header.append($('<span class="lighthouse-recent-activation-title"></span>').text(activation.title));
$header.append($('<span class="lighthouse-recent-activation-time"></span>').text(formatActivationTime(activation.start)));
if (activation.closedAt) {
$header.append('<em class="fa fa-lock lighthouse-recent-activation-closed-icon" title="Activation closed"></em>');
}

var $panel = $('<div class="lighthouse-recent-activation-panel"></div>').hide();
var loaded = false;

$header.on('click', function () {
var willOpen = !$panel.is(':visible');
$header.find('.lighthouse-recent-activation-toggle-icon')
.toggleClass('fa-caret-right', !willOpen)
.toggleClass('fa-caret-down', willOpen);
$panel.slideToggle(150);
if (willOpen && !loaded) {
loaded = true;
renderActivationPeople($panel, activation.activationId);
}
});

$row.append($header).append($panel);
$list.append($row);
});
}

// jobs/view.js's picker modal builds its logo <img> before lighthouseUrl
// (set async via postMessage from the content script) is guaranteed to
// exist yet, then fills the src in once it's ready - same reasoning here.
function whenLighthouseIsReady(cb) {
if (typeof lighthouseUrl !== 'undefined') {
cb();
} else {
var waiting = setInterval(function () {
if (typeof lighthouseUrl !== 'undefined') {
clearInterval(waiting);
cb();
}
}, 200);
}
}

function initRecentActivationsFieldset() {
var isProd = isProductionBeaconApi();

var $existingFieldset = $('#teamMemberSearch').closest('fieldset');
if (!$existingFieldset.length) return;

var $fieldset = $(
'<fieldset id="lighthouseRecentActivationsFieldset">' +
'<legend>' +
'<img id="lighthouseRecentActivationsLogo" style="width:14px;vertical-align:middle;margin-right:5px" />' +
'Recent myAvailability Activation Requests' +
'<button type="button" class="btn btn-xs btn-default lighthouse-recent-activations-refresh"' + (isProd ? '' : ' disabled') + '>' +
'<span class="fa fa-refresh"></span> Refresh' +
'</button>' +
'</legend>' +
'<div class="form-group"><div class="col-xs-12">' +
'<div class="lighthouse-recent-activations-list">' +
'<div class="lighthouse-recent-activations-empty">' +
(isProd ? 'Click Refresh to load recent activations for the assigned unit.' :
'myAvailability activation requests are only available on production Beacon (this is train/dev).') +
'</div>' +
'</div>' +
'</div></div>' +
'</fieldset>'
);
$existingFieldset.before($fieldset);

whenLighthouseIsReady(function () {
$fieldset.find('#lighthouseRecentActivationsLogo').attr('src', lighthouseUrl + 'icons/lh-black.png');
});

if (!isProd) return; // placeholder only - no mams data outside prod

var $list = $fieldset.find('.lighthouse-recent-activations-list');

function loadForEntity(entity) {
// Not every entityAssignedTo is an SES Unit (e.g. a Region or State HQ
// isn't a row in mams's Unit table) - a 404 here just means "nothing to
// show for this assignment", not a real failure.
if (!entity || !entity.Name) {
$list.html('<div class="lighthouse-recent-activations-empty">Select an Assigned To HQ to see its recent activations.</div>');
return;
}

$list.html('<div class="lighthouse-recent-activations-loading">Loading recent activations&hellip;</div>');

fetchUnitActivations(toMamsUnitName(entity.Name), function (err, data) {
if (err) {
if (err.status === 404) {
$list.html('<div class="lighthouse-recent-activations-empty">No recent activations for ' + _.escape(entity.Name) + '.</div>');
} else if (err.status === 409) {
$list.html('<div class="lighthouse-recent-activations-empty">More than one unit is named ' + _.escape(entity.Name) + ' - can\'t show activations for it.</div>');
} else {
$list.html('<div class="lighthouse-recent-activations-error">Could not load recent activations.</div>');
}
return;
}
renderActivations($list, data.activations);
});
}

// Deliberately not auto-loaded on page open - only fetches once a human
// has clicked Refresh at least once. After that first manual load,
// though, changing the Assigned To HQ re-fetches automatically so the
// list doesn't keep showing activations for a unit that's no longer
// selected.
var hasLoadedOnce = false;

$fieldset.find('.lighthouse-recent-activations-refresh').on('click', function () {
hasLoadedOnce = true;
loadForEntity(teamViewModel.entityAssignedTo.peek());
});

teamViewModel.entityAssignedTo.subscribe(function (entity) {
if (!hasLoadedOnce) return;
loadForEntity(entity);
});
}

initRecentActivationsFieldset();

//when team members change
teamViewModel.members.subscribe(function() {
// auto set the first team member as TL
Expand Down
Loading