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.
Test Failed
Push — master ( 89a088...49191e )
by Richard
02:51
created

ticketer.resetEditTicketBlock   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
c 2
b 0
f 0
nc 1
nop 1
dl 0
loc 33
rs 8.8571
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
    manageInstrumentTabsTemplate: null,
14
    appMessageTarget: null,
15
    searchCount: 10,
16
    instrumentOrder: ['V', 'G', 'B', 'D', 'K'],
17
    defaultSongLengthSeconds: 240,
18
    defaultSongIntervalSeconds: 120,
19
    messageTimer: null,
20
21
    /**
22
     * @var {{songInPreview,upcomingCount,iconMapHtml}}
23
     */
24
    displayOptions: {},
25
26
    /**
27
     * List of all performers (objects) who've signed up in this session
28
     */
29
    performers: [],
30
31
    /**
32
     * List of all platform names in the system
33
     */
34
    platforms: [],
35
36
    performerExists: function(performerName) {
37
      for (var i = 0; i < this.performers.length; i++) {
38
        if (this.performers[i].performerName.toLowerCase() == performerName.toLowerCase()) {
39
          return true;
40
        }
41
      }
42
      return false;
43
    },
44
45
    addPerformerByName: function(performerName) {
46
      this.performers.push({performerName: performerName});
47
      // Now resort it
48
      this.performers.sort(function(a, b) {
49
        return a.performerName.localeCompare(b.performerName);
50
      });
51
    },
52
53
    /**
54
     * Run the "upcoming" panel
55
     */
56
    go: function() {
57
      this.initTemplates();
58
59
      ticketer.reloadTickets();
60
      setInterval(function() {
61
        ticketer.reloadTickets();
62
      }, 10000);
63
    },
64
65
    /**
66
     * Draw an "upcoming" ticket
67
     * @param ticket {{band}}
68
     * @returns {*}
69
     */
70
    drawDisplayTicket: function(ticket) {
71
      // Sort band into standard order
72
      var unsortedBand = ticket.band;
73
      var sortedBand = {};
74
      for (var i = 0; i < this.instrumentOrder.length; i++) {
75
        var instrument = this.instrumentOrder[i];
76
        if (unsortedBand.hasOwnProperty(instrument)) {
77
          sortedBand[instrument] = unsortedBand[instrument];
78
        }
79
      }
80
      ticket.band = sortedBand;
81
      var ticketParams = {ticket: ticket, icons: this.displayOptions.iconMapHtml};
82
      return this.upcomingTicketTemplate(ticketParams);
83
    },
84
85
    /**
86
     * Draw a "queue management" ticket
87
     * @param ticket
88
     * @returns {*}
89
     */
90
    drawManageableTicket: function(ticket) {
91
      ticket.used = Number(ticket.used); // Force int
92
93
      return this.manageTemplate({ticket: ticket});
94
    },
95
96
    /**
97
     * Reload all tickets on the upcoming page
98
     */
99
    reloadTickets: function() {
100
      var that = this;
101
102
      $.get('/api/next', function(tickets) {
103
104
        var out = '';
105
        for (var i = 0; i < tickets.length; i++) {
106
          var ticket = tickets[i];
107
          out += that.drawDisplayTicket(ticket);
108
        }
109
110
        var target = $('#target');
111
        target.html(out);
112
113
        target.find('.auto-font').each(
114
          function() {
115
            var fixedWidth = $(this).data('fixed-assetwidth');
116
            if (!fixedWidth) {
117
              fixedWidth = 0;
118
            }
119
            fixedWidth = Number(fixedWidth);
120
121
            var spaceUsedByText = (this.scrollWidth - fixedWidth);
122
            var spaceAvailableForText = (this.clientWidth - fixedWidth);
123
            var rawScale = Math.max(spaceUsedByText / spaceAvailableForText, 1);
124
            var scale = 1.05 * rawScale;
125
126
            if (that.displayOptions.adminQueueHasControls && that.displayOptions.isAdmin) {
127
              scale *= 1.25;
128
            }
129
130
            // 1.05 extra scale to fit neatly, fixedWidth is non-scaling elements
131
            var font = Number($(this).css('font-size').replace(/[^0-9]+$/, ''));
132
            $(this).css('font-size', Number(font / scale).toFixed() + 'px');
133
          }
134
        );
135
136
        target.find('.performingButton').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('Mark song as performing?')) {
139
            that.performButtonCallback(this);
140
          }
141
        });
142
        target.find('.removeButton').click(function() {
143
          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...
144
          if (window.confirm('Remove song?')) {
145
            that.removeButtonCallback(this);
146
          }
147
        });
148
149
      });
150
    },
151
152
    /**
153
     * Enable queue management ticket buttons in the specified element
154
     * @param topElement
155
     */
156
    enableButtons: function(topElement) {
157
      var that = this;
158
159
      $(topElement).find('.performButton').click(function() {
160
        that.performButtonCallback(this);
161
      });
162
163
      $(topElement).find('.removeButton').click(function() {
164
        that.removeButtonCallback(this);
165
      });
166
167
      $(topElement).find('.editButton').click(function() {
168
        that.editButtonCallback(this);
169
      });
170
    },
171
172
    /**
173
     * Activate the search box at the given location
174
     *
175
     * @param {string} songSearchInput Input field identifier
176
     * @param {string} songSearchResultsTarget Container for output list
177
     * @param {function} songClickHandler Function to call when a listed song is clicked
178
     */
179
    enableSongSearchBox: function(songSearchInput, songSearchResultsTarget, songClickHandler) {
180
      var that = this;
181
      $(songSearchInput).keyup(
182
        function() {
183
          var songComplete = $(songSearchResultsTarget);
184
          var input = $(this);
185
          var searchString = input.val();
186
          if (searchString.length >= 3) {
187
            $.ajax({
188
              method: 'POST',
189
              data: {
190
                searchString: searchString,
191
                searchCount: that.searchCount
192
              },
193
              url: '/api/songSearch',
194
              /**
195
               * @param {{songs, searchString}} data
196
               */
197
              success: function(data) {
198
                var songs = data.songs;
199
                if (input.val() == data.searchString) {
200
                  // Ensure autocomplete response is still valid for current input value
201
                  var out = '';
202
                  var song; // Used twice below
203
                  for (var i = 0; i < songs.length; i++) {
204
                    song = songs[i];
205
                    out += that.songAutocompleteItemTemplate({song: song});
206
                  }
207
                  songComplete.html(out).show();
208
209
                  // Now attach whole song as data:
210
                  for (i = 0; i < songs.length; i++) {
211
                    song = songs[i];
212
                    var songId = song.id;
213
                    songComplete.find('.acSong[data-song-id=' + songId + ']').data('song', song);
214
                  }
215
216
                  that.enableAcSongSelector(songComplete, songClickHandler);
217
                }
218
              },
219
              error: function(xhr, status, error) {
220
                void(error);
221
              }
222
            });
223
          } else {
224
            songComplete.html('');
225
          }
226
        }
227
      );
228
    },
229
230
    /**
231
     * Completely (re)generate the add ticket control panel and enable its controls
232
     * @param {?number} currentTicket Optional
233
     */
234
    resetEditTicketBlock: function(currentTicket) {
235
      var that = this;
236
      var controlPanelOuter = $('.editTicketOuter');
237
238
      // Current panel state in function scope
239
      var selectedInstrument = 'V';
240
      var currentBand = {};
241
242
      // Reset band to empty (or to ticket band state)
243
      for (var instrumentIdx = 0; instrumentIdx < that.instrumentOrder.length; instrumentIdx++) {
244
        var instrument = that.instrumentOrder[instrumentIdx];
245
        currentBand[instrument] = [];
246
247
        if (currentTicket && currentTicket.band) {
248
          // Ticket.band is a complex datatype. Current band is just one array of names per instrument. Unpack to show.
249
          if (currentTicket.band.hasOwnProperty(instrument)) {
250
            var instrumentPerformerObjects = currentTicket.band[instrument];
251
            for (var pIdx = 0; pIdx < instrumentPerformerObjects.length; pIdx++) {
252
              currentBand[instrument].push(instrumentPerformerObjects[pIdx].performerName);
253
            }
254
          }
255
        }
256
        // Store all instruments as arrays - most can only be single, but vocals is 1..n potentially
257
      }
258
259
      drawEditTicketForm(currentTicket);
260
      // X var editTicketBlock = $('.editTicket'); // only used in inner scope (applyNewSong)
261
262
      // Enable 'Add' button
263
      $('.editTicketButton').click(editTicketCallback);
264
      $('.cancelTicketButton').click(cancelTicketCallback);
265
      $('.removeSongButton').click(removeSong);
266
267
      $('.toggleButton').click(
268
        function() {
269
          var check = $(this).find('input[type=checkbox]');
270
          check.prop('checked', !check.prop('checked'));
271
        }
272
      );
273
274
      var ticketTitleInput = $('.editTicketTitle');
275
276
      // Copy band name into summary area on Enter
277
      ticketTitleInput.keydown(function(e) {
278
        if (e.keyCode == 13) {
279
          updateBandSummary();
280
        }
281
      });
282
283
      $('.newPerformer').keydown(function(e) {
284
        if (e.keyCode == 13) {
285
          var newPerformerInput = $('.newPerformer');
286
          var newName = newPerformerInput.val();
287
          if (newName.trim().length) {
288
            alterInstrumentPerformerList(selectedInstrument, newName, true);
289
          }
290
          newPerformerInput.val('');
291
        }
292
      });
293
294
      // Set up the song search box in this control panel and set the appropriate callback
295
      var songSearchInput = '.addSongTitle';
296
      var songSearchResultsTarget = '.songComplete';
297
298
      this.enableSongSearchBox(songSearchInput, songSearchResultsTarget, applyNewSong);
299
300
      // ************* Inner functions **************
301
      /**
302
       * Switch to the next visible instrument tab
303
       */
304
      function nextInstrumentTab() {
305
        // Find what offset we're at in instrumentOrder
306
        var currentOffset = 0;
307
        for (var i = 0; i < that.instrumentOrder.length; i++) {
308
          if (that.instrumentOrder[i] == selectedInstrument) {
309
            currentOffset = i;
310
          }
311
        }
312
        var nextOffset = currentOffset + 1;
313
        if (nextOffset >= that.instrumentOrder.length) {
314
          nextOffset = 0;
315
        }
316
        var instrument = that.instrumentOrder[nextOffset];
317
        selectedInstrument = instrument; // Reset before we redraw tabs
318
        var newActiveTab = setActiveTab(instrument);
319
320
        // Make sure we switch to a *visible* tab
321
        if (newActiveTab.hasClass('instrumentUnused')) {
322
          nextInstrumentTab();
323
        }
324
      }
325
326
      /**
327
       * (re)Draw the add/edit ticket control panel in the .editTicketOuter element
328
       */
329
      function drawEditTicketForm(ticket) {
330
        var templateParams = {performers: that.performers};
331
        if (ticket) {
332
          templateParams.ticket = ticket;
333
        }
334
        controlPanelOuter.html(that.editTicketTemplate(templateParams));
335
        if (ticket && ticket.song) {
336
          applyNewSong(ticket.song);
337
        }
338
        updateInstrumentTabPerformers();
339
        rebuildPerformerList();
340
      }
341
342
      /**
343
       * Return the instrument abbreviation played by a given performer name
344
       *
345
       * @param name
346
       * @returns {*}
347
       */
348
      function findPerformerInstrument(name) {
349
        var instrumentPlayers;
350
        for (var instrumentCode in currentBand) {
351
          if (currentBand.hasOwnProperty(instrumentCode)) {
352
            instrumentPlayers = currentBand[instrumentCode];
353
            for (var i = 0; i < instrumentPlayers.length; i++) {
354
              if (instrumentPlayers[i].toUpperCase() == name.toUpperCase()) {
355
                return instrumentCode;
356
              }
357
            }
358
          }
359
        }
360
        return null;
361
      }
362
363
      /**
364
       * Rebuild list of performer buttons according to overall performers list
365
       * and which instruments they are assigned to
366
       */
367
      function rebuildPerformerList() {
368
        var newButton;
369
        var targetElement = controlPanelOuter.find('.performers');
370
        targetElement.text(''); // Remove existing list
371
372
        var lastInitial = '';
373
        var performerCount = that.performers.length;
374
        var letterSpan;
375
        for (var pIdx = 0; pIdx < performerCount; pIdx++) {
376
          var performerName = that.performers[pIdx].performerName;
377
          var performerInstrument = findPerformerInstrument(performerName);
378
          var isPerforming = performerInstrument ? 1 : 0;
379
          var initialLetter = performerName.charAt(0).toUpperCase();
380
          if (lastInitial !== initialLetter) {
381
            if (letterSpan) {
382
              targetElement.append(letterSpan);
383
            }
384
            letterSpan = $('<span class="letterSpan"></span>');
385
            if ((performerCount > 15)) {
386
              letterSpan.append($('<span class="initialLetter">' + initialLetter + '</span>'));
387
            }
388
          }
389
          lastInitial = initialLetter;
390
391
          newButton = $('<span></span>');
392
          newButton.addClass('btn addPerformerButton');
393
          newButton.addClass(isPerforming ? 'btn-primary' : 'btn-default');
394
          if (isPerforming && (performerInstrument !== selectedInstrument)) { // Dim out buttons for other instruments
395
            newButton.attr('disabled', 'disabled');
396
          }
397
          newButton.text(performerName);
398
          newButton.data('selected', isPerforming); // This is where it gets fun - check if user is in band!
399
          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...
400
        }
401
        targetElement.append(letterSpan);
402
403
        // Enable the new buttons
404
        $('.addPerformerButton').click(function() {
405
          var name = $(this).text();
406
          var selected = $(this).data('selected') ? 0 : 1; // Reverse to get new state
407
          if (selected) {
408
            $(this).removeClass('btn-default').addClass('btn-primary');
409
          } else {
410
            $(this).removeClass('btn-primary').addClass('btn-default');
411
          }
412
          $(this).data('selected', selected); // Toggle
413
414
          alterInstrumentPerformerList(selectedInstrument, name, selected);
415
        });
416
      }
417
418
      /**
419
       * Handle click on edit ticket button
420
       */
421
      function editTicketCallback() {
422
        var titleInput = $('.editTicketTitle');
423
        var ticketTitle = titleInput.val();
424
        var songInput = $('.selectedSongId');
425
        var songId = songInput.val();
426
        var privateCheckbox = $('input.privateCheckbox');
427
        var isPrivate = privateCheckbox.is(':checked');
428
        var blockingCheckbox = $('input.blockingCheckbox');
429
        var isBlocked = blockingCheckbox.is(':checked');
430
431
        var data = {
432
          title: ticketTitle,
433
          songId: songId,
434
          band: currentBand,
435
          private: isPrivate,
436
          blocking: isBlocked
437
        };
438
439
        if (currentTicket) {
440
          data.existingTicketId = currentTicket.id;
441
        }
442
443
        that.showAppMessage('Saving ticket');
444
445
        $.ajax({
446
            method: 'POST',
447
            data: data,
448
            url: '/api/saveTicket',
449
            success: function(data, status) {
450
              that.showAppMessage('Saved ticket', 'success');
451
452
              void(status);
453
              var ticketId = data.ticket.id;
454
455
              var ticketBlockSelector = '.ticket[data-ticket-id="' + ticketId + '"]';
456
              var existingTicketBlock = $(ticketBlockSelector);
457
              if (existingTicketBlock.length) {
458
                // Replace existing
459
                existingTicketBlock.after(that.drawManageableTicket(data.ticket));
460
                existingTicketBlock.remove();
461
              } else {
462
                // Append new
463
                $('#target').append(that.drawManageableTicket(data.ticket));
464
              }
465
466
              var ticketBlock = $(ticketBlockSelector);
467
              ticketBlock.data('ticket', data.ticket);
468
              that.enableButtons(ticketBlock);
469
470
              if (data.performers) {
471
                that.performers = data.performers;
472
              }
473
474
              that.updatePerformanceStats();
475
              that.resetEditTicketBlock();
476
477
            },
478
            error: function(xhr, status, error) {
479
              var message = 'Ticket save failed';
480
              that.reportAjaxError(message, xhr, status, error);
481
              void(error);
482
              // FIXME handle error
483
            }
484
          }
485
        );
486
      }
487
488
      function cancelTicketCallback() {
489
        that.resetEditTicketBlock();
490
      }
491
492
      /**
493
       * Return tab corresponding to a given instrument abbreviation
494
       *
495
       * @param {string} instrument Abbreviation
496
       * @returns {jQuery}
497
       */
498
      function getTabByInstrument(instrument) {
499
        return controlPanelOuter.find('.instrument[data-instrument-shortcode=' + instrument + ']');
500
      }
501
502
      /**
503
       * Set the tab for the specified instrument abbreviation as active
504
       *
505
       * @param selectedInstrument
506
       * @returns {jQuery}
507
       */
508
      function setActiveTab(selectedInstrument) {
509
        console.log(['setActiveTab', selectedInstrument]);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
510
        var allInstrumentTabs = controlPanelOuter.find('.instrument');
511
        allInstrumentTabs.removeClass('instrumentSelected');
512
        var selectedTab = getTabByInstrument(selectedInstrument);
513
        selectedTab.addClass('instrumentSelected');
514
        rebuildPerformerList(); // Rebuild in current context
515
        return selectedTab;
516
      }
517
518
      /**
519
       * Update the band summary line in the manage area
520
       */
521
      function updateBandSummary() {
522
        var bandName = $('.editTicketTitle').val();
523
        var members = [];
524
        for (var instrument in currentBand) {
525
          if (currentBand.hasOwnProperty(instrument)) {
526
            for (var i = 0; i < currentBand[instrument].length; i++) {
527
              members.push(currentBand[instrument][i]);
528
            }
529
          }
530
        }
531
        var memberList = members.join(', ');
532
        var summaryHtml = (bandName ? bandName + '<br />' : '') + memberList;
533
        $('.selectedBand').html(summaryHtml);
534
      }
535
536
      /**
537
       * Update all instrument tabs with either performer names or 'needed' note
538
       */
539
      function updateInstrumentTabPerformers() {
540
        var performersSpan;
541
        var performerString;
542
543
        for (var iIdx = 0; iIdx < that.instrumentOrder.length; iIdx++) {
544
          var instrument = that.instrumentOrder[iIdx];
545
546
          performersSpan = controlPanelOuter
547
            .find('.instrument[data-instrument-shortcode=' + instrument + ']')
548
            .find('.instrumentPerformer');
549
550
          performerString = currentBand[instrument].join(', ');
551
          if (!performerString) {
552
            performerString = '<i>Needed</i>';
553
          }
554
          performersSpan.html(performerString);
555
        }
556
557
        //TODO re-enable tab clicks
558
        updateBandSummary();
559
      }
560
561
      /**
562
       * Handle performer add / remove by performer button / text input
563
       * @param instrument
564
       * @param changedPerformer
565
       * @param isAdd
566
       */
567
      function alterInstrumentPerformerList(instrument, changedPerformer, isAdd) {
568
        var currentInstrumentPerformers = currentBand[selectedInstrument];
569
570
        var newInstrumentPerformers = [];
571
        for (var i = 0; i < currentInstrumentPerformers.length; i++) {
572
          var member = currentInstrumentPerformers[i].trim(); // Trim only required when we draw data from manual input
573
          if (member.length) {
574
            if (member.toUpperCase() != changedPerformer.toUpperCase()) {
575
              // If it's not the name on our button, no change
576
              newInstrumentPerformers.push(member);
577
            }
578
          }
579
        }
580
581
        if (isAdd) { // If we've just selected a new user, append them
582
          newInstrumentPerformers.push(changedPerformer);
583
          if (!that.performerExists(changedPerformer)) {
584
            that.addPerformerByName(changedPerformer);
585
          }
586
        }
587
588
        currentBand[selectedInstrument] = newInstrumentPerformers;
589
        // Now update band with new performers of this instrument
590
591
        updateInstrumentTabPerformers();
592
        rebuildPerformerList();
593
594
        if (newInstrumentPerformers.length) { // If we've a performer for this instrument, skip to next
595
          nextInstrumentTab();
596
        }
597
598
      }
599
600
      /**
601
       * Handle click on a song in manage page search results
602
       *
603
       * @param {{id, title, artist, instruments}} song
604
       */
605
      function applyNewSong(song) {
606
        var selectedId = song.id;
607
        var selectedSong = song.artist + ': ' + song.title;
608
609
        var removeSongButton = $('.removeSongButton');
610
        removeSongButton.removeClass('hidden');
611
612
        // Perform actions with selected song
613
        var editTicketBlock = $('.editTicket'); // Temp hack, should already be in scope?
614
615
        editTicketBlock.find('input.selectedSongId').val(selectedId);
616
        editTicketBlock.find('.selectedSong').text(selectedSong);
617
618
        // Redraw instrument tabs according to current songs
619
        var instrumentDiv = controlPanelOuter.find('.instruments');
620
        instrumentDiv.html(that.manageInstrumentTabsTemplate(song.instruments));
621
        instrumentDiv.find('.instrument').removeClass('instrumentSelected');
622
        instrumentDiv.find('.instrument:first').addClass('instrumentSelected');
623
624
        // Enable the instrument tabs
625
        // var allInstrumentTabs = controlPanelOuter.find('.instrument');
626
627
        instrumentDiv.find('.instrument').click(
628
          function() {
629
            selectedInstrument = $(this).data('instrumentShortcode');
630
            console.log(['640',selectedInstrument]);
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
631
            setActiveTab(selectedInstrument);
632
          }
633
        );
634
635
        updateInstrumentTabPerformers();
636
        rebuildPerformerList();
637
      }
638
639
      function removeSong() {
640
        var editTicketBlock = $('.editTicket'); // Temp hack, should already be in scope?
641
        editTicketBlock.find('input.selectedSongId').val(0);
642
        editTicketBlock.find('.selectedSong').text('');
643
        $(songSearchInput).val('');
644
        var removeSongButton = $('.removeSongButton');
645
        removeSongButton.hide();
646
647
      }
648
    },
649
650
    manage: function(tickets) {
651
      var that = this;
652
      this.appMessageTarget = $('#appMessages');
653
      this.initTemplates();
654
      var ticket, ticketBlock; // For loop iterations
655
656
      var out = '';
657
      for (var i = 0; i < tickets.length; i++) {
658
        ticket = tickets[i];
659
        out += that.drawManageableTicket(ticket);
660
        ticketBlock = $('.ticket[data-ticket-id="' + ticket.id + '"]');
661
        ticketBlock.data('ticket', ticket);
662
      }
663
      $('#target').html(out);
664
665
      // Find new tickets (now they're DOM'd) and add data to them
666
      for (i = 0; i < tickets.length; i++) {
667
        ticket = tickets[i];
668
        ticketBlock = $('.ticket[data-ticket-id="' + ticket.id + '"]');
669
        ticketBlock.data('ticket', ticket);
670
      }
671
672
      var $sortContainer = $('.sortContainer');
673
      $sortContainer.sortable({
674
        axis: 'y',
675
        update: function(event, ui) {
676
          void(event);
677
          void(ui);
678
          that.ticketOrderChanged();
679
        }
680
      }).disableSelection().css('cursor', 'move');
681
682
      this.enableButtons($sortContainer);
683
684
      this.updatePerformanceStats();
685
686
      this.resetEditTicketBlock();
687
688
    },
689
690
    initSearchPage: function() {
691
      var that = this;
692
      this.initTemplates();
693
      this.enableSongSearchBox('.searchString', '.songComplete', that.searchPageSongSelectionClick);
694
    },
695
696
    initTemplates: function() {
697
      var that = this;
698
699
      // CommaList = each, with commas joining. Returns value at t as tuple {k,v}
700
      // "The options hash contains a function (options.fn) that behaves like a normal compiled Handlebars template."
701
      // If called without inner template, options.fn is not populated
702
      Handlebars.registerHelper('commalist', function(context, options) {
703
        var retList = [];
704
705
        for (var key in context) {
706
          if (context.hasOwnProperty(key)) {
707
            retList.push(options.fn ? options.fn({k: key, v: context[key]}) : context[key]);
708
          }
709
        }
710
711
        return retList.join(', ');
712
      });
713
714
      Handlebars.registerHelper('instrumentIcon', function(instrumentCode) {
715
        var icon = '<span class="instrumentTextIcon">' + instrumentCode + '</span>';
716
        if (that.displayOptions.hasOwnProperty('iconMapHtml')) {
717
          if (that.displayOptions.iconMapHtml.hasOwnProperty(instrumentCode)) {
718
            icon = that.displayOptions.iconMapHtml[instrumentCode];
719
          }
720
        }
721
        return new Handlebars.SafeString(icon);
722
      });
723
724
      Handlebars.registerHelper('durationToMS', function(duration) {
725
        var seconds = (duration % 60);
726
        if (seconds < 10) {
727
          seconds = '0' + seconds;
728
        }
729
        return Math.floor(duration / 60) + ':' + seconds;
730
      });
731
732
      Handlebars.registerHelper('gameList', function(song) {
733
        return song.platforms.join(', ');
734
      });
735
736
      Handlebars.registerHelper('ifContains', function(haystack, needle, options) {
737
        return (haystack.indexOf(needle) === -1) ? '' : options.fn(this);
738
      });
739
740
      this.manageTemplate = Handlebars.compile(
741
        '<div class="ticket well well-sm {{#if ticket.used}}used{{/if}}' +
742
        ' {{#each ticket.song.platforms }}platform{{ this }} {{/each}}' +
743
        ' {{#if ticket.band.K}}withKeys{{/if}}"' +
744
        ' data-ticket-id="{{ ticket.id }}">' +
745
        '        <div class="pull-right">' +
746
        (function() {
747
          var s = '';
748
          for (var i = 0; i < that.platforms.length; i++) {
749
            var p = that.platforms[i];
750
            s += '<div class="gameMarker gameMarker' + p + '">' +
751
              '{{#ifContains ticket.song.platforms "' + p + '" }}' + p + '{{/ifContains}}</div>';
752
          }
753
          return s;
754
        })() +
755
        '        <button class="btn btn-primary performButton" data-ticket-id="{{ ticket.id }}">Performing</button>' +
756
        '        <button class="btn btn-danger removeButton" data-ticket-id="{{ ticket.id }}">Remove</button>' +
757
        '        <button class="btn editButton" data-ticket-id="{{ ticket.id }}">' +
758
        '<span class="fa fa-edit" title="Edit"></span>' +
759
        '</button>' +
760
        '        </div>' +
761
        '<div class="ticketOrder">' +
762
        '<div class="ticketOrdinal"></div>' +
763
        '<div class="ticketTime"></div>' +
764
        '</div>' +
765
        '<div class="ticketId">' +
766
        '<span class="fa fa-ticket"></span> {{ ticket.id }}</div> ' +
767
        '<div class="ticketMeta">' +
768
        '<div class="blocking">' +
769
        '{{#if ticket.blocking}}<span class="fa fa-hand-stop-o" title="Blocking" />{{/if}}' +
770
        '</div>' +
771
        '<div class="private">' +
772
        '{{#if ticket.private}}<span class="fa fa-eye-slash" title="Private" />{{/if}}' +
773
        '</div>' +
774
        '</div>' +
775
        '<div class="pendingSong">' +
776
        '<span class="fa fa-group"></span> ' +
777
778
        // Display performers with metadata if valid, else just the band title.
779
        /*
780
         '{{#if ticket.performers}}' +
781
         '{{#each ticket.performers}}' +
782
         '<span class="performer performerDoneCount{{songsDone}}" ' +
783
         'data-performer-id="{{performerId}}"> {{performerName}} ' +
784
         ' (<span class="songsDone">{{songsDone}}</span>/<span class="songsTotal">{{songsTotal}}</span>)' +
785
         '</span>' +
786
         '{{/each}}' +
787
         '{{else}}' +
788
         '{{ ticket.title }}' +
789
         '{{/if}}' +
790
         */
791
792
        // Display performers with metadata if valid, else just the band title.
793
        '{{#if ticket.band}}' +
794
        '{{#each ticket.band}} <span class="instrumentTextIcon">{{ @key }}</span>' +
795
        '{{#each this}}' +
796
        '<span class="performer performerDoneCount{{songsDone}}" ' +
797
        'data-performer-id="{{performerId}}" data-performer-name="{{performerName}}"> {{performerName}} ' +
798
        ' (<span class="songsDone">{{songsDone}}</span>/<span class="songsTotal">{{songsTotal}}</span>)' +
799
        '</span>' +
800
        '{{/each}}' +
801
        '{{/each}}' +
802
        '{{/if}}' +
803
804
        '{{#if ticket.title}}' +
805
        '<span class="ticketTitleIcon"><span class="instrumentTextIcon">Title</span> {{ ticket.title }}</span>' +
806
        '{{/if}}' +
807
808
        '{{#if ticket.song}}<br /><span class="fa fa-music"></span> {{ticket.song.artist}}: ' +
809
        '{{ticket.song.title}}' +
810
        ' ({{gameList ticket.song}})' +
811
        '{{/if}}' +
812
        '</div>' +
813
        '</div>'
814
      );
815
816
      this.upcomingTicketTemplate = Handlebars.compile(
817
        '<div class="ticket well ' +
818
        (this.displayOptions.songInPreview ? 'withSong' : 'noSong') +
819
        ' ' +
820
        (this.displayOptions.title ? 'withTitle' : 'noTitle') + // TODO is this used (correctly)?
821
        '" data-ticket-id="{{ ticket.id }}">' +
822
823
        (this.displayOptions.adminQueueHasControls && this.displayOptions.isAdmin ?
824
          '<div class="ticketAdminControls">' +
825
          '<button class="btn btn-sm btn-primary performingButton"' +
826
          ' data-ticket-id="{{ ticket.id }}">Performing</button>' +
827
          '<button class="btn btn-sm btn-danger removeButton" data-ticket-id="{{ ticket.id }}">Remove</button>' +
828
          '</div>'
829
          : '') +
830
831
832
        '<div class="ticketMeta">' +
833
        '<div class="blocking">' +
834
        '{{#if ticket.blocking}}<span class="fa fa-hand-stop-o" title="Blocking" />{{/if}}' +
835
        '</div>' +
836
        '<div class="private">' +
837
        '{{#if ticket.private}}<span class="fa fa-eye-slash" title="Private" />{{/if}}' +
838
        '</div>' +
839
        '</div>' +
840
841
        '  <div class="ticket-inner">' +
842
        '    <p class="text-center band auto-font">{{ticket.title}}</p>' +
843
        '    <p class="performers auto-font" data-fixed-assetwidth="200">' +
844
        '{{#each ticket.band}}' +
845
        '<span class="instrumentTag">{{instrumentIcon @key}}</span>' +
846
        '<span class="instrumentPerformers">{{#commalist this}}{{v.performerName}}{{/commalist}}</span>' +
847
        '{{/each}}' +
848
        '    </p>' +
849
        (this.displayOptions.songInPreview ?
850
          '{{#if ticket.song}}<p class="text-center song auto-font">' +
851
          '{{ticket.song.artist}}: {{ticket.song.title}}' +
852
          ' ({{gameList ticket.song}})' +
853
          '</p>{{/if}}' : '') +
854
        '        </div>' +
855
        '</div>  '
856
      );
857
858
      this.songAutocompleteItemTemplate = Handlebars.compile(
859
        '<div class="acSong" data-song-id="{{ song.id }}">' +
860
        '        <div class="acSong-inner {{#if song.queued}}queued{{/if}}">' +
861
        '        {{song.artist}}: {{song.title}} ({{gameList song}}) ' +
862
        '        </div>' +
863
        '</div>  '
864
      );
865
866
      this.editTicketTemplate = Handlebars.compile(
867
        '<div class="editTicket well">' +
868
        '<div class="pull-right editTicketButtons">' +
869
        '<button class="blockingButton btn btn-warning toggleButton">' +
870
        '<span class="fa fa-hand-stop-o" /> Blocking ' +
871
        ' <input type="checkbox" class="blockingCheckbox" ' +
872
        '  {{#if ticket}}{{# if ticket.blocking }}checked="checked"{{/if}}{{/if}} /></button>' +
873
        '<button class="privacyButton btn btn-warning toggleButton">' +
874
        '<span class="fa fa-eye-slash" /> Private ' +
875
        ' <input type="checkbox" class="privateCheckbox" ' +
876
        '  {{#if ticket}}{{# if ticket.private }}checked="checked"{{/if}}{{/if}} /></button>' +
877
        '<button class="editTicketButton btn btn-success">' +
878
        '<span class="fa fa-save" /> Save</button>' +
879
        '<button class="cancelTicketButton btn">' +
880
        '<span class="fa fa-close" /> Cancel</button>' +
881
        '</div>' +
882
883
        '{{# if ticket}}' +
884
        '<h3 class="editTicketHeader">Edit ticket <span class="fa fa-ticket"></span> {{ticket.id}}</h3>' +
885
        '{{else}}<h3 class="newTicketHeader">Add new ticket</h3>{{/if}}' +
886
887
        '<div class="editTicketInner">' +
888
        '<div class="editTicketSong">' +
889
        '<div class="ticketAspectSummary"><span class="fa fa-music fa-2x" title="Song"></span> ' +
890
        '<input type="hidden" class="selectedSongId"/> ' +
891
        '<span class="selectedSong">{{#if ticket}}{{#if ticket.song}}{{ticket.song.artist}}: ' +
892
        '{{ticket.song.title}}{{/if}}{{/if}}</span>' +
893
894
        '<button title="Remove song from ticket" ' +
895
        'class="btn removeSongButton{{#unless ticket}}{{#unless ticket.song}} hidden{{/unless}}{{/unless}}">' +
896
        ' <span class="fa fa-ban" />' +
897
        '</button>' +
898
899
        '</div>' +
900
        '<div class="input-group input-group">' +
901
        '<span class="input-group-addon" id="search-addon1"><span class="fa fa-search"></span> </span>' +
902
        '<input class="addSongTitle form-control" placeholder="Search song or use code"/>' +
903
        '</div>' +
904
905
        '<div class="songCompleteOuter">' +
906
        '<div class="songComplete"></div>' +
907
        '</div>' + // /songCompleteOuter
908
        '</div>' + // /editTicketSong
909
910
        '<div class="editTicketBandColumn">' +
911
912
        '<div class="ticketAspectSummary"><span class="fa fa-group fa-2x pull-left" title="Performers"></span>' +
913
        '<span class="selectedBand">{{#if ticket}}{{ticket.title}}{{/if}}</span>' +
914
        '</div>' + // /ticketAspectSummary
915
916
        '<div class="input-group">' +
917
        '<span class="input-group-addon" id="group-addon-band"><span class="fa fa-pencil"></span> </span>' +
918
        '<input class="editTicketTitle form-control" placeholder="Band name or message (optional)"' +
919
        ' value="{{#if ticket}}{{ticket.title}}{{/if}}"/>' +
920
        '</div>' + // /input-group
921
922
        '<div class="bandControls">' +
923
        '<div class="bandTabsOuter">' +
924
        '<div class="instruments">' +
925
        '</div>' + // /instruments
926
        '<div class="performerSelect">' +
927
        '<div class="input-group input-group">' +
928
        '<span class="input-group-addon" id="group-addon-performer"><span class="fa fa-plus"></span> </span>' +
929
        '<input class="newPerformer form-control" placeholder="New performer (Firstname Initial)"/>' +
930
        '</div>' +
931
932
        '<div class="performers"></div>' +
933
        '</div>' + // /performerSelect
934
        '</div>' + // /bandTabsOuter
935
        '</div>' + // /bandControls
936
        '</div>' + // /editTicketBandColumn
937
        '<div class="clearfix"></div>' + // Clear after editTicketBandColumn
938
        '</div>' + // /editTicketInner
939
        '</div>' // /editTicket
940
      );
941
942
      this.manageInstrumentTabsTemplate = Handlebars.compile(
943
        '{{#each this}}' +
944
        ' <div class="instrument instrument{{ this.abbreviation }}" data-instrument-shortcode="{{ this.abbreviation }}">' +
945
        '  <div class="instrumentName">{{ this.name }}</div>' +
946
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
947
        ' </div>' +
948
        '{{/each}}'
949
      );
950
951
      this.songDetailsTemplate = Handlebars.compile(
952
        '<div class="songDetails"><h3>{{song.artist}}: {{song.title}}</h3>' +
953
        '<table>' +
954
        '<tr><th>Duration</th><td>{{durationToMS song.duration}}</td></tr> ' +
955
        '<tr><th>Code</th><td>{{song.codeNumber}}</td></tr> ' +
956
        '<tr><th>Instruments </th><td>{{commalist song.instruments}}</td></tr> ' +
957
        '<tr><th>Games</th><td>{{commalist song.platforms}}</td></tr> ' +
958
        '<tr><th>Source</th><td>{{song.source}}</td></tr> ' +
959
        '</table>' +
960
        '</div>'
961
      );
962
963
    },
964
965
    ticketOrderChanged: function() {
966
      var that = this;
967
      var idOrder = [];
968
      $('#target').find('.ticket').each(
969
        function() {
970
          var ticketBlock = $(this);
971
          var ticketId = ticketBlock.data('ticketId');
972
          idOrder.push(ticketId);
973
        }
974
      );
975
976
      that.showAppMessage('Updating ticket order');
977
      $.ajax({
978
        method: 'POST',
979
        data: {
980
          idOrder: idOrder
981
        },
982
        url: '/api/newOrder',
983
        success: function(data, status) {
0 ignored issues
show
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...
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...
984
          // FIXME check return status
985
          that.showAppMessage('Saved revised order', 'success');
986
        },
987
        error: function(xhr, status, error) {
988
          var message = 'Failed to save revised order';
989
          that.reportAjaxError(message, xhr, status, error);
990
        }
991
      });
992
993
      this.updatePerformanceStats();
994
    },
995
996
    performButtonCallback: function(button) {
997
      var that = this;
998
999
      button = $(button);
1000
      var ticketId = button.data('ticketId');
1001
      that.showAppMessage('Mark ticket used');
1002
      $.ajax({
1003
          method: 'POST',
1004
          data: {
1005
            ticketId: ticketId
1006
          },
1007
          url: '/api/useTicket',
1008
          success: function(data, status) {
1009
            that.showAppMessage('Marked ticket used', 'success');
1010
            void(data);
1011
            void(status);
1012
            var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1013
            ticketBlock.addClass('used');
1014
            // TicketBlock.append(' (done)');
1015
1016
            // Fixme receive updated ticket info from API
1017
            var ticket = ticketBlock.data('ticket');
1018
            ticket.startTime = Date.now() / 1000;
1019
            ticket.used = true;
1020
            ticketBlock.data('ticket', ticket);
1021
1022
            that.updatePerformanceStats();
1023
          },
1024
          error: function(xhr, status, error) {
1025
            var message = 'Failed to mark ticket used';
1026
            that.reportAjaxError(message, xhr, status, error);
1027
          }
1028
        }
1029
      );
1030
    },
1031
1032
    removeButtonCallback: function(button) {
1033
      var that = this;
1034
      button = $(button);
1035
      var ticketId = button.data('ticketId');
1036
      that.showAppMessage('Deleting ticket');
1037
      $.ajax({
1038
          method: 'POST',
1039
          data: {
1040
            ticketId: ticketId
1041
          },
1042
          url: '/api/deleteTicket',
1043
          success: function(data, status) {
1044
            that.showAppMessage('Deleted ticket', 'success');
1045
            void(status);
1046
            var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1047
            ticketBlock.remove();
1048
            that.updatePerformanceStats();
1049
          },
1050
          error: function(xhr, status, error) {
1051
            var message = 'Failed to deleted ticket';
1052
            that.reportAjaxError(message, xhr, status, error);
1053
          }
1054
        }
1055
      );
1056
    },
1057
1058
    editButtonCallback: function(button) {
1059
      var that = this;
1060
      button = $(button);
1061
      var ticketId = button.data('ticketId');
1062
1063
      var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1064
      var ticket = ticketBlock.data('ticket'); // TODO possibly load from ajax instead?
1065
      that.resetEditTicketBlock(ticket);
1066
    },
1067
1068
    enableAcSongSelector: function(outerElement, songClickHandler) {
1069
      var that = this;
1070
      outerElement.find('.acSong').click(
1071
        function() {
1072
          // Find & decorate clicked element
1073
          outerElement.find('.acSong').removeClass('selected');
1074
          $(this).addClass('selected');
1075
1076
          var song = $(this).data('song');
1077
          songClickHandler.call(that, song); // Run in 'that' context
1078
        }
1079
      );
1080
    },
1081
1082
1083
    searchPageSongSelectionClick: function(song) {
1084
      var target = $('#searchTarget');
1085
      song.instruments = song.instruments.map(function(s) {
1086
        return s.name;
1087
      }); // Unwrap objects
1088
      target.html(this.songDetailsTemplate({song: song}));
1089
    },
1090
1091
    updatePerformanceStats: function() {
1092
      var that = this;
1093
      var performed = {};
1094
      var lastByPerformer = {};
1095
      var ticketOrdinal = 1;
1096
      var ticketTime = null;
1097
1098
      var pad = function(number) {
1099
        if (number < 10) {
1100
          return '0' + number;
1101
        }
1102
        return number;
1103
      };
1104
1105
      // First check number of songs performed before this one
1106
      var sortContainer = $('.sortContainer');
1107
      var lastSongDuration = null;
1108
      var lastTicketNoSong = true;
1109
1110
      var nthUnused = 1;
1111
1112
      sortContainer.find('.ticket').each(function() {
1113
        var realTime;
1114
        var ticketId = $(this).data('ticket-id');
1115
        var ticketData = $(this).data('ticket');
1116
1117
        if (ticketData.startTime) {
1118
          realTime = new Date(ticketData.startTime * 1000);
1119
        }
1120
1121
        $(this).removeClass('shown');
1122
1123
        if (!(ticketData.used || ticketData.private)) {
1124
          if (nthUnused <= that.displayOptions.upcomingCount) {
1125
            $(this).addClass('shown');
1126
          }
1127
          nthUnused++;
1128
        }
1129
1130
        $(this).find('.ticketOrdinal').text('# ' + ticketOrdinal);
1131
        // Fixme read ticketStart from data if present
1132
        if (realTime) {
1133
          ticketTime = realTime;
1134
        } else if (ticketTime) {
1135
          // If last song had an implicit time, add defaultSongOffsetMs to it and assume next song starts then
1136
          // If this is in the past, assume it starts now!
1137
          var songOffsetMs;
1138
          if (lastTicketNoSong) {
1139
            songOffsetMs = that.defaultSongIntervalSeconds * 1000;
1140
            // Could just be a message, could be a reset / announcement, so treat as an interval only
1141
          } else if (lastSongDuration) {
1142
            songOffsetMs = (that.defaultSongIntervalSeconds + lastSongDuration) * 1000;
1143
          } else {
1144
            songOffsetMs = (that.defaultSongIntervalSeconds + that.defaultSongLengthSeconds) * 1000;
1145
          }
1146
          ticketTime = new Date(Math.max(ticketTime.getTime() + songOffsetMs, Date.now()));
1147
        } else {
1148
          ticketTime = new Date();
1149
        }
1150
        $(this).find('.ticketTime').text(pad(ticketTime.getHours()) + ':' + pad(ticketTime.getMinutes()));
1151
1152
        // Update performer stats (done/total)
1153
        $(this).find('.performer').each(function() {
1154
          var performerId = $(this).data('performer-id');
1155
          var performerName = $(this).data('performer-name');
1156
          if (!performed.hasOwnProperty(performerId)) {
1157
            performed[performerId] = 0;
1158
          }
1159
          $(this).find('.songsDone').text(performed[performerId]);
1160
1161
          $(this).removeClass(
1162
            function(i, oldClass) {
1163
              void(i);
1164
              var classes = oldClass.split(' ');
1165
              var toRemove = [];
1166
              for (var cIdx = 0; cIdx < classes.length; cIdx++) {
1167
                if (classes[cIdx].match(/^performerDoneCount/)) {
1168
                  toRemove.push(classes[cIdx]);
1169
                }
1170
              }
1171
              return toRemove.join(' ');
1172
            }
1173
          ).addClass('performerDoneCount' + performed[performerId]);
1174
          performed[performerId]++;
1175
1176
          // Now check proximity of last song by this performer
1177
          if (lastByPerformer.hasOwnProperty(performerId)) {
1178
            var distance = ticketOrdinal - lastByPerformer[performerId].idx;
1179
            $(this).removeClass('proximityIssue');
1180
            $(this).removeClass('proximityIssue1');
1181
            if ((distance < 3) && (performerName.charAt(0) !== '?')) {
1182
              $(this).addClass('proximityIssue');
1183
              if (distance === 1) {
1184
                $(this).addClass('proximityIssue1');
1185
              }
1186
            }
1187
          } else {
1188
            // Make sure they've not got a proximity marker on a ticket that's been dragged to top
1189
            $(this).removeClass('proximityIssue');
1190
          }
1191
          lastByPerformer[performerId] = {idx: ticketOrdinal, ticketId: ticketId};
1192
        });
1193
        ticketOrdinal++;
1194
1195
        if (ticketData.song) {
1196
          lastSongDuration = ticketData.song.duration;
1197
          lastTicketNoSong = false;
1198
        } else {
1199
          lastSongDuration = 0;
1200
          lastTicketNoSong = true;
1201
        } // Set non-song ticket to minimum duration
1202
      });
1203
1204
      // Then update all totals
1205
      sortContainer.find('.performer').each(function() {
1206
        var performerId = $(this).data('performer-id');
1207
        var totalPerformed = performed[performerId];
1208
        $(this).find('.songsTotal').text(totalPerformed);
1209
      });
1210
    },
1211
1212
    /**
1213
     * Show a message in the defined appMessageTarget (f any)
1214
     *
1215
     * @param message {string} Message to show (replaces any other)
1216
     * @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...
1217
     */
1218
    showAppMessage: function(message, className) {
1219
      var that = this;
1220
      if (this.messageTimer) {
1221
        clearTimeout(this.messageTimer);
1222
      }
1223
1224
      this.messageTimer = setTimeout(function() {
1225
        that.appMessageTarget.html('');
1226
      }, 5000);
1227
1228
      if (!className) {
1229
        className = 'info';
1230
      }
1231
      if (this.appMessageTarget) {
1232
        var block = $('<div />').addClass('alert alert-' + className);
1233
        block.text(message);
1234
        this.appMessageTarget.html('').append(block);
1235
      }
1236
    },
1237
1238
    ucFirst: function(string) {
1239
      return string.charAt(0).toUpperCase() + string.slice(1);
1240
    },
1241
1242
    reportAjaxError: function(message, xhr, status, error) {
1243
      this.showAppMessage(
1244
        this.ucFirst(status) + ': ' + message + ': ' + error + ', ' + xhr.responseJSON.error,
1245
        'danger'
1246
      );
1247
    },
1248
1249
    checkRemoteRedirect: function() {
1250
      window.setInterval(function() {
1251
          $.get('/api/remotesRedirect', function(newPath) {
1252
            if (newPath && (newPath !== window.location.pathname)) {
1253
              window.location.pathname = newPath;
1254
            }
1255
          });
1256
        },
1257
        10000);
1258
    }
1259
  };
1260
}());