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 ( f169ef...30a444 )
by Richard
05:12
created

ticketer.drawConfirmTicketFormIfValid   C

Complexity

Conditions 7
Paths 17

Size

Total Lines 91

Duplication

Lines 0
Ratio 0 %

Importance

Changes 9
Bugs 1 Features 0
Metric Value
c 9
b 1
f 0
dl 0
loc 91
rs 6.5033
cc 7
nc 17
nop 3

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
    selfSubmitTemplate: null,
14
    manageInstrumentTabsTemplate: null,
15
    appMessageTarget: null,
16
    ticketSubmitTemplate: null,
17
    searchCount: 10,
18
    instrumentOrder: null,
19
    defaultSongLengthSeconds: 240,
20
    defaultSongIntervalSeconds: 120,
21
    messageTimer: null,
22
    lastUpdateHash: null,
23
24
    /**
25
     * @var {{songInPreview,upcomingCount,iconMapHtml,selfSubmission}}
26
     */
27
    displayOptions: {},
28
29
    /**
30
     * List of all performers (objects) who've signed up in this session
31
     */
32
    performers: [],
33
34
    /**
35
     * List of all platform names in the system
36
     */
37
    platforms: [],
38
39
    performerExists: function(performerName) {
40
      for (var i = 0; i < this.performers.length; i++) {
41
        if (this.performers[i].performerName.toLowerCase() === performerName.toLowerCase()) {
42
          return true;
43
        }
44
      }
45
      return false;
46
    },
47
48
    addPerformerByName: function(performerName) {
49
      this.performers.push({performerName: performerName});
50
      // Now resort it
51
      this.performers.sort(function(a, b) {
52
        return a.performerName.localeCompare(b.performerName);
53
      });
54
    },
55
56
    /**
57
     * Run the "upcoming" panel
58
     */
59
    go: function() {
60
      this.initTemplates();
61
62
      ticketer.reloadTickets();
63
      setInterval(function() {
64
        ticketer.reloadTickets();
65
      }, 10000);
66
    },
67
68
    sortBand: function(unsortedBand) {
69
      var sortedBand = {};
70
      if (this.instrumentOrder) {
71
        for (var i = 0; i < this.instrumentOrder.length; i++) {
72
          var instrument = this.instrumentOrder[i];
73
          if (unsortedBand.hasOwnProperty(instrument)) {
74
            sortedBand[instrument] = unsortedBand[instrument];
75
          }
76
        }
77
      } else {
78
        sortedBand = unsortedBand;
79
      }
80
      return sortedBand;
81
    },
82
83
    /**
84
     * Draw an "upcoming" ticket
85
     * @param ticket {{band}}
86
     * @returns {*}
87
     */
88
    drawDisplayTicket: function(ticket) {
89
      // Sort band into standard order
90
      var unsortedBand = ticket.band;
91
      ticket.band = this.sortBand(unsortedBand);
92
      var ticketParams = {ticket: ticket, icons: this.displayOptions.iconMapHtml};
93
      return this.upcomingTicketTemplate(ticketParams);
94
    },
95
96
    /**
97
     * Draw a "queue management" ticket
98
     * @param ticket
99
     * @returns {*}
100
     */
101
    drawManageableTicket: function(ticket) {
102
      ticket.used = Number(ticket.used); // Force int
103
104
      return this.manageTemplate({ticket: ticket});
105
    },
106
107
    /**
108
     * Reload all tickets on the upcoming page
109
     */
110
    reloadTickets: function() {
111
      var that = this;
112
113
      $.get('/api/next', function(tickets) {
114
115
        var out = '';
116
        for (var i = 0; i < tickets.length; i++) {
117
          var ticket = tickets[i];
118
          out += that.drawDisplayTicket(ticket);
119
        }
120
121
        var target = $('#target');
122
        target.html(out);
123
124
        target.find('.auto-font').each(
125
          function() {
126
            var fixedWidth = $(this).data('fixed-assetwidth');
127
            if (!fixedWidth) {
128
              fixedWidth = 0;
129
            }
130
            fixedWidth = Number(fixedWidth);
131
            if ((screen.width <= 480) && window.devicePixelRatio) {
0 ignored issues
show
Bug introduced by
The variable screen seems to be never declared. If this is a global, consider adding a /** global: screen */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
132
              fixedWidth = fixedWidth / 2; // Mobile CSS
133
            }
134
135
            var spaceUsedByText = (this.scrollWidth - fixedWidth);
136
            var spaceAvailableForText = (this.clientWidth - fixedWidth);
137
            var rawScale = Math.max(spaceUsedByText / spaceAvailableForText, 1);
138
            var scale = 1.05 * rawScale;
139
140
            if (that.displayOptions.adminQueueHasControls && that.displayOptions.isAdmin) {
141
              scale *= 1.25;
142
            }
143
144
            // 1.05 extra scale to fit neatly, fixedWidth is non-scaling elements
145
            var font = Number($(this).css('font-size').replace(/[^0-9]+$/, ''));
146
            $(this).css('font-size', Number(font / scale).toFixed() + 'px');
147
          }
148
        );
149
150
        target.find('.performingButton').click(function() {
151
          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...
152
          if (window.confirm('Mark song as performing?')) {
153
            that.performButtonCallback(this);
154
          }
155
        });
156
        target.find('.removeButton').click(function() {
157
          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...
158
          if (window.confirm('Remove song?')) {
159
            that.removeButtonCallback(this);
160
          }
161
        });
162
163
      });
164
    },
165
166
    /**
167
     * Enable queue management ticket buttons in the specified element
168
     * @param topElement
169
     */
170
    enableButtons: function(topElement) {
171
      var that = this;
172
173
      $(topElement).find('.performButton').click(function() {
174
        that.performButtonCallback(this);
175
      });
176
177
      $(topElement).find('.removeButton').click(function() {
178
        that.removeButtonCallback(this);
179
      });
180
181
      $(topElement).find('.editButton').click(function() {
182
        that.editButtonCallback(this);
183
      });
184
    },
185
186
    /**
187
     * Activate the search box at the given location
188
     *
189
     * @param {string} songSearchInput Input field identifier
190
     * @param {string} songSearchResultsTarget Container for output list
191
     * @param {function} songClickHandler Function to call when a listed song is clicked
192
     */
193
    enableSongSearchBox: function(songSearchInput, songSearchResultsTarget, songClickHandler) {
194
      var that = this;
195
      $(songSearchInput).keyup(
196
        function() {
197
          var songComplete = $(songSearchResultsTarget);
198
          var input = $(this);
199
          var searchString = input.val();
200
          if (searchString.length >= 3) {
201
            $.ajax({
202
              method: 'POST',
203
              data: {
204
                searchString: searchString,
205
                searchCount: that.searchCount
206
              },
207
              url: '/api/songSearch',
208
              /**
209
               * @param {{songs, searchString}} data
210
               */
211
              success: function(data) {
212
                var songs = data.songs;
213
                if (input.val() == data.searchString) {
214
                  // Ensure autocomplete response is still valid for current input value
215
                  var out = '';
216
                  var song; // Used twice below
217
                  for (var i = 0; i < songs.length; i++) {
218
                    song = songs[i];
219
                    out += that.songAutocompleteItemTemplate({song: song});
220
                  }
221
                  songComplete.html(out).show();
222
223
                  // Now attach whole song as data:
224
                  for (i = 0; i < songs.length; i++) {
225
                    song = songs[i];
226
                    var songId = song.id;
227
                    songComplete.find('.acSong[data-song-id=' + songId + ']').data('song', song);
228
                  }
229
230
                  that.enableAcSongSelector(songComplete, songClickHandler);
231
                }
232
              },
233
              error: function(xhr, status, error) {
234
                void(error);
235
              }
236
            });
237
          } else {
238
            songComplete.html('');
239
          }
240
        }
241
      );
242
    },
243
244
    /**
245
     * Completely (re)generate the add ticket control panel and enable its controls
246
     * @param {?number} currentTicket Optional
247
     */
248
    resetEditTicketBlock: function(currentTicket) {
249
      var that = this;
250
      var controlPanelOuter = $('.editTicketOuter');
251
252
      // Current panel state in function scope
253
      var selectedInstrument = 'V';
254
      var currentBand = {};
255
256
      // Reset band to empty (or to ticket band state)
257
      for (var instrumentIdx = 0; instrumentIdx < that.instrumentOrder.length; instrumentIdx++) {
258
        var instrument = that.instrumentOrder[instrumentIdx];
259
        currentBand[instrument] = [];
260
261
        if (currentTicket && currentTicket.band) {
262
          // Ticket.band is a complex datatype. Current band is just one array of names per instrument. Unpack to show.
263
          if (currentTicket.band.hasOwnProperty(instrument)) {
264
            var instrumentPerformerObjects = currentTicket.band[instrument];
265
            for (var pIdx = 0; pIdx < instrumentPerformerObjects.length; pIdx++) {
266
              currentBand[instrument].push(instrumentPerformerObjects[pIdx].performerName);
267
            }
268
          }
269
        }
270
        // Store all instruments as arrays - most can only be single, but vocals is 1..n potentially
271
      }
272
273
      drawEditTicketForm(currentTicket);
274
      // X var editTicketBlock = $('.editTicket'); // only used in inner scope (applyNewSong)
275
276
      // Enable 'Add' button
277
      $('.editTicketButton').click(editTicketCallback);
278
      $('.cancelTicketButton').click(cancelTicketCallback);
279
      $('.removeSongButton').click(removeSong);
280
281
      $('.toggleButton').click(
282
        function() {
283
          var check = $(this).find('input[type=checkbox]');
284
          check.prop('checked', !check.prop('checked'));
285
        }
286
      );
287
288
      var ticketTitleInput = $('.editTicketTitle');
289
290
      // Copy band name into summary area on Enter
291
      ticketTitleInput.keydown(function(e) {
292
        if (e.keyCode === 13) {
293
          updateBandSummary();
294
        }
295
      });
296
297
      $('.newPerformer').keydown(function(e) {
298
        if (e.keyCode === 13) {
299
          var newPerformerInput = $('.newPerformer');
300
          var newName = newPerformerInput.val();
301
          if (newName.trim().length) {
302
            that.alterInstrumentPerformerList(currentBand, selectedInstrument, newName, true);
303
            // Now update band with new performers of this instrument
304
            updateInstrumentTabPerformers();
305
            rebuildPerformerList(); // Because performer allocations changed
306
            if (currentBand[selectedInstrument].length) { // If we've a performer for this instrument, skip to next
307
              nextInstrumentTab();
308
            }
309
          }
310
          newPerformerInput.val('');
311
        }
312
      });
313
314
      // Set up the song search box in this control panel and set the appropriate callback
315
      var songSearchInput = '.addSongTitle';
316
      var songSearchResultsTarget = '.songComplete';
317
318
      this.enableSongSearchBox(songSearchInput, songSearchResultsTarget, applyNewSong);
319
320
      // ************* Inner functions **************
321
      /**
322
       * Switch to the next visible instrument tab
323
       */
324
      function nextInstrumentTab() {
325
        // Find what offset we're at in instrumentOrder
326
        var currentOffset = 0;
327
        for (var i = 0; i < that.instrumentOrder.length; i++) {
328
          if (that.instrumentOrder[i] === selectedInstrument) {
329
            currentOffset = i;
330
          }
331
        }
332
        var nextOffset = currentOffset + 1;
333
        if (nextOffset >= that.instrumentOrder.length) {
334
          nextOffset = 0;
335
        }
336
        var instrument = that.instrumentOrder[nextOffset];
337
        selectedInstrument = instrument; // Reset before we redraw tabs
338
        var newActiveTab = setActiveTab(instrument);
339
340
        // Make sure we switch to a *visible* tab
341
        if (newActiveTab.hasClass('instrumentUnused')) {
342
          nextInstrumentTab();
343
        }
344
      }
345
346
      /**
347
       * (re)Draw the add/edit ticket control panel in the .editTicketOuter element
348
       */
349
      function drawEditTicketForm(ticket) {
350
        var templateParams = {performers: that.performers};
351
        if (ticket) {
352
          templateParams.ticket = ticket;
353
        }
354
        controlPanelOuter.html(that.editTicketTemplate(templateParams));
355
        if (ticket && ticket.song) {
356
          applyNewSong(ticket.song);
357
        }
358
        updateInstrumentTabPerformers();
359
        rebuildPerformerList(); // Initial management form display
360
      }
361
362
363
      /**
364
       * Rebuild list of performer buttons according to overall performers list
365
       * and which instruments they are assigned to
366
       *
367
       * TODO refactor so that the current standard method is as for the management page and calls an internal
368
       * function (buildPerformerList ?) with targetElement,Callback,instrument functions?
369
       */
370
      function rebuildPerformerList() {
371
        var newButton;
372
        var targetElement = controlPanelOuter.find('.performerControls');
373
        targetElement.text(''); // Remove existing list
374
375
        var lastInitial = '';
376
        var performerCount = that.performers.length;
377
        var letterSpan;
378
        for (var pIdx = 0; pIdx < performerCount; pIdx++) {
379
          var performerName = that.performers[pIdx].performerName;
380
          var performerInstrument = that.findPerformerInstrument(performerName, currentBand);
381
          var isPerforming = performerInstrument ? 1 : 0;
382
          var initialLetter = performerName.charAt(0).toUpperCase();
383
          if (lastInitial !== initialLetter) { // If we're changing letter
384
            if (letterSpan) {
385
              targetElement.append(letterSpan); // Stash the previous letterspan if present
386
            }
387
            letterSpan = $('<span class="letterSpan"></span>'); // Create a new span
388
            if ((performerCount > 15)) {
389
              letterSpan.append($('<span class="initialLetter">' + initialLetter + '</span>'));
390
            }
391
          }
392
          lastInitial = initialLetter;
393
394
          newButton = $('<span></span>');
395
          newButton.addClass('btn addPerformerButton');
396
          newButton.addClass(isPerforming ? 'btn-primary' : 'btn-default');
397
          if (isPerforming && (performerInstrument !== selectedInstrument)) { // Dim out buttons for other instruments
398
            newButton.attr('disabled', 'disabled');
399
          }
400
          newButton.text(performerName);
401
          newButton.data('selected', isPerforming); // This is where it gets fun - check if user is in band!
402
          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...
403
        }
404
        targetElement.append(letterSpan);
405
406
        // Enable the new buttons
407
        $('.addPerformerButton').click(function() {
408
          var name = $(this).text();
409
          var selected = $(this).data('selected') ? 0 : 1; // Reverse to get new state
410
          if (selected) {
411
            $(this).removeClass('btn-default').addClass('btn-primary');
412
          } else {
413
            $(this).removeClass('btn-primary').addClass('btn-default');
414
          }
415
          $(this).data('selected', selected); // Toggle
416
417
          that.alterInstrumentPerformerList(currentBand, selectedInstrument, name, selected);
418
          updateInstrumentTabPerformers();
419
          rebuildPerformerList(); // Because performer allocations changed
420
          if (currentBand[selectedInstrument].length) { // If we've a performer for this instrument, skip to next
421
            nextInstrumentTab();
422
          }
423
        });
424
      }
425
426
      /**
427
       * Handle click on edit ticket button
428
       */
429
      function editTicketCallback() {
430
        var titleInput = $('.editTicketTitle');
431
        var ticketTitle = titleInput.val();
432
        var songInput = $('.selectedSongId');
433
        var songId = songInput.val();
434
        var privateCheckbox = $('input.privateCheckbox');
435
        var isPrivate = privateCheckbox.is(':checked');
436
        var blockingCheckbox = $('input.blockingCheckbox');
437
        var isBlocked = blockingCheckbox.is(':checked');
438
439
        var data = {
440
          title: ticketTitle,
441
          songId: songId,
442
          band: currentBand,
443
          private: isPrivate,
444
          blocking: isBlocked
445
        };
446
447
        if (currentTicket) {
448
          data.existingTicketId = currentTicket.id;
449
        }
450
451
        that.showAppMessage('Saving ticket');
452
453
        $.ajax({
454
            method: 'POST',
455
            data: data,
456
            url: '/api/saveTicket',
457
            success: function(data, status) {
458
              that.showAppMessage('Saved ticket', 'success');
459
460
              void(status);
461
              var ticketId = data.ticket.id;
462
463
              var ticketBlockSelector = '.ticket[data-ticket-id="' + ticketId + '"]';
464
              var existingTicketBlock = $(ticketBlockSelector);
465
              if (existingTicketBlock.length) {
466
                // Replace existing
467
                existingTicketBlock.after(that.drawManageableTicket(data.ticket));
468
                existingTicketBlock.remove();
469
              } else {
470
                // Append new
471
                $('#target').append(that.drawManageableTicket(data.ticket));
472
              }
473
474
              var ticketBlock = $(ticketBlockSelector);
475
              ticketBlock.data('ticket', data.ticket);
476
              that.enableButtons(ticketBlock);
477
478
              if (data.performers) {
479
                that.performers = data.performers;
480
              }
481
482
              that.updatePerformanceStats();
483
              that.resetEditTicketBlock();
484
485
            },
486
            error: function(xhr, status, error) {
487
              var message = 'Ticket save failed';
488
              that.reportAjaxError(message, xhr, status, error);
489
              void(error);
490
              // FIXME handle error
491
            }
492
          }
493
        );
494
      }
495
496
      function cancelTicketCallback() {
497
        that.resetEditTicketBlock();
498
      }
499
500
      /**
501
       * Return tab corresponding to a given instrument abbreviation
502
       *
503
       * @param {string} instrument Abbreviation
504
       * @returns {jQuery}
505
       */
506
      function getTabByInstrument(instrument) {
507
        return controlPanelOuter.find('.instrument[data-instrument-shortcode=' + instrument + ']');
508
      }
509
510
      /**
511
       * Set the tab for the specified instrument abbreviation as active
512
       *
513
       * @param selectedInstrument
514
       * @returns {jQuery}
515
       */
516
      function setActiveTab(selectedInstrument) {
517
        var allInstrumentTabs = controlPanelOuter.find('.instrument');
518
        allInstrumentTabs.removeClass('instrumentSelected');
519
        var selectedTab = getTabByInstrument(selectedInstrument);
520
        selectedTab.addClass('instrumentSelected');
521
        rebuildPerformerList(); // Because current instrument context changed
522
        return selectedTab;
523
      }
524
525
      /**
526
       * Update the band summary line in the manage area
527
       */
528
      function updateBandSummary() {
529
        var bandName = $('.editTicketTitle').val();
530
        var members = [];
531
        for (var instrument in currentBand) {
532
          if (currentBand.hasOwnProperty(instrument)) {
533
            for (var i = 0; i < currentBand[instrument].length; i++) {
534
              members.push(currentBand[instrument][i]);
535
            }
536
          }
537
        }
538
        var memberList = members.join(', ');
539
        var summaryHtml = (bandName ? bandName + '<br />' : '') + memberList;
540
        $('.selectedBand').html(summaryHtml);
541
      }
542
543
      /**
544
       * Update all instrument tabs with either performer names or 'needed' note
545
       */
546
      function updateInstrumentTabPerformers() {
547
        var performersSpan;
548
        var performerString;
549
550
        for (var iIdx = 0; iIdx < that.instrumentOrder.length; iIdx++) {
551
          var instrument = that.instrumentOrder[iIdx];
552
553
          performersSpan = controlPanelOuter
554
            .find('.instrument[data-instrument-shortcode=' + instrument + ']')
555
            .find('.instrumentPerformer');
556
557
          performerString = currentBand[instrument].join(', ');
558
          if (!performerString) {
559
            performerString = '<i>Needed</i>';
560
          }
561
          performersSpan.html(performerString);
562
        }
563
        updateBandSummary();
564
      }
565
566
      /**
567
       * Handle click on a song in manage page search results
568
       *
569
       * @param {{id, title, artist, instruments}} song
570
       */
571
      function applyNewSong(song) {
572
        var selectedId = song.id;
573
        var selectedSong = song.artist + ': ' + song.title;
574
575
        var removeSongButton = $('.removeSongButton');
576
        removeSongButton.removeClass('hidden');
577
578
        // Perform actions with selected song
579
        var editTicketBlock = $('.editTicket'); // Temp hack, should already be in scope?
580
581
        editTicketBlock.find('input.selectedSongId').val(selectedId);
582
        editTicketBlock.find('.selectedSong').text(selectedSong);
583
584
        // Redraw instrument tabs according to current songs
585
        var instrumentDiv = controlPanelOuter.find('.instruments');
586
        instrumentDiv.html(that.manageInstrumentTabsTemplate(song.instruments));
587
        instrumentDiv.find('.instrument').removeClass('instrumentSelected');
588
        instrumentDiv.find('.instrument:first').addClass('instrumentSelected');
589
590
        // Enable the instrument tabs
591
        // Var allInstrumentTabs = controlPanelOuter.find('.instrument');
592
593
        instrumentDiv.find('.instrument').click(
594
          function() {
595
            selectedInstrument = $(this).data('instrumentShortcode');
596
            setActiveTab(selectedInstrument);
597
          }
598
        );
599
600
        // Iterate through currentBand and remove any instruments not present in song abbreviations
601
        var validInstruments = song.instruments.map(function(i) {
602
          return i.abbreviation;
603
        });
604
        for (var instrument in currentBand) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
605
          if (validInstruments.indexOf(instrument) === -1) {
606
            currentBand[instrument] = [];
607
          }
608
        }
609
610
        updateInstrumentTabPerformers();
611
        rebuildPerformerList(); // Because song changed
612
      }
613
614
      function removeSong() {
615
        var editTicketBlock = $('.editTicket'); // Temp hack, should already be in scope?
616
        editTicketBlock.find('input.selectedSongId').val(0);
617
        editTicketBlock.find('.selectedSong').text('');
618
        $(songSearchInput).val('');
619
        var removeSongButton = $('.removeSongButton');
620
        removeSongButton.hide();
621
622
      }
623
    },
624
625
    manage: function(tickets) {
626
      var that = this;
627
      this.appMessageTarget = $('#appMessages');
628
      this.initTemplates();
629
      var ticket, ticketBlock; // For loop iterations
630
631
      if (this.displayOptions.selfSubmission) {
632
        // Enable warning on hash change
633
        var updateWarning = $('#updateWarning');
634
635
        $.get('/api/updateHash', function(data) {
636
          that.lastUpdateHash = data.hash;
637
          updateWarning.find('.btn').click(function() {
638
            window.location.reload(true);
639
          });
640
        });
641
642
        window.setInterval(function() {
643
            $.get('/api/updateHash', function(data) {
644
              // X window.alert('Tickets: '+$('.ticket').length + ' / '+ data.hash  + ' / ' + that.lastUpdateHash);
645
              if ((that.lastUpdateHash !== data.hash) && (data.hash !== $('.ticket').length)) {
646
                // Note: abusing "opaque" hash here to avoid false-positives! (assuming it's undeleted count)
647
                updateWarning.show();
648
              }
649
            });
650
          },
651
          10000);
652
      }
653
654
      var out = '';
655
      for (var i = 0; i < tickets.length; i++) {
656
        ticket = tickets[i];
657
        out += that.drawManageableTicket(ticket);
658
        ticketBlock = $('.ticket[data-ticket-id="' + ticket.id + '"]');
659
        ticketBlock.data('ticket', ticket);
660
      }
661
      $('#target').html(out);
662
663
      // Find new tickets (now they're DOM'd) and add data to them
664
      for (i = 0; i < tickets.length; i++) {
665
        ticket = tickets[i];
666
        ticketBlock = $('.ticket[data-ticket-id="' + ticket.id + '"]');
667
        ticketBlock.data('ticket', ticket);
668
      }
669
670
      var $sortContainer = $('.sortContainer');
671
      $sortContainer.sortable({
672
        axis: 'y',
673
        update: function(event, ui) {
674
          void(event);
675
          void(ui);
676
          that.ticketOrderChanged();
677
        }
678
      }).disableSelection().css('cursor', 'move');
679
680
      this.enableButtons($sortContainer);
681
682
      this.updatePerformanceStats();
683
684
      this.resetEditTicketBlock();
685
686
    },
687
688
    initSearchPage: function() {
689
      var that = this;
690
      this.initTemplates();
691
      this.enableSongSearchBox('.searchString', '.songComplete', that.searchPageSongSelectionClick);
692
    },
693
694
    initTemplates: function() {
695
      var that = this;
696
697
      // CommaList = each, with commas joining. Returns value at t as tuple {k,v}
698
      // "The options hash contains a function (options.fn) that behaves like a normal compiled Handlebars template."
699
      // If called without inner template, options.fn is not populated
700
      Handlebars.registerHelper('commalist', function(context, options) {
701
        var retList = [];
702
703
        for (var key in context) {
704
          if (context.hasOwnProperty(key)) {
705
            retList.push(options.fn ? options.fn({k: key, v: context[key]}) : context[key]);
706
          }
707
        }
708
709
        return retList.join(', ');
710
      });
711
712
      Handlebars.registerHelper('instrumentIcon', function(instrumentCode) {
713
        var icon = '<span class="instrumentTextIcon">' + instrumentCode + '</span>';
714
        if (that.displayOptions.hasOwnProperty('iconMapHtml')) {
715
          if (that.displayOptions.iconMapHtml.hasOwnProperty(instrumentCode)) {
716
            icon = that.displayOptions.iconMapHtml[instrumentCode];
717
          }
718
        }
719
        return new Handlebars.SafeString(icon);
720
      });
721
722
      Handlebars.registerHelper('durationToMS', function(duration) {
723
        var seconds = (duration % 60);
724
        if (seconds < 10) {
725
          seconds = '0' + seconds;
726
        }
727
        return Math.floor(duration / 60) + ':' + seconds;
728
      });
729
730
      Handlebars.registerHelper('gameList', function(song) {
731
        return song.platforms.join(', ');
732
      });
733
734
      Handlebars.registerHelper('ifContains', function(haystack, needle, options) {
735
        return (haystack.indexOf(needle) === -1) ? '' : options.fn(this);
736
      });
737
738
      this.manageTemplate = Handlebars.compile(
739
        '<div class="ticket well well-sm {{#if ticket.used}}used{{/if}}' +
740
        ' {{#if ticket.song}}{{#each ticket.song.platforms }}platform{{ this }} {{/each}}{{/if}}' +
741
        ' {{#if ticket.band.K}}withKeys{{/if}}"' +
742
        ' data-ticket-id="{{ ticket.id }}">' +
743
        '        <div class="pull-right">' +
744
        (function() {
745
          var s = '';
746
          for (var i = 0; i < that.platforms.length; i++) {
747
            var p = that.platforms[i];
748
            s += '<div class="gameMarker gameMarker' + p + '">' +
749
              '{{#if ticket.song}}{{#ifContains ticket.song.platforms "' + p + '" }}' + p +
750
              '{{/ifContains}}{{/if}}</div>';
751
          }
752
          return s;
753
        })() +
754
        '        <button class="btn btn-primary performButton" data-ticket-id="{{ ticket.id }}">Performing</button>' +
755
        '        <button class="btn btn-danger removeButton" data-ticket-id="{{ ticket.id }}">Remove</button>' +
756
        '        <button class="btn editButton" data-ticket-id="{{ ticket.id }}">' +
757
        '<span class="fa fa-edit" title="Edit"></span>' +
758
        '</button>' +
759
        '        </div>' +
760
        '<div class="ticketOrder">' +
761
        '<div class="ticketOrdinal"></div>' +
762
        '<div class="ticketTime"></div>' +
763
        '</div>' +
764
        '<div class="ticketId">' +
765
        '<span class="fa fa-ticket"></span> {{ ticket.id }}</div> ' +
766
        '<div class="ticketMeta">' +
767
        '<div class="blocking">' +
768
        '{{#if ticket.blocking}}<span class="fa fa-hand-stop-o" title="Blocking" />{{/if}}' +
769
        '</div>' +
770
        '<div class="private">' +
771
        '{{#if ticket.private}}<span class="fa fa-eye-slash" title="Private" />{{/if}}' +
772
        '</div>' +
773
        '</div>' +
774
        '<div class="pendingSong">' +
775
        '<span class="fa fa-group"></span> ' +
776
777
        // Display performers with metadata if valid, else just the band title.
778
        /*
779
         '{{#if ticket.performers}}' +
780
         '{{#each ticket.performers}}' +
781
         '<span class="performer performerDoneCount{{songsDone}}" ' +
782
         'data-performer-id="{{performerId}}"> {{performerName}} ' +
783
         ' (<span class="songsDone">{{songsDone}}</span>/<span class="songsTotal">{{songsTotal}}</span>)' +
784
         '</span>' +
785
         '{{/each}}' +
786
         '{{else}}' +
787
         '{{ ticket.title }}' +
788
         '{{/if}}' +
789
         */
790
791
        // Display performers with metadata if valid, else just the band title.
792
        '{{#if ticket.band}}' +
793
        '{{#each ticket.band}} <span class="instrumentTextIcon">{{ @key }}</span>' +
794
        '{{#each this}}' +
795
        '<span class="performer performerDoneCount{{songsDone}}" ' +
796
        'data-performer-id="{{performerId}}" data-performer-name="{{performerName}}"> {{performerName}} ' +
797
        ' (<span class="songsDone">{{songsDone}}</span>/<span class="songsTotal">{{songsTotal}}</span>)' +
798
        '</span>' +
799
        '{{/each}}' +
800
        '{{/each}}' +
801
        '{{/if}}' +
802
803
        '{{#if ticket.title}}' +
804
        '<span class="ticketTitleIcon"><span class="instrumentTextIcon">Title</span> {{ ticket.title }}</span>' +
805
        '{{/if}}' +
806
807
        '{{#if ticket.song}}<br /><span class="fa fa-music"></span> {{ticket.song.artist}}: ' +
808
        '{{ticket.song.title}}' +
809
        ' ({{gameList ticket.song}})' +
810
        '{{/if}}' +
811
        '</div>' +
812
        '</div>'
813
      );
814
815
      this.upcomingTicketTemplate = Handlebars.compile(
816
        '<div class="ticket well ' +
817
        (this.displayOptions.songInPreview ? 'withSong' : 'noSong') +
818
        ' ' +
819
        (this.displayOptions.title ? 'withTitle' : 'noTitle') + // TODO is this used (correctly)?
820
        '" data-ticket-id="{{ ticket.id }}">' +
821
822
        (this.displayOptions.adminQueueHasControls && this.displayOptions.isAdmin ?
823
          '<div class="ticketAdminControls">' +
824
          '<button class="btn btn-sm btn-primary performingButton"' +
825
          ' data-ticket-id="{{ ticket.id }}">Performing</button>' +
826
          '<button class="btn btn-sm btn-danger removeButton" data-ticket-id="{{ ticket.id }}">Remove</button>' +
827
          '</div>'
828
          : '') +
829
830
831
        '<div class="ticketMeta">' +
832
        '<div class="blocking">' +
833
        '{{#if ticket.blocking}}<span class="fa fa-hand-stop-o" title="Blocking" />{{/if}}' +
834
        '</div>' +
835
        '<div class="private">' +
836
        '{{#if ticket.private}}<span class="fa fa-eye-slash" title="Private" />{{/if}}' +
837
        '</div>' +
838
        '</div>' +
839
840
        '  <div class="ticket-inner">' +
841
        '    <p class="text-center band auto-font">{{ticket.title}}</p>' +
842
        '    <p class="performers auto-font" data-fixed-assetwidth="200">' +
843
        '{{#each ticket.band}}' +
844
        '<span class="instrumentTag">{{instrumentIcon @key}}</span>' +
845
        '<span class="instrumentPerformers">{{#commalist this}}{{v.performerName}}{{/commalist}}</span>' +
846
        '{{/each}}' +
847
        '    </p>' +
848
        (this.displayOptions.songInPreview ?
849
          '{{#if ticket.song}}<p class="text-center song auto-font">' +
850
          '{{ticket.song.artist}}: {{ticket.song.title}}' +
851
          ' ({{gameList ticket.song}})' +
852
          '</p>{{/if}}' : '') +
853
        '        </div>' +
854
        '</div>  '
855
      );
856
857
      this.songAutocompleteItemTemplate = Handlebars.compile(
858
        '<div class="acSong" data-song-id="{{ song.id }}">' +
859
        '        <div class="acSong-inner {{#if song.queued}}queued{{/if}}">' +
860
        '        {{song.artist}}: {{song.title}} ({{gameList song}}) ' +
861
        '        </div>' +
862
        '</div>  '
863
      );
864
865
      this.editTicketTemplate = Handlebars.compile(
866
        '<div class="editTicket well">' +
867
        '<div class="pull-right editTicketButtons">' +
868
        '<button class="blockingButton btn btn-warning toggleButton">' +
869
        '<span class="fa fa-hand-stop-o" /> Blocking ' +
870
        ' <input type="checkbox" class="blockingCheckbox" ' +
871
        '  {{#if ticket}}{{# if ticket.blocking }}checked="checked"{{/if}}{{/if}} /></button>' +
872
        '<button class="privacyButton btn btn-warning toggleButton">' +
873
        '<span class="fa fa-eye-slash" /> Private ' +
874
        ' <input type="checkbox" class="privateCheckbox" ' +
875
        '  {{#if ticket}}{{# if ticket.private }}checked="checked"{{/if}}{{/if}} /></button>' +
876
        '<button class="editTicketButton btn btn-success">' +
877
        '<span class="fa fa-save" /> Save</button>' +
878
        '<button class="cancelTicketButton btn">' +
879
        '<span class="fa fa-close" /> Cancel</button>' +
880
        '</div>' +
881
882
        '{{# if ticket}}' +
883
        '<h3 class="editTicketHeader">Edit ticket <span class="fa fa-ticket"></span> {{ticket.id}}</h3>' +
884
        '{{else}}<h3 class="newTicketHeader">Add new ticket</h3>{{/if}}' +
885
886
        '<div class="editTicketInner">' +
887
        '<div class="editTicketSong">' +
888
        '<div class="ticketAspectSummary"><span class="fa fa-music fa-2x" title="Song"></span> ' +
889
        '<input type="hidden" class="selectedSongId"/> ' +
890
        '<span class="selectedSong">{{#if ticket}}{{#if ticket.song}}{{ticket.song.artist}}: ' +
891
        '{{ticket.song.title}}{{/if}}{{/if}}</span>' +
892
893
        '<button title="Remove song from ticket" ' +
894
        'class="btn removeSongButton{{#unless ticket}}{{#unless ticket.song}} hidden{{/unless}}{{/unless}}">' +
895
        ' <span class="fa fa-ban" />' +
896
        '</button>' +
897
898
        '</div>' +
899
        '<div class="input-group input-group">' +
900
        '<span class="input-group-addon" id="search-addon1"><span class="fa fa-search"></span> </span>' +
901
        '<input class="addSongTitle form-control" placeholder="Search song or use code"/>' +
902
        '</div>' +
903
904
        '<div class="songCompleteOuter">' +
905
        '<div class="songComplete"></div>' +
906
        '</div>' + // /songCompleteOuter
907
        '</div>' + // /editTicketSong
908
909
        '<div class="editTicketBandColumn">' +
910
911
        '<div class="ticketAspectSummary"><span class="fa fa-group fa-2x pull-left" title="Performers"></span>' +
912
        '<span class="selectedBand">{{#if ticket}}{{ticket.title}}{{/if}}</span>' +
913
        '</div>' + // /ticketAspectSummary
914
915
        '<div class="input-group">' +
916
        '<span class="input-group-addon" id="group-addon-band"><span class="fa fa-pencil"></span> </span>' +
917
        '<input class="editTicketTitle form-control" placeholder="Band name or message (optional)"' +
918
        ' value="{{#if ticket}}{{ticket.title}}{{/if}}"/>' +
919
        '</div>' + // /input-group
920
921
        '<div class="bandControls">' +
922
        '<div class="bandTabsOuter">' +
923
        '<div class="instruments">' +
924
        '</div>' + // /instruments
925
        '<div class="performerSelect">' +
926
        '<div class="input-group input-group">' +
927
        '<span class="input-group-addon" id="group-addon-performer"><span class="fa fa-plus"></span> </span>' +
928
        '<input class="newPerformer form-control" placeholder="New performer (Firstname Initial)"/>' +
929
        '</div>' +
930
931
        '<div class="performerControls"></div>' +
932
        '</div>' + // /performerSelect
933
        '</div>' + // /bandTabsOuter
934
        '</div>' + // /bandControls
935
        '</div>' + // /editTicketBandColumn
936
        '<div class="clearfix"></div>' + // Clear after editTicketBandColumn
937
        '</div>' + // /editTicketInner
938
        '</div>' // /editTicket
939
      );
940
941
      this.manageInstrumentTabsTemplate = Handlebars.compile(
942
        '{{#each this}}' +
943
        ' <div class="instrument instrument{{ this.abbreviation }}" ' +
944
        '   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.instrumentNames}}</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
        '<span id="performSongButton" class="btn btn-success btn-lg" ' +
961
        'style="display: none; margin-top: 6px;">Perform this song</span> ' +
962
        '</div>'
963
      );
964
965
      this.selfSubmitTemplate = Handlebars.compile(
966
        // { song, players }
967
        '<p>Enter each performer as "firstname initial" (eg "David B") the same way each time,' +
968
        'so that we can uniquely identify you and ensure everyone gets an equal chance to perform. ' +
969
        'Performers can have up to three pending songs in the queue at a time ' +
970
        '(shown with <span class="fa fa-pause"></span> for users with 3+).</p>' +
971
        '<table class="table table-striped">' +
972
        '{{#each song.instruments}}' +
973
        '<tr class="instrumentRow" data-instrument="{{ this.abbreviation }}">' +
974
        '<td class="performerControlsCell"><p><b>{{ this.name }} performer</b></p>' +
975
        '<div class="performerControls"></div>' +
976
        '<p>Or add a new name: <input class="performer" data-instrument="{{ this.abbreviation }}"/></p>' +
977
        '</td><td class="noMobile"><span class="fa fa-arrow-right fa-3x" style="color: #999"></span></td>' +
978
        '<td><p><b>{{ this.name }}</b></p><div class="performerList performerList_{{ this.abbreviation }}"></div> ' +
979
        '</td></tr>{{/each}}' +
980
        '</table>'
981
      );
982
983
      this.ticketSubmitTemplate = Handlebars.compile(
984
        '<h4>Your Request Slip</h4>' +
985
        '<div class="songDetails"><p><b>{{song.artist}}: {{song.title}}</b></p>' +
986
        // '<div class="bandName"><input type="text" placeholder="Your band name (optional)"/></div> ' +
987
        '{{#each band}}' +
988
        '<span class="instrumentTag">{{instrumentIcon @key}}</span> ' +
989
        '<span class="instrumentPerformers">{{commalist this}}</span><br />' +
990
        '{{/each}}' +
991
        '<div class="submitControls">' +
992
        '<input type=text title="Secret code" class="submissionKey" placeholder="Code needed" />' +
993
        '<span class="btn btn-primary submitUserTicketButton">Submit this</span></div>' +
994
        '</div>');
995
    },
996
997
    ticketOrderChanged: function() {
998
      var that = this;
999
      var idOrder = [];
1000
      $('#target').find('.ticket').each(
1001
        function() {
1002
          var ticketBlock = $(this);
1003
          var ticketId = ticketBlock.data('ticketId');
1004
          idOrder.push(ticketId);
1005
        }
1006
      );
1007
1008
      that.showAppMessage('Updating ticket order');
1009
      $.ajax({
1010
        method: 'POST',
1011
        data: {
1012
          idOrder: idOrder
1013
        },
1014
        url: '/api/newOrder',
1015
        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...
1016
          // FIXME check return status
1017
          that.showAppMessage('Saved revised order', 'success');
1018
        },
1019
        error: function(xhr, status, error) {
1020
          var message = 'Failed to save revised order';
1021
          that.reportAjaxError(message, xhr, status, error);
1022
        }
1023
      });
1024
1025
      this.updatePerformanceStats();
1026
    },
1027
1028
    performButtonCallback: function(button) {
1029
      var that = this;
1030
1031
      button = $(button);
1032
      var ticketId = button.data('ticketId');
1033
      that.showAppMessage('Mark ticket used');
1034
      $.ajax({
1035
          method: 'POST',
1036
          data: {
1037
            ticketId: ticketId
1038
          },
1039
          url: '/api/useTicket',
1040
          success: function(data, status) {
1041
            that.showAppMessage('Marked ticket used', 'success');
1042
            void(data);
1043
            void(status);
1044
            var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1045
            ticketBlock.addClass('used');
1046
            // TicketBlock.append(' (done)');
1047
1048
            // Fixme receive updated ticket info from API
1049
            var ticket = ticketBlock.data('ticket');
1050
            ticket.startTime = Date.now() / 1000;
1051
            ticket.used = true;
1052
            ticketBlock.data('ticket', ticket);
1053
1054
            that.updatePerformanceStats();
1055
          },
1056
          error: function(xhr, status, error) {
1057
            var message = 'Failed to mark ticket used';
1058
            that.reportAjaxError(message, xhr, status, error);
1059
          }
1060
        }
1061
      );
1062
    },
1063
1064
    removeButtonCallback: function(button) {
1065
      var that = this;
1066
      button = $(button);
1067
      var ticketId = button.data('ticketId');
1068
      that.showAppMessage('Deleting ticket');
1069
      $.ajax({
1070
          method: 'POST',
1071
          data: {
1072
            ticketId: ticketId
1073
          },
1074
          url: '/api/deleteTicket',
1075
          success: function(data, status) {
1076
            that.showAppMessage('Deleted ticket', 'success');
1077
            void(status);
1078
            var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1079
            ticketBlock.remove();
1080
            that.updatePerformanceStats();
1081
          },
1082
          error: function(xhr, status, error) {
1083
            var message = 'Failed to deleted ticket';
1084
            that.reportAjaxError(message, xhr, status, error);
1085
          }
1086
        }
1087
      );
1088
    },
1089
1090
    editButtonCallback: function(button) {
1091
      var that = this;
1092
      button = $(button);
1093
      var ticketId = button.data('ticketId');
1094
1095
      var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1096
      var ticket = ticketBlock.data('ticket'); // TODO possibly load from ajax instead?
1097
      that.resetEditTicketBlock(ticket);
1098
    },
1099
1100
    enableAcSongSelector: function(outerElement, songClickHandler) {
1101
      var that = this;
1102
      outerElement.find('.acSong').click(
1103
        function() {
1104
          // Find & decorate clicked element
1105
          outerElement.find('.acSong').removeClass('selected');
1106
          $(this).addClass('selected');
1107
1108
          var song = $(this).data('song');
1109
          songClickHandler.call(that, song); // Run in 'that' context
1110
        }
1111
      );
1112
    },
1113
1114
1115
    searchPageSongSelectionClick: function(song) {
1116
      // Don't allow "Perform this song" if it's already in the queue (song.queued)
1117
      if (song.queued && this.displayOptions.selfSubmission) {
1118
        window.alert('Song taken, please choose another');
1119
        return;
1120
      }
1121
1122
      var target = $('#searchTarget');
1123
      song.instrumentNames = song.instruments.map(function(s) {
1124
        return s.name;
1125
      }); // Unwrap objects
1126
      target.html(this.songDetailsTemplate({song: song}));
1127
      $('#userTicketConfirmFormOuter').html('').hide(); // Remove older messages
1128
1129
      if (this.displayOptions.selfSubmission) {
1130
        var that = this;
1131
        target.find('#performSongButton').show().click(function() {
1132
          that.performSongButtonClick(song);
1133
        });
1134
      }
1135
      // Scroll to choice
1136
      $('html, body').animate({
1137
        scrollTop: (target.offset().top)
1138
      }, 50);
1139
    },
1140
1141
    /**
1142
     * Callback that opens & builds the self-submission form
1143
     *
1144
     * @param song
1145
     */
1146
    performSongButtonClick: function(song) {
1147
      $('.songComplete').hide();
1148
      var userSubmitFormOuter = $('#userSubmitFormOuter');
1149
      userSubmitFormOuter.html(this.selfSubmitTemplate({song: song}));
1150
1151
      var band = {};
1152
      var that = this;
1153
1154
      this.reloadPerformers(function() {
1155
        that.drawPerformerButtonsForAllInstruments(userSubmitFormOuter, band, song);
1156
1157
        // Also enable text input
1158
        // Copy band name into summary area on Enter
1159
        userSubmitFormOuter.find('input.performer').keydown(function(e) {
1160
          if (e.keyCode === 13) {
1161
            var input = $(this);
1162
            var instrument = input.data('instrument');
1163
            var performer = input.val().trim();
1164
            if (performer.match(/\w+\s\w+/)) {
1165
              band[instrument] = band[instrument] ? band[instrument] : [];
1166
              that.alterInstrumentPerformerList(band, instrument, performer, true);
1167
              var containingRow = $('.instrumentRow[data-instrument=' + instrument + ']');
1168
              // Display players under instrument
1169
              $(containingRow).find('.performerList').text(band[instrument].join(', ')); // FIXME display more neatly?
1170
              // after all changes, redraw ALL buttons
1171
              that.drawPerformerButtonsForAllInstruments($('#userSubmitFormOuter'), band, song);
1172
1173
              input.val('');
1174
              that.drawConfirmTicketFormIfValid('#userTicketConfirmFormOuter', band, song);
1175
            } else {
1176
              window.alert('Name format must be Forename Initial');
1177
            }
1178
          }
1179
        });
1180
1181
        // END enable text input
1182
1183
        userSubmitFormOuter.show();
1184
        $('html, body').animate({
1185
          scrollTop: (userSubmitFormOuter.offset().top)
1186
        }, 50);
1187
      });
1188
    },
1189
1190
    /**
1191
     *
1192
     * @param userSubmitFormOuter
1193
     * @param band
1194
     * @param song
1195
     */
1196
    drawPerformerButtonsForAllInstruments: function(userSubmitFormOuter, band, song) {
1197
      var that = this;
1198
      userSubmitFormOuter.find('tr.instrumentRow').each(
1199
        function() {
1200
          var element = $(this);
1201
          var instrument = element.data('instrument');
1202
          that.drawPerformerButtonsForInstrumentInBand(element.find('.performerControls'), instrument, band, song);
1203
        }
1204
      );
1205
    },
1206
1207
    /**
1208
     * Compare rebuildPerformerList (Manage Tickets page)
1209
     *
1210
     * @param targetElement
1211
     * @param instrumentCode
1212
     * @param band
1213
     * @param song
1214
     */
1215
    drawPerformerButtonsForInstrumentInBand: function(targetElement, instrumentCode, band, song) {
1216
      var that = this;
1217
      var clickCallback = function() {
1218
        var button = $(this);
1219
        if (button.attr('disabled')) { // Ignore these buttons
1220
          return;
1221
        }
1222
        var instrument = button.data('instrument');
1223
        var performer = button.data('performer');
1224
        band[instrument] = band[instrument] ? band[instrument] : [];
1225
        var existingInstrument = that.findPerformerInstrument(performer, band);
1226
        if (existingInstrument) {
1227
          // TODO If it's this instrument, remove this user
1228
          that.alterInstrumentPerformerList(band, instrument, performer, false);
1229
          // TODO If it's another instrument… we may have an issue (interface should (be made to) block this)
1230
        } else {
1231
          // Add this performer
1232
          that.alterInstrumentPerformerList(band, instrument, performer, true);
1233
        }
1234
1235
        var containingRow = $('.instrumentRow[data-instrument=' + instrument + ']'); // Display players under instrument
1236
        $(containingRow).find('.performerList').text(band[instrument].join(', ')); // FIXME display more neatly?
1237
1238
        // after all changes, redraw ALL buttons
1239
        that.drawPerformerButtonsForAllInstruments($('#userSubmitFormOuter'), band, song);
1240
1241
        that.drawConfirmTicketFormIfValid('#userTicketConfirmFormOuter', band, song);
1242
      };
1243
1244
      // $(targetElement).html('PerformerButtons '+ instrumentCode);
1245
      var newButton;
1246
      // FIXME: need to load latest performers list
1247
      targetElement.text(''); // Remove existing list
1248
1249
      var lastInitial = '';
1250
      var performerCount = this.performers.length; // Legitimately global (performers is app-wide)
1251
      var letterSpan;
1252
      for (var pIdx = 0; pIdx < performerCount; pIdx++) {
1253
        var performer = this.performers[pIdx];
1254
        var performerName = performer.performerName;
1255
        var performerInstrument = this.findPerformerInstrument(performerName, band);
1256
        var isPerforming = performerInstrument ? 1 : 0;
1257
        var initialLetter = performerName.charAt(0).toUpperCase();
1258
        if (lastInitial !== initialLetter) { // If we're changing letter
1259
          if (letterSpan) {
1260
            targetElement.append(letterSpan); // Stash the previous letterspan if present
1261
          }
1262
          letterSpan = $('<span class="letterSpan"></span>'); // Create a new span
1263
          if ((performerCount > 15)) {
1264
            letterSpan.append($('<span class="initialLetter">' + initialLetter + '</span>'));
1265
          }
1266
        }
1267
        lastInitial = initialLetter;
1268
1269
        newButton = $('<span></span>');
1270
        newButton.text(performerName);
1271
        newButton.addClass('btn addPerformerButton').data({
1272
          instrument: instrumentCode,
1273
          performer: performerName
1274
        });
1275
1276
        newButton.addClass(isPerforming ? 'btn-primary' : 'btn-default');
1277
        if (isPerforming && (performerInstrument !== instrumentCode)) { // Dim out buttons for other instruments
1278
          newButton.attr('disabled', 'disabled');
1279
        }
1280
1281
        if (performer.songsPending > 2) {
1282
          newButton.attr('disabled', 'disabled');
1283
          newButton.addClass('notAvailable');
1284
          newButton.html(newButton.html() + ' <span class="fa fa-pause"></span>');
1285
        }
1286
1287
        newButton.data('selected', isPerforming); // This is where it gets fun - check if user is in band!
1288
        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...
1289
      }
1290
      targetElement.append(letterSpan);
1291
      targetElement.find('.addPerformerButton').click(clickCallback);
1292
    },
1293
1294
    bandMemberCount: function(bandObject) {
1295
      var count = 0;
1296
      for (var instrument in bandObject) {
1297
        if (bandObject.hasOwnProperty(instrument)) {
1298
          if (Array.isArray(bandObject[instrument])) {
1299
            count += bandObject[instrument].length;
1300
          }
1301
        }
1302
      }
1303
      return count;
1304
    },
1305
1306
    drawConfirmTicketFormIfValid: function(element, band, song) {
1307
      var formBlock = $(element);
1308
      var ticket = {};
1309
      var that = this;
1310
1311
      // X console.log(['song', song]);
1312
      // X console.log(['band', band, this.bandMemberCount(band)]);
1313
1314
      if (song && song.id && band && this.bandMemberCount(band)) {
1315
        formBlock.show();
1316
        formBlock.html('TICKET FORM');
1317
        ticket.song = song;
1318
        ticket.songId = song.id;
1319
        ticket.band = band;
1320
1321
        formBlock.html(this.ticketSubmitTemplate(ticket));
1322
1323
        var submitButton = formBlock.find('.submitUserTicketButton');
1324
        var submissionKeyInput = formBlock.find('.submissionKey');
1325
        if (this.displayOptions.selfSubmissionKeyNeeded && !that.displayOptions.isAdmin) {
1326
          submitButton.attr('disabled', 'disabled');
1327
          submissionKeyInput.keydown(
1328
            function() {
1329
              if ($(this).val().length) {
1330
                submitButton.removeAttr('disabled');
1331
              } else {
1332
                submitButton.attr('disabled', 'disabled');
1333
              }
1334
            }
1335
          );
1336
        } else {
1337
          submissionKeyInput.hide();
1338
        }
1339
1340
        submitButton.click(function() {
1341
          if (that.displayOptions.selfSubmissionKeyNeeded && !that.displayOptions.isAdmin) {
1342
            ticket.submissionKey = submissionKeyInput.val();
1343
            if (!ticket.submissionKey.trim()) {
1344
              window.alert('Please check submission code');
1345
              return false;
1346
            }
1347
          }
1348
          $.ajax({
1349
              method: 'POST',
1350
              data: ticket,
1351
              url: '/api/saveTicket',
1352
              success: function(data, status) {
1353
                void(status);
1354
1355
                if (data.ticket) {
1356
                  that.showAppMessage('Saved ticket', 'success');
1357
                  formBlock.html('<div class="alert alert-success" role="alert">Ticket submitted</div>');
1358
                  $('#userSubmitFormOuter').hide().html('');
1359
                  $('#searchTarget').html('');
1360
                } else if (data.error && (data.error === 'E_BAD_SECRET')) {
1361
                  window.alert('Please check submission code');
1362
                } else {
1363
                  that.showAppMessage('Error saving ticket', 'danger');
1364
                  formBlock.html(
1365
                    '<div class="alert alert-danger" role="alert"><p>Unable to save ticket: ' +
1366
                    (data.message ? data.message : 'Internal Error') +
1367
                    '</p><p>Please reload the page to try again</p>' +
1368
                    '</div>');
1369
                }
1370
1371
              },
1372
              error: function(xhr, status, error) {
1373
                var message = 'Ticket save failed';
1374
                that.reportAjaxError(message, xhr, status, error);
1375
                void(error);
1376
                formBlock.html('<div class="alert alert-danger" role="alert">Internal error saving ticket</div>');
1377
              }
1378
            }
1379
          );
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1380
        });
1381
      }
1382
1383
      /* Data format
1384
       var data = {
1385
       title: ticketTitle,
1386
       songId: songId,
1387
       band: currentBand,
1388
       private: isPrivate, // n/a
1389
       blocking: isBlocked // n/a
1390
       };
1391
1392
       // see: url: '/api/saveTicket'
1393
       // probably admin-only at the moment
1394
       */
1395
1396
    },
1397
1398
    updatePerformanceStats: function() {
1399
      var that = this;
1400
      var performed = {};
1401
      var lastByPerformer = {};
1402
      var ticketOrdinal = 1;
1403
      var ticketTime = null;
1404
1405
      var pad = function(number) {
1406
        if (number < 10) {
1407
          return '0' + number;
1408
        }
1409
        return number;
1410
      };
1411
1412
      // First check number of songs performed before this one
1413
      var sortContainer = $('.sortContainer');
1414
      var lastSongDuration = null;
1415
      var lastTicketNoSong = true;
1416
1417
      var nthUnused = 1;
1418
1419
      sortContainer.find('.ticket').each(function() {
1420
        var realTime;
1421
        var ticketId = $(this).data('ticket-id');
1422
        var ticketData = $(this).data('ticket');
1423
1424
        if (ticketData.startTime) {
1425
          realTime = new Date(ticketData.startTime * 1000);
1426
        }
1427
1428
        $(this).removeClass('shown');
1429
1430
        if (!(ticketData.used || ticketData.private)) {
1431
          if (nthUnused <= that.displayOptions.upcomingCount) {
1432
            $(this).addClass('shown');
1433
          }
1434
          nthUnused++;
1435
        }
1436
1437
        $(this).find('.ticketOrdinal').text('# ' + ticketOrdinal);
1438
        // Fixme read ticketStart from data if present
1439
        if (realTime) {
1440
          ticketTime = realTime;
1441
        } else if (ticketTime) {
1442
          // If last song had an implicit time, add defaultSongOffsetMs to it and assume next song starts then
1443
          // If this is in the past, assume it starts now!
1444
          var songOffsetMs;
1445
          if (lastTicketNoSong) {
1446
            songOffsetMs = that.defaultSongIntervalSeconds * 1000;
1447
            // Could just be a message, could be a reset / announcement, so treat as an interval only
1448
          } else if (lastSongDuration) {
1449
            songOffsetMs = (that.defaultSongIntervalSeconds + lastSongDuration) * 1000;
1450
          } else {
1451
            songOffsetMs = (that.defaultSongIntervalSeconds + that.defaultSongLengthSeconds) * 1000;
1452
          }
1453
          ticketTime = new Date(Math.max(ticketTime.getTime() + songOffsetMs, Date.now()));
1454
        } else {
1455
          ticketTime = new Date();
1456
        }
1457
        $(this).find('.ticketTime').text(pad(ticketTime.getHours()) + ':' + pad(ticketTime.getMinutes()));
1458
1459
        // Update performer stats (done/total)
1460
        $(this).find('.performer').each(function() {
1461
          var performerId = $(this).data('performer-id');
1462
          var performerName = $(this).data('performer-name');
1463
          if (!performed.hasOwnProperty(performerId)) {
1464
            performed[performerId] = 0;
1465
          }
1466
          $(this).find('.songsDone').text(performed[performerId]);
1467
1468
          $(this).removeClass(
1469
            function(i, oldClass) {
1470
              void(i);
1471
              var classes = oldClass.split(' ');
1472
              var toRemove = [];
1473
              for (var cIdx = 0; cIdx < classes.length; cIdx++) {
1474
                if (classes[cIdx].match(/^performerDoneCount/)) {
1475
                  toRemove.push(classes[cIdx]);
1476
                }
1477
              }
1478
              return toRemove.join(' ');
1479
            }
1480
          ).addClass('performerDoneCount' + performed[performerId]);
1481
          performed[performerId]++;
1482
1483
          // Now check proximity of last song by this performer
1484
          if (lastByPerformer.hasOwnProperty(performerId)) {
1485
            var distance = ticketOrdinal - lastByPerformer[performerId].idx;
1486
            $(this).removeClass('proximityIssue');
1487
            $(this).removeClass('proximityIssue1');
1488
            if ((distance < 3) && (performerName.charAt(0) !== '?')) {
1489
              $(this).addClass('proximityIssue');
1490
              if (distance === 1) {
1491
                $(this).addClass('proximityIssue1');
1492
              }
1493
            }
1494
          } else {
1495
            // Make sure they've not got a proximity marker on a ticket that's been dragged to top
1496
            $(this).removeClass('proximityIssue');
1497
          }
1498
          lastByPerformer[performerId] = {idx: ticketOrdinal, ticketId: ticketId};
1499
        });
1500
        ticketOrdinal++;
1501
1502
        if (ticketData.song) {
1503
          lastSongDuration = ticketData.song.duration;
1504
          lastTicketNoSong = false;
1505
        } else {
1506
          lastSongDuration = 0;
1507
          lastTicketNoSong = true;
1508
        } // Set non-song ticket to minimum duration
1509
      });
1510
1511
      // Then update all totals
1512
      sortContainer.find('.performer').each(function() {
1513
        var performerId = $(this).data('performer-id');
1514
        var totalPerformed = performed[performerId];
1515
        $(this).find('.songsTotal').text(totalPerformed);
1516
      });
1517
    },
1518
1519
    /**
1520
     * Show a message in the defined appMessageTarget (f any)
1521
     *
1522
     * @param message {string} Message to show (replaces any other)
1523
     * @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...
1524
     */
1525
    showAppMessage: function(message, className) {
1526
      var that = this;
1527
      if (this.messageTimer) {
1528
        clearTimeout(this.messageTimer);
1529
      }
1530
1531
      this.messageTimer = setTimeout(function() {
1532
        that.appMessageTarget.html('');
1533
      }, 5000);
1534
1535
      if (!className) {
1536
        className = 'info';
1537
      }
1538
      if (this.appMessageTarget) {
1539
        var block = $('<div />').addClass('alert alert-' + className);
1540
        block.text(message);
1541
        this.appMessageTarget.html('').append(block);
1542
      }
1543
    },
1544
1545
    ucFirst: function(string) {
1546
      return string.charAt(0).toUpperCase() + string.slice(1);
1547
    },
1548
1549
    reportAjaxError: function(message, xhr, status, error) {
1550
      this.showAppMessage(
1551
        this.ucFirst(status) + ': ' + message + ': ' + error + ', ' + xhr.responseJSON.error,
1552
        'danger'
1553
      );
1554
    },
1555
1556
    checkRemoteRedirect: function() {
1557
      window.setInterval(function() {
1558
          $.get('/api/remotesRedirect', function(newPath) {
1559
            if (newPath && (newPath !== window.location.pathname)) {
1560
              window.location.pathname = newPath;
1561
            }
1562
          });
1563
        },
1564
        10000);
1565
    },
1566
1567
    reloadPerformers: function(callback) {
1568
      var that = this;
1569
      $.get('/api/performers', function(performers) {
1570
        that.performers = performers;
1571
        if (callback) {
1572
          callback();
1573
        }
1574
      });
1575
    },
1576
1577
1578
    /**
1579
     * Return the instrument abbreviation played by a given performer name
1580
     *
1581
     * @param performerName
1582
     * @param band
1583
     * @returns {*}
1584
     */
1585
    findPerformerInstrument: function(performerName, band) {
1586
      var instrumentPlayers;
1587
      for (var instrumentCode in band) {
1588
        if (band.hasOwnProperty(instrumentCode)) {
1589
          instrumentPlayers = band[instrumentCode];
1590
          for (var i = 0; i < instrumentPlayers.length; i++) {
1591
            if (instrumentPlayers[i].toUpperCase() === performerName.toUpperCase()) {
1592
              // X console.log(performerName + ' plays ' + instrumentCode);
1593
              return instrumentCode;
1594
            }
1595
          }
1596
        }
1597
      }
1598
      return null;
1599
    },
1600
1601
    /**
1602
     * Handle performer add / remove by performer button / text input
1603
     *
1604
     * @param band Object map of instrument: [performers]
1605
     * @param instrument Instrument code
1606
     * @param changedPerformer Performer to add or remove
1607
     * @param isAdd If true, add performer, else remove
1608
     */
1609
    alterInstrumentPerformerList: function(band, instrument, changedPerformer, isAdd) {
1610
      var that = this;
1611
      var currentInstrumentPerformers = band[instrument];
1612
1613
      var newInstrumentPerformers = [];
1614
      for (var i = 0; i < currentInstrumentPerformers.length; i++) {
1615
        var member = currentInstrumentPerformers[i].trim(); // Trim only required when we draw data from manual input
1616
        if (member.length) {
1617
          if (member.toUpperCase() !== changedPerformer.toUpperCase()) {
1618
            // If it's not the name on our button, no change
1619
            newInstrumentPerformers.push(member);
1620
          }
1621
        }
1622
      }
1623
1624
      if (isAdd) { // If we've just selected a new user, append them
1625
        newInstrumentPerformers.push(changedPerformer);
1626
        if (!that.performerExists(changedPerformer)) {
1627
          that.addPerformerByName(changedPerformer);
1628
        }
1629
      }
1630
1631
      band[instrument] = newInstrumentPerformers;
1632
1633
    }
1634
  };
1635
}());