' : '';
@@ -356,7 +356,7 @@ function buildCreateTeamPickerModal() {
function renderCreateTeamPickerList(listSelector, people, checked) {
var $list = $(listSelector);
if (!people || !people.length) {
- $list.html('
No responders
');
+ $list.html('
No responses
');
return;
}
$list.html(_.map(people, function (person) {
diff --git a/src/injectscripts/teams/create.js b/src/injectscripts/teams/create.js
index 373e01d7..571f4ece 100644
--- a/src/injectscripts/teams/create.js
+++ b/src/injectscripts/teams/create.js
@@ -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
@@ -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('
Loading responses…
');
+
+ fetchActivationResponses(activationId, function (err, summary) {
+ if (err || !summary) {
+ $panel.html('
Could not load responses.
');
+ return;
+ }
+
+ var categories = summary.categories || {};
+ var $groups = $('');
+
+ $.each(ACTIVATION_PEOPLE_CATEGORIES, function (i, cat) {
+ var data = categories[cat.key];
+ if (!data || !data.Names || !data.Names.length) return;
+
+ var $group = $('');
+ $group.append(
+ $('')
+ .text(cat.label + ' (' + data.Count + ')')
+ );
+
+ var $names = $('
');
+ return;
+ }
+
+ $.each(activations, function (i, activation) {
+ var $row = $('');
+ var $header = $('');
+
+ $header.append('');
+ $header.append($('').text(activation.title));
+ $header.append($('').text(formatActivationTime(activation.start)));
+ if (activation.closedAt) {
+ $header.append('');
+ }
+
+ var $panel = $('').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 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 = $(
+ ''
+ );
+ $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('
Select an Assigned To HQ to see its recent activations.
');
+ return;
+ }
+
+ $list.html('
Loading recent activations…
');
+
+ fetchUnitActivations(toMamsUnitName(entity.Name), function (err, data) {
+ if (err) {
+ if (err.status === 404) {
+ $list.html('
No recent activations for ' + _.escape(entity.Name) + '.