GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 23c71a...dcb9a9 )
by Richard
11:04
created

ticketer.updatePerformanceStats   D

Complexity

Conditions 9
Paths 120

Size

Total Lines 91

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 91
c 0
b 0
f 0
rs 4.8871
cc 9
nc 120
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
/**
2
 * Created by wechsler on 04/08/15.
3
 */
4
var ticketer = (function() {
5
  'use strict';
6
7
  return {
8
    upcomingTicketTemplate: null,
9
    manageTemplate: null,
10
    songAutocompleteItemTemplate: null,
11
    editTicketTemplate: null,
12
    songDetailsTemplate: null,
13
    appMessageTarget: null,
14
    searchCount: 10,
15
    instrumentOrder: ['V', 'G', 'B', 'D', 'K'],
16
    defaultSongLengthSeconds: 240,
17
    defaultSongIntervalSeconds: 120,
18
    messageTimer: null,
19
20
    /**
21
     * @var {{songInPreview,upcomingCount,iconMapHtml}}
22
     */
23
    displayOptions: {},
24
25
    /**
26
     * List of all performers (objects) who've signed up in this session
27
     */
28
    performers: [],
29
30
    performerExists: function(performerName) {
31
      for (var i = 0; i < this.performers.length; i++) {
32
        if (this.performers[i].performerName.toLowerCase() == performerName.toLowerCase()) {
33
          return true;
34
        }
35
      }
36
      return false;
37
    },
38
39
    addPerformerByName: function(performerName) {
40
      this.performers.push({performerName: performerName});
41
      // Now resort it
42
      this.performers.sort(function(a, b) {
43
        return a.performerName.localeCompare(b.performerName);
44
      });
45
    },
46
47
    /**
48
     * Run the "upcoming" panel
49
     */
50
    go: function() {
51
      this.initTemplates();
52
53
      ticketer.reloadTickets();
54
      setInterval(function() {
55
        ticketer.reloadTickets();
56
      }, 10000);
57
    },
58
59
    /**
60
     * Draw an "upcoming" ticket
61
     * @param ticket {{band}}
62
     * @returns {*}
63
     */
64
    drawDisplayTicket: function(ticket) {
65
      // Sort band into standard order
66
      var unsortedBand = ticket.band;
67
      var sortedBand = {};
68
      for (var i = 0; i < this.instrumentOrder.length; i++) {
69
        var instrument = this.instrumentOrder[i];
70
        if (unsortedBand.hasOwnProperty(instrument)) {
71
          sortedBand[instrument] = unsortedBand[instrument];
72
        }
73
      }
74
      ticket.band = sortedBand;
75
      var ticketParams = {ticket: ticket, icons: this.displayOptions.iconMapHtml};
76
      return this.upcomingTicketTemplate(ticketParams);
77
    },
78
79
    /**
80
     * Draw a "queue management" ticket
81
     * @param ticket
82
     * @returns {*}
83
     */
84
    drawManageableTicket: function(ticket) {
85
      ticket.used = Number(ticket.used); // Force int
86
87
      return this.manageTemplate({ticket: ticket});
88
    },
89
90
    /**
91
     * Reload all tickets on the upcoming page
92
     */
93
    reloadTickets: function() {
94
      var that = this;
95
96
      $.get('/api/next', function(tickets) {
97
98
        var out = '';
99
        for (var i = 0; i < tickets.length; i++) {
100
          var ticket = tickets[i];
101
          out += that.drawDisplayTicket(ticket);
102
        }
103
104
        var target = $('#target');
105
        target.html(out);
106
107
        target.find('.auto-font').each(
108
          function() {
109
            var fixedWidth = $(this).data('fixed-assetwidth');
110
            if (!fixedWidth) {
111
              fixedWidth = 0;
112
            }
113
            fixedWidth = Number(fixedWidth);
114
115
            var spaceUsedByText = (this.scrollWidth - fixedWidth);
116
            var spaceAvailableForText = (this.clientWidth - fixedWidth);
117
            var rawScale = Math.max(spaceUsedByText / spaceAvailableForText, 1);
118
            var scale = 1.05 * rawScale;
119
120
            if (that.displayOptions.adminQueueHasControls && that.displayOptions.isAdmin) {
121
              scale *= 1.25;
122
            }
123
124
            // 1.05 extra scale to fit neatly, fixedWidth is non-scaling elements
125
            var font = Number($(this).css('font-size').replace(/[^0-9]+$/, ''));
126
            $(this).css('font-size', Number(font / scale).toFixed() + 'px');
127
          }
128
        );
129
130
        target.find('.performingButton').click(function() {
131
          var ticketId = $(this).data('ticket-id');
0 ignored issues
show
Unused Code introduced by
The variable ticketId seems to be never used. Consider removing it.
Loading history...
132
          if (window.confirm('Mark song as performing?')) {
133
            that.performButtonCallback(this);
134
          }
135
        });
136
        target.find('.removeButton').click(function() {
137
          var ticketId = $(this).data('ticket-id');
0 ignored issues
show
Unused Code introduced by
The variable ticketId seems to be never used. Consider removing it.
Loading history...
138
          if (window.confirm('Remove song?')) {
139
            that.removeButtonCallback(this);
140
          }
141
        });
142
143
      });
144
    },
145
146
    /**
147
     * Enable queue management ticket buttons in the specified element
148
     * @param topElement
149
     */
150
    enableButtons: function(topElement) {
151
      var that = this;
152
153
      $(topElement).find('.performButton').click(function() {
154
        that.performButtonCallback(this);
155
      });
156
157
      $(topElement).find('.removeButton').click(function() {
158
        that.removeButtonCallback(this);
159
      });
160
161
      $(topElement).find('.editButton').click(function() {
162
        that.editButtonCallback(this);
163
      });
164
    },
165
166
    enableSongSearchBox: function(songSearchInput, songSearchResultsTarget, songClickHandler) {
167
      var that = this;
168
      $(songSearchInput).keyup(
169
        function() {
170
          var songComplete = $(songSearchResultsTarget);
171
          var input = $(this);
172
          var searchString = input.val();
173
          if (searchString.length >= 3) {
174
            $.ajax({
175
              method: 'POST',
176
              data: {
177
                searchString: searchString,
178
                searchCount: that.searchCount
179
              },
180
              url: '/api/songSearch',
181
              /**
182
               * @param {{songs, searchString}} data
183
               */
184
              success: function(data) {
185
                var songs = data.songs;
186
                if (input.val() == data.searchString) {
187
                  // Ensure autocomplete response is still valid for current input value
188
                  var out = '';
189
                  var song; // Used twice below
190
                  for (var i = 0; i < songs.length; i++) {
191
                    song = songs[i];
192
                    out += that.songAutocompleteItemTemplate({song: song});
193
                  }
194
                  songComplete.html(out).show();
195
196
                  // Now attach whole song as data:
197
                  for (i = 0; i < songs.length; i++) {
198
                    song = songs[i];
199
                    var songId = song.id;
200
                    songComplete.find('.acSong[data-song-id=' + songId + ']').data('song', song);
201
                  }
202
203
                  that.enableAcSongSelector(songComplete, songClickHandler);
204
                }
205
              },
206
              error: function(xhr, status, error) {
207
                void(error);
208
              }
209
            });
210
          } else {
211
            songComplete.html('');
212
          }
213
        }
214
      );
215
    },
216
217
    /**
218
     * Completely (re)generate the add ticket control panel and enable its controls
219
     * @param {?number} currentTicket Optional
220
     */
221
    resetEditTicketBlock: function(currentTicket) {
222
      var that = this;
223
      var controlPanelOuter = $('.editTicketOuter');
224
225
      // Current panel state in function scope
226
      var selectedInstrument = 'V';
227
      var currentBand = {};
228
229
      // Reset band to empty (or to ticket band state)
230
      for (var instrumentIdx = 0; instrumentIdx < that.instrumentOrder.length; instrumentIdx++) {
231
        var instrument = that.instrumentOrder[instrumentIdx];
232
        currentBand[instrument] = [];
233
234
        if (currentTicket && currentTicket.band) {
235
          // Ticket.band is a complex datatype. Current band is just one array of names per instrument. Unpack to show.
236
          if (currentTicket.band.hasOwnProperty(instrument)) {
237
            var instrumentPerformerObjects = currentTicket.band[instrument];
238
            for (var pIdx = 0; pIdx < instrumentPerformerObjects.length; pIdx++) {
239
              currentBand[instrument].push(instrumentPerformerObjects[pIdx].performerName);
240
            }
241
          }
242
        }
243
        // Store all instruments as arrays - most can only be single, but vocals is 1..n potentially
244
      }
245
246
      drawEditTicketForm(currentTicket);
247
      // X var editTicketBlock = $('.editTicket'); // only used in inner scope (applyNewSong)
248
249
      // Enable 'Add' button
250
      $('.editTicketButton').click(editTicketCallback);
251
      $('.cancelTicketButton').click(cancelTicketCallback);
252
      $('.removeSongButton').click(removeSong);
253
254
      $('.toggleButton').click(
255
        function() {
256
          var check = $(this).find('input[type=checkbox]');
257
          check.prop('checked', !check.prop('checked'));
258
        }
259
      );
260
261
      // Enable the instrument tabs
262
      var allInstrumentTabs = controlPanelOuter.find('.instrument');
263
264
      allInstrumentTabs.click(
265
        function() {
266
          selectedInstrument = $(this).data('instrumentShortcode');
267
          setActiveTab(selectedInstrument);
268
        }
269
      );
270
271
      var ticketTitleInput = $('.editTicketTitle');
272
273
      // Copy band name into summary area on Enter
274
      ticketTitleInput.keydown(function(e) {
275
        if (e.keyCode == 13) {
276
          updateBandSummary();
277
        }
278
      });
279
280
      $('.newPerformer').keydown(function(e) {
281
        if (e.keyCode == 13) {
282
          var newPerformerInput = $('.newPerformer');
283
          var newName = newPerformerInput.val();
284
          if (newName.trim().length) {
285
            alterInstrumentPerformerList(selectedInstrument, newName, true);
286
          }
287
          newPerformerInput.val('');
288
        }
289
      });
290
291
      // Set up the song search box in this control panel and set the appropriate callback
292
      var songSearchInput = '.addSongTitle';
293
      var songSearchResultsTarget = '.songComplete';
294
295
      this.enableSongSearchBox(songSearchInput, songSearchResultsTarget, applyNewSong);
296
297
      // ************* Inner functions **************
298
      /**
299
       * Switch to the next visible instrument tab
300
       */
301
      function nextInstrumentTab() {
302
        // Find what offset we're at in instrumentOrder
303
        var currentOffset = 0;
304
        for (var i = 0; i < that.instrumentOrder.length; i++) {
305
          if (that.instrumentOrder[i] == selectedInstrument) {
306
            currentOffset = i;
307
          }
308
        }
309
        var nextOffset = currentOffset + 1;
310
        if (nextOffset >= that.instrumentOrder.length) {
311
          nextOffset = 0;
312
        }
313
        var instrument = that.instrumentOrder[nextOffset];
314
        selectedInstrument = instrument; // Reset before we redraw tabs
315
        var newActiveTab = setActiveTab(instrument);
316
317
        // Make sure we switch to a *visible* tab
318
        if (newActiveTab.hasClass('instrumentUnused')) {
319
          nextInstrumentTab();
320
        }
321
      }
322
323
      /**
324
       * (re)Draw the add/edit ticket control panel in the .editTicketOuter element
325
       */
326
      function drawEditTicketForm(ticket) {
327
        var templateParams = {performers: that.performers};
328
        if (ticket) {
329
          templateParams.ticket = ticket;
330
        }
331
        controlPanelOuter.html(that.editTicketTemplate(templateParams));
332
        updateInstrumentTabs();
333
        rebuildPerformerList(controlPanelOuter.find('.performers'));
0 ignored issues
show
Bug introduced by
The call to rebuildPerformerList seems to have too many arguments starting with controlPanelOuter.find(".performers").
Loading history...
334
        if (ticket && ticket.song) {
335
          applyNewSong(ticket.song);
336
        }
337
      }
338
339
      function findPerformerInstrument(name) {
340
        var instrumentPlayers;
341
        for (var instrumentCode in currentBand) {
342
          if (currentBand.hasOwnProperty(instrumentCode)) {
343
            instrumentPlayers = currentBand[instrumentCode];
344
            for (var i = 0; i < instrumentPlayers.length; i++) {
345
              if (instrumentPlayers[i].toUpperCase() == name.toUpperCase()) {
346
                return instrumentCode;
347
              }
348
            }
349
          }
350
        }
351
        return null;
352
      }
353
354
      /**
355
       * Rebuild list of performer buttons according to overall performers list
356
       * and which instruments they are assigned to
357
       */
358
      function rebuildPerformerList() {
359
        var newButton;
360
        var targetElement = controlPanelOuter.find('.performers');
361
        targetElement.text(''); // Remove existing list
362
363
        var lastInitial = '';
364
        var performerCount = that.performers.length;
365
        var letterSpan;
366
        for (var pIdx = 0; pIdx < performerCount; pIdx++) {
367
          var performerName = that.performers[pIdx].performerName;
368
          var performerInstrument = findPerformerInstrument(performerName);
369
          var isPerforming = performerInstrument ? 1 : 0;
370
          var initialLetter = performerName.charAt(0).toUpperCase();
371
          if (lastInitial !== initialLetter) {
372
            if (letterSpan) {
373
              targetElement.append(letterSpan);
374
            }
375
            letterSpan = $('<span class="letterSpan"></span>');
376
            if ((performerCount > 15)) {
377
              letterSpan.append($('<span class="initialLetter">' + initialLetter + '</span>'));
378
            }
379
          }
380
          lastInitial = initialLetter;
381
382
          newButton = $('<span></span>');
383
          newButton.addClass('btn addPerformerButton');
384
          newButton.addClass(isPerforming ? 'btn-primary' : 'btn-default');
385
          if (isPerforming && (performerInstrument !== selectedInstrument)) { // Dim out buttons for other instruments
386
            newButton.attr('disabled', 'disabled');
387
          }
388
          newButton.text(performerName);
389
          newButton.data('selected', isPerforming); // This is where it gets fun - check if user is in band!
390
          letterSpan.append(newButton);
0 ignored issues
show
Bug introduced by
The variable letterSpan seems to not be initialized for all possible execution paths.
Loading history...
391
        }
392
        targetElement.append(letterSpan);
393
394
        // Enable the new buttons
395
        $('.addPerformerButton').click(function() {
396
          var name = $(this).text();
397
          var selected = $(this).data('selected') ? 0 : 1; // Reverse to get new state
398
          if (selected) {
399
            $(this).removeClass('btn-default').addClass('btn-primary');
400
          } else {
401
            $(this).removeClass('btn-primary').addClass('btn-default');
402
          }
403
          $(this).data('selected', selected); // Toggle
404
405
          alterInstrumentPerformerList(selectedInstrument, name, selected);
406
        });
407
      }
408
409
      /**
410
       * Handle click on edit ticket button
411
       */
412
      function editTicketCallback() {
413
        var titleInput = $('.editTicketTitle');
414
        var ticketTitle = titleInput.val();
415
        var songInput = $('.selectedSongId');
416
        var songId = songInput.val();
417
        var privateCheckbox = $('input.privateCheckbox');
418
        var isPrivate = privateCheckbox.is(':checked');
419
        var blockingCheckbox = $('input.blockingCheckbox');
420
        var isBlocked = blockingCheckbox.is(':checked');
421
422
        var data = {
423
          title: ticketTitle,
424
          songId: songId,
425
          band: currentBand,
426
          private: isPrivate,
427
          blocking: isBlocked
428
        };
429
430
        if (currentTicket) {
431
          data.existingTicketId = currentTicket.id;
432
        }
433
434
        that.showAppMessage('Saving ticket');
435
436
        $.ajax({
437
            method: 'POST',
438
            data: data,
439
            url: '/api/saveTicket',
440
            success: function(data, status) {
441
              that.showAppMessage('Saved ticket', 'success');
442
443
              void(status);
444
              var ticketId = data.ticket.id;
445
446
              var ticketBlockSelector = '.ticket[data-ticket-id="' + ticketId + '"]';
447
              var existingTicketBlock = $(ticketBlockSelector);
448
              if (existingTicketBlock.length) {
449
                // Replace existing
450
                existingTicketBlock.after(that.drawManageableTicket(data.ticket));
451
                existingTicketBlock.remove();
452
              } else {
453
                // Append new
454
                $('#target').append(that.drawManageableTicket(data.ticket));
455
              }
456
457
              var ticketBlock = $(ticketBlockSelector);
458
              ticketBlock.data('ticket', data.ticket);
459
              that.enableButtons(ticketBlock);
460
461
              if (data.performers) {
462
                that.performers = data.performers;
463
              }
464
465
              that.updatePerformanceStats();
466
              that.resetEditTicketBlock();
467
468
            },
469
            error: function(xhr, status, error) {
470
              var message = 'Ticket save failed';
471
              that.reportAjaxError(message, xhr, status, error);
472
              void(error);
473
              // FIXME handle error
474
            }
475
          }
476
        );
477
      }
478
479
      function cancelTicketCallback() {
480
        that.resetEditTicketBlock();
481
      }
482
483
      function getTabByInstrument(instrument) {
484
        return controlPanelOuter.find('.instrument[data-instrument-shortcode=' + instrument + ']');
485
      }
486
487
      function setActiveTab(selectedInstrument) {
488
        allInstrumentTabs.removeClass('instrumentSelected');
489
        var selectedTab = getTabByInstrument(selectedInstrument);
490
        selectedTab.addClass('instrumentSelected');
491
        rebuildPerformerList(); // Rebuild in current context
492
        return selectedTab;
493
      }
494
495
      function updateBandSummary() {
496
        var bandName = $('.editTicketTitle').val();
497
        var members = [];
498
        for (var instrument in currentBand) {
499
          if (currentBand.hasOwnProperty(instrument)) {
500
            for (var i = 0; i < currentBand[instrument].length; i++) {
501
              members.push(currentBand[instrument][i]);
502
            }
503
          }
504
        }
505
        var memberList = members.join(', ');
506
        var summaryHtml = (bandName ? bandName + '<br />' : '') + memberList;
507
        $('.selectedBand').html(summaryHtml);
508
      }
509
510
      function updateInstrumentTabs() {
511
        var performersSpan;
512
        var performerString;
513
514
        for (var iIdx = 0; iIdx < that.instrumentOrder.length; iIdx++) {
515
          var instrument = that.instrumentOrder[iIdx];
516
517
          performersSpan = controlPanelOuter
518
            .find('.instrument[data-instrument-shortcode=' + instrument + ']')
519
            .find('.instrumentPerformer');
520
521
          performerString = currentBand[instrument].join(', ');
522
          if (!performerString) {
523
            performerString = '<i>Needed</i>';
524
          }
525
          performersSpan.html(performerString);
526
        }
527
528
        updateBandSummary();
529
      }
530
531
      /**
532
       * Handle performer add / remove by performer button / text input
533
       * @param instrument
534
       * @param changedPerformer
535
       * @param isAdd
536
       */
537
      function alterInstrumentPerformerList(instrument, changedPerformer, isAdd) {
538
        var currentInstrumentPerformers = currentBand[selectedInstrument];
539
540
        var newInstrumentPerformers = [];
541
        for (var i = 0; i < currentInstrumentPerformers.length; i++) {
542
          var member = currentInstrumentPerformers[i].trim(); // Trim only required when we draw data from manual input
543
          if (member.length) {
544
            if (member.toUpperCase() != changedPerformer.toUpperCase()) {
545
              // If it's not the name on our button, no change
546
              newInstrumentPerformers.push(member);
547
            }
548
          }
549
        }
550
551
        if (isAdd) { // If we've just selected a new user, append them
552
          newInstrumentPerformers.push(changedPerformer);
553
          if (!that.performerExists(changedPerformer)) {
554
            that.addPerformerByName(changedPerformer);
555
          }
556
        }
557
558
        currentBand[selectedInstrument] = newInstrumentPerformers;
559
        // Now update band with new performers of this instrument
560
561
        updateInstrumentTabs();
562
        rebuildPerformerList();
563
564
        if (newInstrumentPerformers.length) { // If we've a performer for this instrument, skip to next
565
          nextInstrumentTab();
566
        }
567
568
      }
569
570
      /**
571
       *
572
       * @param {{id, title, artist, hasKeys, hasHarmony}} song
573
       */
574
      function applyNewSong(song) {
575
        var selectedId = song.id;
576
        var selectedSong = song.artist + ': ' + song.title;
577
578
        var removeSongButton = $('.removeSongButton');
579
        removeSongButton.removeClass('hidden');
580
581
        // Perform actions with selected song
582
        var editTicketBlock = $('.editTicket'); // Temp hack, should already be in scope?
583
584
        editTicketBlock.find('input.selectedSongId').val(selectedId);
585
        editTicketBlock.find('.selectedSong').text(selectedSong);
586
        var keysTab = controlPanelOuter.find('.instrumentKeys');
587
        if (song.hasKeys) {
588
          keysTab.removeClass('instrumentUnused');
589
        } else {
590
          keysTab.addClass('instrumentUnused');
591
          // Also uncheck any performer for instrument (allow use elsewhere)
592
          currentBand.K = [];
593
          keysTab.find('.instrumentPerformer').html('<i>Needed</i>');
594
          rebuildPerformerList();
595
        }
596
      }
597
598
      function removeSong() {
599
        var editTicketBlock = $('.editTicket'); // Temp hack, should already be in scope?
600
        editTicketBlock.find('input.selectedSongId').val(0);
601
        editTicketBlock.find('.selectedSong').text('');
602
        $(songSearchInput).val('');
603
        var removeSongButton = $('.removeSongButton');
604
        removeSongButton.hide();
605
606
      }
607
    },
608
609
    manage: function(tickets) {
610
      var that = this;
611
      this.appMessageTarget = $('#appMessages');
612
      this.initTemplates();
613
      var ticket, ticketBlock; // For loop iterations
614
615
      var out = '';
616
      for (var i = 0; i < tickets.length; i++) {
617
        ticket = tickets[i];
618
        out += that.drawManageableTicket(ticket);
619
        ticketBlock = $('.ticket[data-ticket-id="' + ticket.id + '"]');
620
        ticketBlock.data('ticket', ticket);
621
      }
622
      $('#target').html(out);
623
624
      // Find new tickets (now they're DOM'd) and add data to them
625
      for (i = 0; i < tickets.length; i++) {
626
        ticket = tickets[i];
627
        ticketBlock = $('.ticket[data-ticket-id="' + ticket.id + '"]');
628
        ticketBlock.data('ticket', ticket);
629
      }
630
631
      var $sortContainer = $('.sortContainer');
632
      $sortContainer.sortable({
633
        axis: 'y',
634
        update: function(event, ui) {
635
          void(event);
636
          void(ui);
637
          that.ticketOrderChanged();
638
        }
639
      }).disableSelection().css('cursor', 'move');
640
641
      this.enableButtons($sortContainer);
642
643
      this.updatePerformanceStats();
644
645
      this.resetEditTicketBlock();
646
647
    },
648
649
    initSearchPage: function() {
650
      var that = this;
651
      this.initTemplates();
652
      this.enableSongSearchBox('.searchString', '.songComplete', that.searchPageSongSelectionClick);
653
    },
654
655
    initTemplates: function() {
656
      var that = this;
657
658
      // CommaList = each, with commas joining. Returns value at t as tuple {k,v}
659
      // TODO work out how to use options to more closely mimic 'each'
660
      Handlebars.registerHelper('commalist', function(context, options) {
661
        var retList = [];
662
663
        for (var key in context) {
664
          if (context.hasOwnProperty(key)) {
665
            retList.push(options.fn({k: key, v: context[key]}));
666
          }
667
        }
668
669
        return retList.join(', ');
670
      });
671
672
      Handlebars.registerHelper('instrumentIcon', function(instrumentCode) {
673
        var icon = '<span class="instrumentTextIcon">' + instrumentCode + '</span>';
674
        if (that.displayOptions.hasOwnProperty('iconMapHtml')) {
675
          if (that.displayOptions.iconMapHtml.hasOwnProperty(instrumentCode)) {
676
            icon = that.displayOptions.iconMapHtml[instrumentCode];
677
          }
678
        }
679
        return new Handlebars.SafeString(icon);
680
      });
681
682
      Handlebars.registerHelper('durationToMS', function(duration) {
683
        var seconds = (duration % 60);
684
        if (seconds < 10) {
685
          seconds = '0' + seconds;
686
        }
687
        return Math.floor(duration / 60) + ':' + seconds;
688
      });
689
690
      Handlebars.registerHelper('gameList', function(song) {
691
        var all = [];
692
        if (song.inRb3) {
693
          all.push('RB3');
694
        }
695
        if (song.inRb4) {
696
          all.push('RB4');
697
        }
698
        return all.join(', ');
699
      });
700
701
      this.manageTemplate = Handlebars.compile(
702
        '<div class="ticket well well-sm {{#if ticket.used}}used{{/if}}' +
703
        ' {{#if ticket.song.inRb3}}rb3ticket{{/if}} {{#if ticket.song.inRb4}}rb4ticket{{/if}}' +
704
        ' {{#if ticket.band.K}}withKeys{{/if}}"' +
705
        ' data-ticket-id="{{ ticket.id }}">' +
706
        '        <div class="pull-right">' +
707
        '        <div class="gameMarker gameMarkerRb3">{{#if ticket.song.inRb3}}RB3{{/if}}</div>' +
708
        '        <div class="gameMarker gameMarkerRb4">{{#if ticket.song.inRb4}}RB4{{/if}}</div>' +
709
        '        <button class="btn btn-primary performButton" data-ticket-id="{{ ticket.id }}">Performing</button>' +
710
        '        <button class="btn btn-danger removeButton" data-ticket-id="{{ ticket.id }}">Remove</button>' +
711
        '        <button class="btn editButton" data-ticket-id="{{ ticket.id }}">' +
712
        '<span class="fa fa-edit" title="Edit"></span>' +
713
        '</button>' +
714
        '        </div>' +
715
        '<div class="ticketOrder">' +
716
        '<div class="ticketOrdinal"></div>' +
717
        '<div class="ticketTime"></div>' +
718
        '</div>' +
719
        '<div class="ticketId">' +
720
        '<span class="fa fa-ticket"></span> {{ ticket.id }}</div> ' +
721
        '<div class="ticketMeta">' +
722
        '<div class="blocking">' +
723
        '{{#if ticket.blocking}}<span class="fa fa-hand-stop-o" title="Blocking" />{{/if}}' +
724
        '</div>' +
725
        '<div class="private">' +
726
        '{{#if ticket.private}}<span class="fa fa-eye-slash" title="Private" />{{/if}}' +
727
        '</div>' +
728
        '</div>' +
729
        '<div class="pendingSong">' +
730
        '<span class="fa fa-group"></span> ' +
731
732
        // Display performers with metadata if valid, else just the band title.
733
        /*
734
         '{{#if ticket.performers}}' +
735
         '{{#each ticket.performers}}' +
736
         '<span class="performer performerDoneCount{{songsDone}}" ' +
737
         'data-performer-id="{{performerId}}"> {{performerName}} ' +
738
         ' (<span class="songsDone">{{songsDone}}</span>/<span class="songsTotal">{{songsTotal}}</span>)' +
739
         '</span>' +
740
         '{{/each}}' +
741
         '{{else}}' +
742
         '{{ ticket.title }}' +
743
         '{{/if}}' +
744
         */
745
746
        // Display performers with metadata if valid, else just the band title.
747
        '{{#if ticket.band}}' +
748
        '{{#each ticket.band}} <span class="instrumentTextIcon">{{ @key }}</span>' +
749
        '{{#each this}}' +
750
        '<span class="performer performerDoneCount{{songsDone}}" ' +
751
        'data-performer-id="{{performerId}}" data-performer-name="{{performerName}}"> {{performerName}} ' +
752
        ' (<span class="songsDone">{{songsDone}}</span>/<span class="songsTotal">{{songsTotal}}</span>)' +
753
        '</span>' +
754
        '{{/each}}' +
755
        '{{/each}}' +
756
        '{{/if}}' +
757
758
        '{{#if ticket.title}}' +
759
        '<span class="ticketTitleIcon"><span class="instrumentTextIcon">Title</span> {{ ticket.title }}</span>' +
760
        '{{/if}}' +
761
762
        '{{#if ticket.song}}<br /><span class="fa fa-music"></span> {{ticket.song.artist}}: ' +
763
        '{{ticket.song.title}}' +
764
        ' ({{gameList ticket.song}})' +
765
        '{{/if}}' +
766
        '</div>' +
767
        '</div>'
768
      );
769
770
      this.upcomingTicketTemplate = Handlebars.compile(
771
        '<div class="ticket well ' +
772
        (this.displayOptions.songInPreview ? 'withSong' : 'noSong') +
773
        ' ' +
774
        (this.displayOptions.title ? 'withTitle' : 'noTitle') + // TODO is this used (correctly)?
775
        '" data-ticket-id="{{ ticket.id }}">' +
776
777
        (this.displayOptions.adminQueueHasControls && this.displayOptions.isAdmin ?
778
        '<div class="ticketAdminControls">' +
779
        '<button class="btn btn-sm btn-primary performingButton" data-ticket-id="{{ ticket.id }}">Performing</button>' +
780
        '<button class="btn btn-sm btn-danger removeButton" data-ticket-id="{{ ticket.id }}">Remove</button>' +
781
        '</div>'
782
          : '') +
783
784
785
        '<div class="ticketMeta">' +
786
        '<div class="blocking">' +
787
        '{{#if ticket.blocking}}<span class="fa fa-hand-stop-o" title="Blocking" />{{/if}}' +
788
        '</div>' +
789
        '<div class="private">' +
790
        '{{#if ticket.private}}<span class="fa fa-eye-slash" title="Private" />{{/if}}' +
791
        '</div>' +
792
        '</div>' +
793
794
        '  <div class="ticket-inner">' +
795
        '    <p class="text-center band auto-font">{{ticket.title}}</p>' +
796
        '    <p class="performers auto-font" data-fixed-assetwidth="200">' +
797
        '{{#each ticket.band}}' +
798
        '<span class="instrumentTag">{{instrumentIcon @key}}</span>' +
799
        '<span class="instrumentPerformers">{{#commalist this}}{{v.performerName}}{{/commalist}}</span>' +
800
        '{{/each}}' +
801
        '    </p>' +
802
        (this.displayOptions.songInPreview ?
803
        '{{#if ticket.song}}<p class="text-center song auto-font">' +
804
        '{{ticket.song.artist}}: {{ticket.song.title}}' +
805
        ' ({{gameList ticket.song}})' +
806
        '</p>{{/if}}' : '') +
807
        '        </div>' +
808
        '</div>  '
809
      );
810
811
      this.songAutocompleteItemTemplate = Handlebars.compile(
812
        '<div class="acSong" data-song-id="{{ song.id }}">' +
813
        '        <div class="acSong-inner {{#if song.queued}}queued{{/if}}">' +
814
        '        {{song.artist}}: {{song.title}} ({{gameList song}}) ' +
815
        '        </div>' +
816
        '</div>  '
817
      );
818
819
      this.editTicketTemplate = Handlebars.compile(
820
        '<div class="editTicket well">' +
821
        '<div class="pull-right editTicketButtons">' +
822
        '<button class="blockingButton btn btn-warning toggleButton">' +
823
        '<span class="fa fa-hand-stop-o" /> Blocking ' +
824
        ' <input type="checkbox" class="blockingCheckbox" ' +
825
        '  {{#if ticket}}{{# if ticket.blocking }}checked="checked"{{/if}}{{/if}} /></button>' +
826
        '<button class="privacyButton btn btn-warning toggleButton">' +
827
        '<span class="fa fa-eye-slash" /> Private ' +
828
        ' <input type="checkbox" class="privateCheckbox" ' +
829
        '  {{#if ticket}}{{# if ticket.private }}checked="checked"{{/if}}{{/if}} /></button>' +
830
        '<button class="editTicketButton btn btn-success">' +
831
        '<span class="fa fa-save" /> Save</button>' +
832
        '<button class="cancelTicketButton btn">' +
833
        '<span class="fa fa-close" /> Cancel</button>' +
834
        '</div>' +
835
836
        '{{# if ticket}}' +
837
        '<h3 class="editTicketHeader">Edit ticket <span class="fa fa-ticket"></span> {{ticket.id}}</h3>' +
838
        '{{else}}<h3 class="newTicketHeader">Add new ticket</h3>{{/if}}' +
839
840
        '<div class="editTicketInner">' +
841
        '<div class="editTicketSong">' +
842
        '<div class="ticketAspectSummary"><span class="fa fa-music fa-2x" title="Song"></span> ' +
843
        '<input type="hidden" class="selectedSongId"/> ' +
844
        '<span class="selectedSong">{{#if ticket}}{{#if ticket.song}}{{ticket.song.artist}}: ' +
845
        '{{ticket.song.title}}{{/if}}{{/if}}</span>' +
846
847
        '<button title="Remove song from ticket" ' +
848
        'class="btn removeSongButton{{#unless ticket}}{{#unless ticket.song}} hidden{{/unless}}{{/unless}}">' +
849
        ' <span class="fa fa-ban" />' +
850
        '</button>' +
851
852
        '</div>' +
853
        '<div class="input-group input-group">' +
854
        '<span class="input-group-addon" id="search-addon1"><span class="fa fa-search"></span> </span>' +
855
        '<input class="addSongTitle form-control" placeholder="Search song or use code"/>' +
856
        '</div>' +
857
858
        '<div class="songCompleteOuter">' +
859
        '<div class="songComplete"></div>' +
860
        '</div>' + // /songCompleteOuter
861
        '</div>' + // /editTicketSong
862
863
        '<div class="editTicketBandColumn">' +
864
865
        '<div class="ticketAspectSummary"><span class="fa fa-group fa-2x pull-left" title="Performers"></span>' +
866
        '<span class="selectedBand">{{#if ticket}}{{ticket.title}}{{/if}}</span>' +
867
        '</div>' + // /ticketAspectSummary
868
869
        '<div class="input-group">' +
870
        '<span class="input-group-addon" id="group-addon-band"><span class="fa fa-pencil"></span> </span>' +
871
        '<input class="editTicketTitle form-control" placeholder="Band name or message (optional)"' +
872
        ' value="{{#if ticket}}{{ticket.title}}{{/if}}"/>' +
873
        '</div>' + // /input-group
874
875
        '<div class="bandControls">' +
876
        '<div class="bandTabsOuter">' +
877
        '<div class="instruments">' +
878
        ' <div class="instrument instrumentVocals instrumentSelected" data-instrument-shortcode="V">' +
879
        '  <div class="instrumentName">Vocals</div>' +
880
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
881
        ' </div>' +
882
        ' <div class="instrument instrumentGuitar" data-instrument-shortcode="G">' +
883
        '  <div class="instrumentName">Guitar</div>' +
884
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
885
        ' </div>' +
886
        ' <div class="instrument instrumentBass" data-instrument-shortcode="B">' +
887
        '  <div class="instrumentName">Bass</div>' +
888
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
889
        ' </div>' +
890
        ' <div class="instrument instrumentDrums" data-instrument-shortcode="D">' +
891
        '  <div class="instrumentName">Drums</div>' +
892
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
893
        ' </div>' +
894
        ' <div class="instrument instrumentKeys instrumentUnused" data-instrument-shortcode="K">' +
895
        '  <div class="instrumentName">Keyboard</div>' +
896
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
897
        ' </div>' +
898
        '</div>' + // /instruments
899
        '<div class="performerSelect">' +
900
        '<div class="input-group input-group">' +
901
        '<span class="input-group-addon" id="group-addon-performer"><span class="fa fa-plus"></span> </span>' +
902
        '<input class="newPerformer form-control" placeholder="New performer (Firstname Initial)"/>' +
903
        '</div>' +
904
905
        '<div class="performers"></div>' +
906
        '</div>' + // /performerSelect
907
        '</div>' + // /bandTabsOuter
908
        '</div>' + // /bandControls
909
        '</div>' + // /editTicketBandColumn
910
        '<div class="clearfix"></div>' + // Clear after editTicketBandColumn
911
        '</div>' + // /editTicketInner
912
        '</div>' // /editTicket
913
      );
914
915
      this.songDetailsTemplate = Handlebars.compile(
916
        '<div class="songDetails"><h3>{{song.artist}}: {{song.title}}</h3>' +
917
        '<table>' +
918
        '<tr><th>Duration</th><td>{{durationToMS song.duration}}</td></tr> ' +
919
        '<tr><th>Code</th><td>{{song.codeNumber}}</td></tr> ' +
920
        '<tr><th>Harmony? </th><td>{{#if song.hasHarmony}}Yes{{else}}No{{/if}}</td></tr> ' +
921
        '<tr><th>Keys?</th><td>{{#if song.hasKeys}}Yes{{else}}No{{/if}}</td></tr> ' +
922
        '<tr><th>Games</th><td>{{#if song.inRb3}}RB3{{/if}} {{#if song.inRb4}}RB4{{/if}}</td></tr> ' +
923
        '<tr><th>Source</th><td>{{song.source}}</td></tr> ' +
924
        '</table>' +
925
        '</div>'
926
      );
927
928
    },
929
930
    ticketOrderChanged: function() {
931
      var that = this;
932
      var idOrder = [];
933
      $('#target').find('.ticket').each(
934
        function() {
935
          var ticketBlock = $(this);
936
          var ticketId = ticketBlock.data('ticketId');
937
          idOrder.push(ticketId);
938
        }
939
      );
940
941
      that.showAppMessage('Updating ticket order');
942
      $.ajax({
943
        method: 'POST',
944
        data: {
945
          idOrder: idOrder
946
        },
947
        url: '/api/newOrder',
948
        success: function(data, status) {
0 ignored issues
show
Unused Code introduced by
The parameter data is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter status is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
949
          // FIXME check return status
950
          that.showAppMessage('Saved revised order', 'success');
951
        },
952
        error: function(xhr, status, error) {
953
          var message = 'Failed to save revised order';
954
          that.reportAjaxError(message, xhr, status, error);
955
        }
956
      });
957
958
      this.updatePerformanceStats();
959
    },
960
961
    performButtonCallback: function(button) {
962
      var that = this;
963
964
      button = $(button);
965
      var ticketId = button.data('ticketId');
966
      that.showAppMessage('Mark ticket used');
967
      $.ajax({
968
          method: 'POST',
969
          data: {
970
            ticketId: ticketId
971
          },
972
          url: '/api/useTicket',
973
          success: function(data, status) {
974
            that.showAppMessage('Marked ticket used', 'success');
975
            void(data);
976
            void(status);
977
            var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
978
            ticketBlock.addClass('used');
979
            // TicketBlock.append(' (done)');
980
981
            // Fixme receive updated ticket info from API
982
            var ticket = ticketBlock.data('ticket');
983
            ticket.startTime = Date.now() / 1000;
984
            ticket.used = true;
985
            ticketBlock.data('ticket', ticket);
986
987
            that.updatePerformanceStats();
988
          },
989
          error: function(xhr, status, error) {
990
            var message = 'Failed to mark ticket used';
991
            that.reportAjaxError(message, xhr, status, error);
992
          }
993
        }
994
      );
995
    },
996
997
    removeButtonCallback: function(button) {
998
      var that = this;
999
      button = $(button);
1000
      var ticketId = button.data('ticketId');
1001
      that.showAppMessage('Deleting ticket');
1002
      $.ajax({
1003
          method: 'POST',
1004
          data: {
1005
            ticketId: ticketId
1006
          },
1007
          url: '/api/deleteTicket',
1008
          success: function(data, status) {
1009
            that.showAppMessage('Deleted ticket', 'success');
1010
            void(status);
1011
            var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1012
            ticketBlock.remove();
1013
            that.updatePerformanceStats();
1014
          },
1015
          error: function(xhr, status, error) {
1016
            var message = 'Failed to deleted ticket';
1017
            that.reportAjaxError(message, xhr, status, error);
1018
          }
1019
        }
1020
      );
1021
    },
1022
1023
    editButtonCallback: function(button) {
1024
      var that = this;
1025
      button = $(button);
1026
      var ticketId = button.data('ticketId');
1027
1028
      var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1029
      var ticket = ticketBlock.data('ticket'); // TODO possibly load from ajax instead?
1030
      that.resetEditTicketBlock(ticket);
1031
    },
1032
1033
    enableAcSongSelector: function(outerElement, songClickHandler) {
1034
      var that = this;
1035
      outerElement.find('.acSong').click(
1036
        function() {
1037
          // Find & decorate clicked element
1038
          outerElement.find('.acSong').removeClass('selected');
1039
          $(this).addClass('selected');
1040
1041
          var song = $(this).data('song');
1042
          songClickHandler.call(that, song); // Run in 'that' context
1043
        }
1044
      );
1045
    },
1046
1047
1048
    searchPageSongSelectionClick: function(song) {
1049
      var target = $('#searchTarget');
1050
      target.html(this.songDetailsTemplate({song: song}));
1051
    },
1052
1053
    updatePerformanceStats: function() {
1054
      var that = this;
1055
      var performed = {};
1056
      var lastByPerformer = {};
1057
      var ticketOrdinal = 1;
1058
      var ticketTime = null;
1059
1060
      var pad = function(number) {
1061
        if (number < 10) {
1062
          return '0' + number;
1063
        }
1064
        return number;
1065
      };
1066
1067
      // First check number of songs performed before this one
1068
      var sortContainer = $('.sortContainer');
1069
      var lastSongDuration = null;
1070
      var lastTicketNoSong = true;
1071
1072
      var nthUnused = 1;
1073
1074
      sortContainer.find('.ticket').each(function() {
1075
        var realTime;
1076
        var ticketId = $(this).data('ticket-id');
1077
        var ticketData = $(this).data('ticket');
1078
1079
        if (ticketData.startTime) {
1080
          realTime = new Date(ticketData.startTime * 1000);
1081
        }
1082
1083
        $(this).removeClass('shown');
1084
1085
        if (!(ticketData.used || ticketData.private)) {
1086
          if (nthUnused <= that.displayOptions.upcomingCount) {
1087
            $(this).addClass('shown');
1088
          }
1089
          nthUnused++;
1090
        }
1091
1092
        $(this).find('.ticketOrdinal').text('# ' + ticketOrdinal);
1093
        // Fixme read ticketStart from data if present
1094
        if (realTime) {
1095
          ticketTime = realTime;
1096
        } else if (ticketTime) {
1097
          // If last song had an implicit time, add defaultSongOffsetMs to it and assume next song starts then
1098
          // If this is in the past, assume it starts now!
1099
          var songOffsetMs;
1100
          if (lastTicketNoSong) {
1101
            songOffsetMs = that.defaultSongIntervalSeconds * 1000;
1102
            // Could just be a message, could be a reset / announcement, so treat as an interval only
1103
          } else if (lastSongDuration) {
1104
            songOffsetMs = (that.defaultSongIntervalSeconds + lastSongDuration) * 1000;
1105
          } else {
1106
            songOffsetMs = (that.defaultSongIntervalSeconds + that.defaultSongLengthSeconds) * 1000;
1107
          }
1108
          ticketTime = new Date(Math.max(ticketTime.getTime() + songOffsetMs, Date.now()));
1109
        } else {
1110
          ticketTime = new Date();
1111
        }
1112
        $(this).find('.ticketTime').text(pad(ticketTime.getHours()) + ':' + pad(ticketTime.getMinutes()));
1113
1114
        // Update performer stats (done/total)
1115
        $(this).find('.performer').each(function() {
1116
          var performerId = $(this).data('performer-id');
1117
          var performerName = $(this).data('performer-name');
1118
          if (!performed.hasOwnProperty(performerId)) {
1119
            performed[performerId] = 0;
1120
          }
1121
          $(this).find('.songsDone').text(performed[performerId]);
1122
1123
          $(this).removeClass(
1124
            function(i, oldClass) {
1125
              void(i);
1126
              var classes = oldClass.split(' ');
1127
              var toRemove = [];
1128
              for (var cIdx = 0; cIdx < classes.length; cIdx++) {
1129
                if (classes[cIdx].match(/^performerDoneCount/)) {
1130
                  toRemove.push(classes[cIdx]);
1131
                }
1132
              }
1133
              return toRemove.join(' ');
1134
            }
1135
          ).addClass('performerDoneCount' + performed[performerId]);
1136
          performed[performerId]++;
1137
1138
          // Now check proximity of last song by this performer
1139
          if (lastByPerformer.hasOwnProperty(performerId)) {
1140
            var distance = ticketOrdinal - lastByPerformer[performerId].idx;
1141
            $(this).removeClass('proximityIssue');
1142
            $(this).removeClass('proximityIssue1');
1143
            if ((distance < 3) && (performerName.charAt(0) !== '?')) {
1144
              $(this).addClass('proximityIssue');
1145
              if (distance === 1) {
1146
                $(this).addClass('proximityIssue1');
1147
              }
1148
            }
1149
          } else {
1150
            // Make sure they've not got a proximity marker on a ticket that's been dragged to top
1151
            $(this).removeClass('proximityIssue');
1152
          }
1153
          lastByPerformer[performerId] = {idx: ticketOrdinal, ticketId: ticketId};
1154
        });
1155
        ticketOrdinal++;
1156
1157
        if (ticketData.song) {
1158
          lastSongDuration = ticketData.song.duration;
1159
          lastTicketNoSong = false;
1160
        } else {
1161
          lastSongDuration = 0;
1162
          lastTicketNoSong = true;
1163
        } // Set non-song ticket to minimum duration
1164
      });
1165
1166
      // Then update all totals
1167
      sortContainer.find('.performer').each(function() {
1168
        var performerId = $(this).data('performer-id');
1169
        var totalPerformed = performed[performerId];
1170
        $(this).find('.songsTotal').text(totalPerformed);
1171
      });
1172
    },
1173
1174
    /**
1175
     * Show a message in the defined appMessageTarget (f any)
1176
     *
1177
     * @param message {string} Message to show (replaces any other)
1178
     * @param [className='info'] {string} 'info','success','warning','danger'
0 ignored issues
show
Documentation Bug introduced by
The parameter [className='info'] does not exist. Did you maybe mean className instead?
Loading history...
1179
     */
1180
    showAppMessage: function(message, className) {
1181
      var that = this;
1182
      if (this.messageTimer) {
1183
        clearTimeout(this.messageTimer);
1184
      }
1185
1186
      this.messageTimer = setTimeout(function() {
1187
        that.appMessageTarget.html('');
1188
      }, 5000);
1189
1190
      if (!className) {
1191
        className = 'info';
1192
      }
1193
      if (this.appMessageTarget) {
1194
        var block = $('<div />').addClass('alert alert-' + className);
1195
        block.text(message);
1196
        this.appMessageTarget.html('').append(block);
1197
      }
1198
    },
1199
1200
    ucFirst: function(string) {
1201
      return string.charAt(0).toUpperCase() + string.slice(1);
1202
    },
1203
1204
    reportAjaxError: function(message, xhr, status, error) {
1205
      this.showAppMessage(
1206
        this.ucFirst(status) + ': ' + message + ': ' + error + ', ' + xhr.responseJSON.error,
1207
        'danger'
1208
      );
1209
    },
1210
1211
    checkRemoteRedirect: function() {
1212
      window.setInterval(function() {
1213
          $.get('/api/remotesRedirect', function(newPath) {
1214
            if (newPath && (newPath !== window.location.pathname)) {
1215
              window.location.pathname = newPath;
1216
            }
1217
          });
1218
        },
1219
        10000);
1220
    }
1221
  };
1222
}());