github.com/lunarobliq/gophish@v0.8.1-0.20230523153303-93511002234d/static/js/src/app/campaign_results.js (about)

     1  var map = null
     2  var doPoll = true;
     3  
     4  // statuses is a helper map to point result statuses to ui classes
     5  var statuses = {
     6      "Email Sent": {
     7          color: "#1abc9c",
     8          label: "label-success",
     9          icon: "fa-envelope",
    10          point: "ct-point-sent"
    11      },
    12      "Emails Sent": {
    13          color: "#1abc9c",
    14          label: "label-success",
    15          icon: "fa-envelope",
    16          point: "ct-point-sent"
    17      },
    18      "In progress": {
    19          label: "label-primary"
    20      },
    21      "Queued": {
    22          label: "label-info"
    23      },
    24      "Completed": {
    25          label: "label-success"
    26      },
    27      "Email Opened": {
    28          color: "#f9bf3b",
    29          label: "label-warning",
    30          icon: "fa-envelope-open",
    31          point: "ct-point-opened"
    32      },
    33      "Clicked Link": {
    34          color: "#F39C12",
    35          label: "label-clicked",
    36          icon: "fa-mouse-pointer",
    37          point: "ct-point-clicked"
    38      },
    39      "Success": {
    40          color: "#f05b4f",
    41          label: "label-danger",
    42          icon: "fa-exclamation",
    43          point: "ct-point-clicked"
    44      },
    45      //not a status, but is used for the campaign timeline and user timeline
    46      "Email Reported": {
    47          color: "#45d6ef",
    48          label: "label-info",
    49          icon: "fa-bullhorn",
    50          point: "ct-point-reported"
    51      },
    52      "Error": {
    53          color: "#6c7a89",
    54          label: "label-default",
    55          icon: "fa-times",
    56          point: "ct-point-error"
    57      },
    58      "Error Sending Email": {
    59          color: "#6c7a89",
    60          label: "label-default",
    61          icon: "fa-times",
    62          point: "ct-point-error"
    63      },
    64      "Submitted Data": {
    65          color: "#f05b4f",
    66          label: "label-danger",
    67          icon: "fa-exclamation",
    68          point: "ct-point-clicked"
    69      },
    70      "Unknown": {
    71          color: "#6c7a89",
    72          label: "label-default",
    73          icon: "fa-question",
    74          point: "ct-point-error"
    75      },
    76      "Sending": {
    77          color: "#428bca",
    78          label: "label-primary",
    79          icon: "fa-spinner",
    80          point: "ct-point-sending"
    81      },
    82      "Retrying": {
    83          color: "#6c7a89",
    84          label: "label-default",
    85          icon: "fa-clock-o",
    86          point: "ct-point-error"
    87      },
    88      "Scheduled": {
    89          color: "#428bca",
    90          label: "label-primary",
    91          icon: "fa-clock-o",
    92          point: "ct-point-sending"
    93      },
    94      "Campaign Created": {
    95          label: "label-success",
    96          icon: "fa-rocket"
    97      }
    98  }
    99  
   100  var statusMapping = {
   101      "Email Sent": "sent",
   102      "Email Opened": "opened",
   103      "Clicked Link": "clicked",
   104      "Submitted Data": "submitted_data",
   105      "Email Reported": "reported",
   106  }
   107  
   108  // This is an underwhelming attempt at an enum
   109  // until I have time to refactor this appropriately.
   110  var progressListing = [
   111      "Email Sent",
   112      "Email Opened",
   113      "Clicked Link",
   114      "Submitted Data"
   115  ]
   116  
   117  var campaign = {}
   118  var bubbles = []
   119  
   120  function dismiss() {
   121      $("#modal\\.flashes").empty()
   122      $("#modal").modal('hide')
   123      $("#resultsTable").dataTable().DataTable().clear().draw()
   124  }
   125  
   126  // Deletes a campaign after prompting the user
   127  function deleteCampaign() {
   128      Swal.fire({
   129          title: "Are you sure?",
   130          text: "This will delete the campaign. This can't be undone!",
   131          type: "warning",
   132          animation: false,
   133          showCancelButton: true,
   134          confirmButtonText: "Delete Campaign",
   135          confirmButtonColor: "#428bca",
   136          reverseButtons: true,
   137          allowOutsideClick: false,
   138          showLoaderOnConfirm: true,
   139          preConfirm: function () {
   140              return new Promise(function (resolve, reject) {
   141                  api.campaignId.delete(campaign.id)
   142                      .success(function (msg) {
   143                          resolve()
   144                      })
   145                      .error(function (data) {
   146                          reject(data.responseJSON.message)
   147                      })
   148              })
   149          }
   150      }).then(function (result) {
   151          if(result.value){
   152              Swal.fire(
   153                  'Campaign Deleted!',
   154                  'This campaign has been deleted!',
   155                  'success'
   156              );
   157          }
   158          $('button:contains("OK")').on('click', function () {
   159              location.href = '/campaigns'
   160          })
   161      })
   162  }
   163  
   164  // Completes a campaign after prompting the user
   165  function completeCampaign() {
   166      Swal.fire({
   167          title: "Are you sure?",
   168          text: "Gophish will stop processing events for this campaign",
   169          type: "warning",
   170          animation: false,
   171          showCancelButton: true,
   172          confirmButtonText: "Complete Campaign",
   173          confirmButtonColor: "#428bca",
   174          reverseButtons: true,
   175          allowOutsideClick: false,
   176          showLoaderOnConfirm: true,
   177          preConfirm: function () {
   178              return new Promise(function (resolve, reject) {
   179                  api.campaignId.complete(campaign.id)
   180                      .success(function (msg) {
   181                          resolve()
   182                      })
   183                      .error(function (data) {
   184                          reject(data.responseJSON.message)
   185                      })
   186              })
   187          }
   188      }).then(function (result) {
   189          if (result.value){
   190              Swal.fire(
   191                  'Campaign Completed!',
   192                  'This campaign has been completed!',
   193                  'success'
   194              );
   195              $('#complete_button')[0].disabled = true;
   196              $('#complete_button').text('Completed!')
   197              doPoll = false;
   198          }
   199      })
   200  }
   201  
   202  // Exports campaign results as a CSV file
   203  function exportAsCSV(scope) {
   204      exportHTML = $("#exportButton").html()
   205      var csvScope = null
   206      var filename = campaign.name + ' - ' + capitalize(scope) + '.csv'
   207      switch (scope) {
   208          case "results":
   209              csvScope = campaign.results
   210              break;
   211          case "events":
   212              csvScope = campaign.timeline
   213              break;
   214      }
   215      if (!csvScope) {
   216          return
   217      }
   218      $("#exportButton").html('<i class="fa fa-spinner fa-spin"></i>')
   219      var csvString = Papa.unparse(csvScope, {
   220          'escapeFormulae': true
   221      })
   222      var csvData = new Blob([csvString], {
   223          type: 'text/csv;charset=utf-8;'
   224      });
   225      if (navigator.msSaveBlob) {
   226          navigator.msSaveBlob(csvData, filename);
   227      } else {
   228          var csvURL = window.URL.createObjectURL(csvData);
   229          var dlLink = document.createElement('a');
   230          dlLink.href = csvURL;
   231          dlLink.setAttribute('download', filename)
   232          document.body.appendChild(dlLink)
   233          dlLink.click();
   234          document.body.removeChild(dlLink)
   235      }
   236      $("#exportButton").html(exportHTML)
   237  }
   238  
   239  function replay(event_idx) {
   240      request = campaign.timeline[event_idx]
   241      details = JSON.parse(request.details)
   242      url = null
   243      form = $('<form>').attr({
   244          method: 'POST',
   245          target: '_blank',
   246      })
   247      /* Create a form object and submit it */
   248      $.each(Object.keys(details.payload), function (i, param) {
   249          if (param == "rid") {
   250              return true;
   251          }
   252          if (param == "__original_url") {
   253              url = details.payload[param];
   254              return true;
   255          }
   256          $('<input>').attr({
   257              name: param,
   258          }).val(details.payload[param]).appendTo(form);
   259      })
   260      /* Ensure we know where to send the user */
   261      // Prompt for the URL
   262      Swal.fire({
   263          title: 'Where do you want the credentials submitted to?',
   264          input: 'text',
   265          showCancelButton: true,
   266          inputPlaceholder: "http://example.com/login",
   267          inputValue: url || "",
   268          inputValidator: function (value) {
   269              return new Promise(function (resolve, reject) {
   270                  if (value) {
   271                      resolve();
   272                  } else {
   273                      reject('Invalid URL.');
   274                  }
   275              });
   276          }
   277      }).then(function (result) {
   278          if (result.value){
   279              url = result.value
   280              submitForm()
   281          }
   282      })
   283      return
   284      submitForm()
   285  
   286      function submitForm() {
   287          form.attr({
   288              action: url
   289          })
   290          form.appendTo('body').submit().remove()
   291      }
   292  }
   293  
   294  /**
   295   * Returns an HTML string that displays the OS and browser that clicked the link
   296   * or submitted credentials.
   297   * 
   298   * @param {object} event_details - The "details" parameter for a campaign
   299   *  timeline event
   300   * 
   301   */
   302  var renderDevice = function (event_details) {
   303      var ua = UAParser(details.browser['user-agent'])
   304      var detailsString = '<div class="timeline-device-details">'
   305  
   306      var deviceIcon = 'laptop'
   307      if (ua.device.type) {
   308          if (ua.device.type == 'tablet' || ua.device.type == 'mobile') {
   309              deviceIcon = ua.device.type
   310          }
   311      }
   312  
   313      var deviceVendor = ''
   314      if (ua.device.vendor) {
   315          deviceVendor = ua.device.vendor.toLowerCase()
   316          if (deviceVendor == 'microsoft') deviceVendor = 'windows'
   317      }
   318  
   319      var deviceName = 'Unknown'
   320      if (ua.os.name) {
   321          deviceName = ua.os.name
   322          if (deviceName == "Mac OS") {
   323              deviceVendor = 'apple'
   324          } else if (deviceName == "Windows") {
   325              deviceVendor = 'windows'
   326          }
   327          if (ua.device.vendor && ua.device.model) {
   328              deviceName = ua.device.vendor + ' ' + ua.device.model
   329          }
   330      }
   331  
   332      if (ua.os.version) {
   333          deviceName = deviceName + ' (OS Version: ' + ua.os.version + ')'
   334      }
   335  
   336      deviceString = '<div class="timeline-device-os"><span class="fa fa-stack">' +
   337          '<i class="fa fa-' + escapeHtml(deviceIcon) + ' fa-stack-2x"></i>' +
   338          '<i class="fa fa-vendor-icon fa-' + escapeHtml(deviceVendor) + ' fa-stack-1x"></i>' +
   339          '</span> ' + escapeHtml(deviceName) + '</div>'
   340  
   341      detailsString += deviceString
   342  
   343      var deviceBrowser = 'Unknown'
   344      var browserIcon = 'info-circle'
   345      var browserVersion = ''
   346  
   347      if (ua.browser && ua.browser.name) {
   348          deviceBrowser = ua.browser.name
   349          // Handle the "mobile safari" case
   350          deviceBrowser = deviceBrowser.replace('Mobile ', '')
   351          if (deviceBrowser) {
   352              browserIcon = deviceBrowser.toLowerCase()
   353              if (browserIcon == 'ie') browserIcon = 'internet-explorer'
   354          }
   355          browserVersion = '(Version: ' + ua.browser.version + ')'
   356      }
   357  
   358      var browserString = '<div class="timeline-device-browser"><span class="fa fa-stack">' +
   359          '<i class="fa fa-' + escapeHtml(browserIcon) + ' fa-stack-1x"></i></span> ' +
   360          deviceBrowser + ' ' + browserVersion + '</div>'
   361  
   362      detailsString += browserString
   363      detailsString += '</div>'
   364      return detailsString
   365  }
   366  
   367  function renderTimeline(data) {
   368      record = {
   369          "id": data[0],
   370          "first_name": data[2],
   371          "last_name": data[3],
   372          "email": data[4],
   373          "position": data[5],
   374          "status": data[6],
   375          "reported": data[7],
   376          "send_date": data[8]
   377      }
   378      results = '<div class="timeline col-sm-12 well well-lg">' +
   379          '<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) +
   380          '</h6><span class="subtitle">Email: ' + escapeHtml(record.email) +
   381          '<br>Result ID: ' + escapeHtml(record.id) + '</span>' +
   382          '<div class="timeline-graph col-sm-6">'
   383      $.each(campaign.timeline, function (i, event) {
   384          if (!event.email || event.email == record.email) {
   385              // Add the event
   386              results += '<div class="timeline-entry">' +
   387                  '    <div class="timeline-bar"></div>'
   388              results +=
   389                  '    <div class="timeline-icon ' + statuses[event.message].label + '">' +
   390                  '    <i class="fa ' + statuses[event.message].icon + '"></i></div>' +
   391                  '    <div class="timeline-message">' + escapeHtml(event.message) +
   392                  '    <span class="timeline-date">' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm:ss a') + '</span>'
   393              if (event.details) {
   394                  details = JSON.parse(event.details)
   395                  if (event.message == "Clicked Link" || event.message == "Submitted Data") {
   396                      deviceView = renderDevice(details)
   397                      if (deviceView) {
   398                          results += deviceView
   399                      }
   400                  }
   401                  if (event.message == "Submitted Data") {
   402                      results += '<div class="timeline-replay-button"><button onclick="replay(' + i + ')" class="btn btn-success">'
   403                      results += '<i class="fa fa-refresh"></i> Replay Credentials</button></div>'
   404                      results += '<div class="timeline-event-details"><i class="fa fa-caret-right"></i> View Details</div>'
   405                  }
   406                  if (details.payload) {
   407                      results += '<div class="timeline-event-results">'
   408                      results += '    <table class="table table-condensed table-bordered table-striped">'
   409                      results += '        <thead><tr><th>Parameter</th><th>Value(s)</tr></thead><tbody>'
   410                      $.each(Object.keys(details.payload), function (i, param) {
   411                          if (param == "rid") {
   412                              return true;
   413                          }
   414                          results += '    <tr>'
   415                          results += '        <td>' + escapeHtml(param) + '</td>'
   416                          results += '        <td>' + escapeHtml(details.payload[param]) + '</td>'
   417                          results += '    </tr>'
   418                      })
   419                      results += '       </tbody></table>'
   420                      results += '</div>'
   421                  }
   422                  if (details.error) {
   423                      results += '<div class="timeline-event-details"><i class="fa fa-caret-right"></i> View Details</div>'
   424                      results += '<div class="timeline-event-results">'
   425                      results += '<span class="label label-default">Error</span> ' + details.error
   426                      results += '</div>'
   427                  }
   428              }
   429              results += '</div></div>'
   430          }
   431      })
   432      // Add the scheduled send event at the bottom
   433      if (record.status == "Scheduled" || record.status == "Retrying") {
   434          results += '<div class="timeline-entry">' +
   435              '    <div class="timeline-bar"></div>'
   436          results +=
   437              '    <div class="timeline-icon ' + statuses[record.status].label + '">' +
   438              '    <i class="fa ' + statuses[record.status].icon + '"></i></div>' +
   439              '    <div class="timeline-message">' + "Scheduled to send at " + record.send_date + '</span>'
   440      }
   441      results += '</div></div>'
   442      return results
   443  }
   444  
   445  var renderTimelineChart = function (chartopts) {
   446      return Highcharts.chart('timeline_chart', {
   447          chart: {
   448              zoomType: 'x',
   449              type: 'line',
   450              height: "200px"
   451          },
   452          title: {
   453              text: 'Campaign Timeline'
   454          },
   455          xAxis: {
   456              type: 'datetime',
   457              dateTimeLabelFormats: {
   458                  second: '%l:%M:%S',
   459                  minute: '%l:%M',
   460                  hour: '%l:%M',
   461                  day: '%b %d, %Y',
   462                  week: '%b %d, %Y',
   463                  month: '%b %Y'
   464              }
   465          },
   466          yAxis: {
   467              min: 0,
   468              max: 2,
   469              visible: false,
   470              tickInterval: 1,
   471              labels: {
   472                  enabled: false
   473              },
   474              title: {
   475                  text: ""
   476              }
   477          },
   478          tooltip: {
   479              formatter: function () {
   480                  return Highcharts.dateFormat('%A, %b %d %l:%M:%S %P', new Date(this.x)) +
   481                      '<br>Event: ' + this.point.message + '<br>Email: <b>' + this.point.email + '</b>'
   482              }
   483          },
   484          legend: {
   485              enabled: false
   486          },
   487          plotOptions: {
   488              series: {
   489                  marker: {
   490                      enabled: true,
   491                      symbol: 'circle',
   492                      radius: 3
   493                  },
   494                  cursor: 'pointer',
   495              },
   496              line: {
   497                  states: {
   498                      hover: {
   499                          lineWidth: 1
   500                      }
   501                  }
   502              }
   503          },
   504          credits: {
   505              enabled: false
   506          },
   507          series: [{
   508              data: chartopts['data'],
   509              dashStyle: "shortdash",
   510              color: "#cccccc",
   511              lineWidth: 1,
   512              turboThreshold: 0
   513          }]
   514      })
   515  }
   516  
   517  /* Renders a pie chart using the provided chartops */
   518  var renderPieChart = function (chartopts) {
   519      return Highcharts.chart(chartopts['elemId'], {
   520          chart: {
   521              type: 'pie',
   522              events: {
   523                  load: function () {
   524                      var chart = this,
   525                          rend = chart.renderer,
   526                          pie = chart.series[0],
   527                          left = chart.plotLeft + pie.center[0],
   528                          top = chart.plotTop + pie.center[1];
   529                      this.innerText = rend.text(chartopts['data'][0].count, left, top).
   530                      attr({
   531                          'text-anchor': 'middle',
   532                          'font-size': '24px',
   533                          'font-weight': 'bold',
   534                          'fill': chartopts['colors'][0],
   535                          'font-family': 'Helvetica,Arial,sans-serif'
   536                      }).add();
   537                  },
   538                  render: function () {
   539                      this.innerText.attr({
   540                          text: chartopts['data'][0].count
   541                      })
   542                  }
   543              }
   544          },
   545          title: {
   546              text: chartopts['title']
   547          },
   548          plotOptions: {
   549              pie: {
   550                  innerSize: '80%',
   551                  dataLabels: {
   552                      enabled: false
   553                  }
   554              }
   555          },
   556          credits: {
   557              enabled: false
   558          },
   559          tooltip: {
   560              formatter: function () {
   561                  if (this.key == undefined) {
   562                      return false
   563                  }
   564                  return '<span style="color:' + this.color + '">\u25CF</span>' + this.point.name + ': <b>' + this.y + '%</b><br/>'
   565              }
   566          },
   567          series: [{
   568              data: chartopts['data'],
   569              colors: chartopts['colors'],
   570          }]
   571      })
   572  }
   573  
   574  /* Updates the bubbles on the map
   575  
   576  @param {campaign.result[]} results - The campaign results to process
   577  */
   578  var updateMap = function (results) {
   579      if (!map) {
   580          return
   581      }
   582      bubbles = []
   583      $.each(campaign.results, function (i, result) {
   584          // Check that it wasn't an internal IP
   585          if (result.latitude == 0 && result.longitude == 0) {
   586              return true;
   587          }
   588          newIP = true
   589          $.each(bubbles, function (i, bubble) {
   590              if (bubble.ip == result.ip) {
   591                  bubbles[i].radius += 1
   592                  newIP = false
   593                  return false
   594              }
   595          })
   596          if (newIP) {
   597              bubbles.push({
   598                  latitude: result.latitude,
   599                  longitude: result.longitude,
   600                  name: result.ip,
   601                  fillKey: "point",
   602                  radius: 2
   603              })
   604          }
   605      })
   606      map.bubbles(bubbles)
   607  }
   608  
   609  /**
   610   * Creates a status label for use in the results datatable
   611   * @param {string} status 
   612   * @param {moment(datetime)} send_date 
   613   */
   614  function createStatusLabel(status, send_date) {
   615      var label = statuses[status].label || "label-default";
   616      var statusColumn = "<span class=\"label " + label + "\">" + status + "</span>"
   617      // Add the tooltip if the email is scheduled to be sent
   618      if (status == "Scheduled" || status == "Retrying") {
   619          var sendDateMessage = "Scheduled to send at " + send_date
   620          statusColumn = "<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"top\" data-html=\"true\" title=\"" + sendDateMessage + "\">" + status + "</span>"
   621      }
   622      return statusColumn
   623  }
   624  
   625  /* poll - Queries the API and updates the UI with the results
   626   *
   627   * Updates:
   628   * * Timeline Chart
   629   * * Email (Donut) Chart
   630   * * Map Bubbles
   631   * * Datatables
   632   */
   633  function poll() {
   634      api.campaignId.results(campaign.id)
   635          .success(function (c) {
   636              campaign = c
   637              /* Update the timeline */
   638              var timeline_series_data = []
   639              $.each(campaign.timeline, function (i, event) {
   640                  var event_date = moment.utc(event.time).local()
   641                  timeline_series_data.push({
   642                      email: event.email,
   643                      message: event.message,
   644                      x: event_date.valueOf(),
   645                      y: 1,
   646                      marker: {
   647                          fillColor: statuses[event.message].color
   648                      }
   649                  })
   650              })
   651              var timeline_chart = $("#timeline_chart").highcharts()
   652              timeline_chart.series[0].update({
   653                  data: timeline_series_data
   654              })
   655              /* Update the results donut chart */
   656              var email_series_data = {}
   657              // Load the initial data
   658              Object.keys(statusMapping).forEach(function (k) {
   659                  email_series_data[k] = 0
   660              });
   661              $.each(campaign.results, function (i, result) {
   662                  email_series_data[result.status]++;
   663                  if (result.reported) {
   664                      email_series_data['Email Reported']++
   665                  }
   666                  // Backfill status values
   667                  var step = progressListing.indexOf(result.status)
   668                  for (var i = 0; i < step; i++) {
   669                      email_series_data[progressListing[i]]++
   670                  }
   671              })
   672              $.each(email_series_data, function (status, count) {
   673                  var email_data = []
   674                  if (!(status in statusMapping)) {
   675                      return true
   676                  }
   677                  email_data.push({
   678                      name: status,
   679                      y: Math.floor((count / campaign.results.length) * 100),
   680                      count: count
   681                  })
   682                  email_data.push({
   683                      name: '',
   684                      y: 100 - Math.floor((count / campaign.results.length) * 100)
   685                  })
   686                  var chart = $("#" + statusMapping[status] + "_chart").highcharts()
   687                  chart.series[0].update({
   688                      data: email_data
   689                  })
   690              })
   691  
   692              /* Update the datatable */
   693              resultsTable = $("#resultsTable").DataTable()
   694              resultsTable.rows().every(function (i, tableLoop, rowLoop) {
   695                  var row = this.row(i)
   696                  var rowData = row.data()
   697                  var rid = rowData[0]
   698                  $.each(campaign.results, function (j, result) {
   699                      if (result.id == rid) {
   700                          rowData[8] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
   701                          rowData[7] = result.reported
   702                          rowData[6] = result.status
   703                          resultsTable.row(i).data(rowData)
   704                          if (row.child.isShown()) {
   705                              $(row.node()).find("#caret").removeClass("fa-caret-right")
   706                              $(row.node()).find("#caret").addClass("fa-caret-down")
   707                              row.child(renderTimeline(row.data()))
   708                          }
   709                          return false
   710                      }
   711                  })
   712              })
   713              resultsTable.draw(false)
   714              /* Update the map information */
   715              updateMap(campaign.results)
   716              $('[data-toggle="tooltip"]').tooltip()
   717              $("#refresh_message").hide()
   718              $("#refresh_btn").show()
   719          })
   720  }
   721  
   722  function load() {
   723      campaign.id = window.location.pathname.split('/').slice(-1)[0]
   724      var use_map = JSON.parse(localStorage.getItem('gophish.use_map'))
   725      api.campaignId.results(campaign.id)
   726          .success(function (c) {
   727              campaign = c
   728              if (campaign) {
   729                  $("title").text(c.name + " - Gophish")
   730                  $("#loading").hide()
   731                  $("#campaignResults").show()
   732                  // Set the title
   733                  $("#page-title").text("Results for " + c.name)
   734                  if (c.status == "Completed") {
   735                      $('#complete_button')[0].disabled = true;
   736                      $('#complete_button').text('Completed!');
   737                      doPoll = false;
   738                  }
   739                  // Setup viewing the details of a result
   740                  $("#resultsTable").on("click", ".timeline-event-details", function () {
   741                      // Show the parameters
   742                      payloadResults = $(this).parent().find(".timeline-event-results")
   743                      if (payloadResults.is(":visible")) {
   744                          $(this).find("i").removeClass("fa-caret-down")
   745                          $(this).find("i").addClass("fa-caret-right")
   746                          payloadResults.hide()
   747                      } else {
   748                          $(this).find("i").removeClass("fa-caret-right")
   749                          $(this).find("i").addClass("fa-caret-down")
   750                          payloadResults.show()
   751                      }
   752                  })
   753                  // Setup the results table
   754                  resultsTable = $("#resultsTable").DataTable({
   755                      destroy: true,
   756                      "order": [
   757                          [2, "asc"]
   758                      ],
   759                      columnDefs: [{
   760                              orderable: false,
   761                              targets: "no-sort"
   762                          }, {
   763                              className: "details-control",
   764                              "targets": [1]
   765                          }, {
   766                              "visible": false,
   767                              "targets": [0, 8]
   768                          },
   769                          {
   770                              "render": function (data, type, row) {
   771                                  return createStatusLabel(data, row[8])
   772                              },
   773                              "targets": [6]
   774                          },
   775                          {
   776                              className: "text-center",
   777                              "render": function (reported, type, row) {
   778                                  if (type == "display") {
   779                                      if (reported) {
   780                                          return "<i class='fa fa-check-circle text-center text-success'></i>"
   781                                      }
   782                                      return "<i role='button' class='fa fa-times-circle text-center text-muted' onclick='report_mail(\"" + row[0] + "\", \"" + campaign.id + "\");'></i>"
   783                                  }
   784                                  return reported
   785                              },
   786                              "targets": [7]
   787                          }
   788                      ]
   789                  });
   790                  resultsTable.clear();
   791                  var email_series_data = {}
   792                  var timeline_series_data = []
   793                  Object.keys(statusMapping).forEach(function (k) {
   794                      email_series_data[k] = 0
   795                  });
   796                  $.each(campaign.results, function (i, result) {
   797                      resultsTable.row.add([
   798                          result.id,
   799                          "<i id=\"caret\" class=\"fa fa-caret-right\"></i>",
   800                          escapeHtml(result.first_name) || "",
   801                          escapeHtml(result.last_name) || "",
   802                          escapeHtml(result.email) || "",
   803                          escapeHtml(result.position) || "",
   804                          result.status,
   805                          result.reported,
   806                          moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
   807                      ])
   808                      email_series_data[result.status]++;
   809                      if (result.reported) {
   810                          email_series_data['Email Reported']++
   811                      }
   812                      // Backfill status values
   813                      var step = progressListing.indexOf(result.status)
   814                      for (var i = 0; i < step; i++) {
   815                          email_series_data[progressListing[i]]++
   816                      }
   817                  })
   818                  resultsTable.draw();
   819                  // Setup tooltips
   820                  $('[data-toggle="tooltip"]').tooltip()
   821                  // Setup the individual timelines
   822                  $('#resultsTable tbody').on('click', 'td.details-control', function () {
   823                      var tr = $(this).closest('tr');
   824                      var row = resultsTable.row(tr);
   825                      if (row.child.isShown()) {
   826                          // This row is already open - close it
   827                          row.child.hide();
   828                          tr.removeClass('shown');
   829                          $(this).find("i").removeClass("fa-caret-down")
   830                          $(this).find("i").addClass("fa-caret-right")
   831                      } else {
   832                          // Open this row
   833                          $(this).find("i").removeClass("fa-caret-right")
   834                          $(this).find("i").addClass("fa-caret-down")
   835                          row.child(renderTimeline(row.data())).show();
   836                          tr.addClass('shown');
   837                      }
   838                  });
   839                  // Setup the graphs
   840                  $.each(campaign.timeline, function (i, event) {
   841                      if (event.message == "Campaign Created") {
   842                          return true
   843                      }
   844                      var event_date = moment.utc(event.time).local()
   845                      timeline_series_data.push({
   846                          email: event.email,
   847                          message: event.message,
   848                          x: event_date.valueOf(),
   849                          y: 1,
   850                          marker: {
   851                              fillColor: statuses[event.message].color
   852                          }
   853                      })
   854                  })
   855                  renderTimelineChart({
   856                      data: timeline_series_data
   857                  })
   858                  $.each(email_series_data, function (status, count) {
   859                      var email_data = []
   860                      if (!(status in statusMapping)) {
   861                          return true
   862                      }
   863                      email_data.push({
   864                          name: status,
   865                          y: Math.floor((count / campaign.results.length) * 100),
   866                          count: count
   867                      })
   868                      email_data.push({
   869                          name: '',
   870                          y: 100 - Math.floor((count / campaign.results.length) * 100)
   871                      })
   872                      var chart = renderPieChart({
   873                          elemId: statusMapping[status] + '_chart',
   874                          title: status,
   875                          name: status,
   876                          data: email_data,
   877                          colors: [statuses[status].color, '#dddddd']
   878                      })
   879                  })
   880  
   881                  if (use_map) {
   882                      $("#resultsMapContainer").show()
   883                      map = new Datamap({
   884                          element: document.getElementById("resultsMap"),
   885                          responsive: true,
   886                          fills: {
   887                              defaultFill: "#ffffff",
   888                              point: "#283F50"
   889                          },
   890                          geographyConfig: {
   891                              highlightFillColor: "#1abc9c",
   892                              borderColor: "#283F50"
   893                          },
   894                          bubblesConfig: {
   895                              borderColor: "#283F50"
   896                          }
   897                      });
   898                  }
   899                  updateMap(campaign.results)
   900              }
   901          })
   902          .error(function () {
   903              $("#loading").hide()
   904              errorFlash(" Campaign not found!")
   905          })
   906  }
   907  
   908  var setRefresh
   909  
   910  function refresh() {
   911      if (!doPoll) {
   912          return;
   913      }
   914      $("#refresh_message").show()
   915      $("#refresh_btn").hide()
   916      poll()
   917      clearTimeout(setRefresh)
   918      setRefresh = setTimeout(refresh, 60000)
   919  };
   920  
   921  function report_mail(rid, cid) {
   922      Swal.fire({
   923          title: "Are you sure?",
   924          text: "This result will be flagged as reported (RID: " + rid + ")",
   925          type: "question",
   926          animation: false,
   927          showCancelButton: true,
   928          confirmButtonText: "Continue",
   929          confirmButtonColor: "#428bca",
   930          reverseButtons: true,
   931          allowOutsideClick: false,
   932          showLoaderOnConfirm: true
   933      }).then(function (result) {
   934          if (result.value){
   935              api.campaignId.get(cid).success((function(c) {
   936                  report_url = new URL(c.url)
   937                  report_url.pathname = '/report'
   938                  report_url.search = "?rid=" + rid
   939                  $.ajax({
   940                      url: report_url,
   941                      method: "GET",
   942                      success: function(data) {
   943                          refresh();
   944                      }
   945                  });
   946              }));
   947          }
   948      })
   949  }
   950  
   951  $(document).ready(function () {
   952      Highcharts.setOptions({
   953          global: {
   954              useUTC: false
   955          }
   956      })
   957      load();
   958  
   959      // Start the polling loop
   960      setRefresh = setTimeout(refresh, 60000)
   961  })