codeapp-js 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/AI/skills/autoreview/SKILL.md +69 -0
  2. package/AI/skills/connections/SKILL.md +4 -4
  3. package/AI/skills/dataverse/SKILL.md +4 -2
  4. package/AI/skills/keyvault/SKILL.md +139 -0
  5. package/AI/skills/office365-groups/SKILL.md +46 -25
  6. package/AI/skills/office365-outlook/SKILL.md +56 -25
  7. package/AI/skills/office365-users/SKILL.md +41 -36
  8. package/AI/skills/sharepoint/SKILL.md +174 -31
  9. package/AI/skills/start/SKILL.md +1 -0
  10. package/codeApp/dist/connectors/autoreview.js +1654 -0
  11. package/codeApp/dist/connectors/office365groups.js +2995 -432
  12. package/{examples/outlook Demo/.power/schemas/office365/office365.Schema.json → codeApp/dist/connectors/office365outlook.js} +7439 -16
  13. package/codeApp/dist/connectors/office365users.js +2990 -349
  14. package/codeApp/dist/connectors/sharepoint.js +529 -308
  15. package/examples/combined demo/dist/connectors/office365outlook.js +28521 -0
  16. package/examples/combined demo/dist/connectors/office365users.js +3154 -0
  17. package/examples/combined demo/dist/index.js +2 -6
  18. package/examples/combined demo/power.config.json +1 -1
  19. package/examples/groups Demo/{.power/schemas/office365groups/office365groups.Schema.json → dist/connectors/office365groups.js } +3205 -2204
  20. package/examples/groups Demo/dist/index.js +1 -5
  21. package/examples/groups Demo/power.config.json +1 -1
  22. package/examples/myProfile/dist/connectors/office365users.js +3154 -0
  23. package/examples/myProfile/dist/index.js +1 -5
  24. package/examples/myProfile/power.config.json +1 -1
  25. package/examples/outlook Demo/dist/connectors/office365outlook.js +28521 -0
  26. package/examples/outlook Demo/dist/index.js +2 -5
  27. package/examples/outlook Demo/power.config.json +1 -1
  28. package/examples/sharePoint Demo/dist/connectors/sharepoint.js +687 -0
  29. package/examples/sharePoint Demo/dist/index.js +86 -127
  30. package/examples/sharePoint Demo/power.config.json +1 -1
  31. package/package.json +1 -1
  32. package/codeApp/.power/schemas/appschemas/dataSourcesInfo.ts +0 -6275
  33. package/codeApp/.power/schemas/jira/jira.Schema.json +0 -6903
  34. package/codeApp/.power/schemas/keyvault/keyvault.Schema.json +0 -1600
  35. package/codeApp/.power/schemas/office365groups/office365groups.Schema.json +0 -2204
  36. package/codeApp/.power/schemas/teams/teams.Schema.json +0 -11112
  37. package/codeApp/dist/connectors/outlook.js +0 -1393
  38. package/codeApp/src/generated/models/AzureKeyVaultModel.ts +0 -107
  39. package/codeApp/src/generated/models/JiraModel.ts +0 -501
  40. package/codeApp/src/generated/models/Office365GroupsModel.ts +0 -363
  41. package/codeApp/src/generated/models/Office365OutlookModel.ts +0 -2046
  42. package/codeApp/src/generated/models/Office365UsersModel.ts +0 -254
  43. package/codeApp/src/generated/services/AzureKeyVaultService.ts +0 -257
  44. package/codeApp/src/generated/services/JiraService.ts +0 -1124
  45. package/codeApp/src/generated/services/Office365GroupsService.ts +0 -326
  46. package/codeApp/src/generated/services/Office365OutlookService.ts +0 -2476
  47. package/codeApp/src/generated/services/Office365UsersService.ts +0 -358
  48. package/examples/apps/kanban/dist/dataverse.js +0 -94
  49. package/examples/apps/kanban/dist/environmentVar.js +0 -55
  50. package/examples/apps/kanban/dist/index.css +0 -605
  51. package/examples/apps/kanban/dist/index.html +0 -21
  52. package/examples/apps/kanban/dist/index.js +0 -860
  53. package/examples/apps/kanban/dist/office365groups.js +0 -97
  54. package/examples/apps/kanban/dist/office365users.js +0 -451
  55. package/examples/apps/kanban/dist/outlook.js +0 -162
  56. package/examples/apps/kanban/dist/power-apps-data.js +0 -2953
  57. package/examples/apps/kanban/dist/sharepoint.js +0 -435
  58. package/examples/apps/kanban/power.config.json +0 -35
  59. package/examples/apps/kanban/src/generated/index.ts +0 -14
  60. package/examples/apps/kanban/src/generated/models/Office365GroupsModel.ts +0 -363
  61. package/examples/apps/kanban/src/generated/models/Office365OutlookModel.ts +0 -2046
  62. package/examples/apps/kanban/src/generated/models/Office365UsersModel.ts +0 -254
  63. package/examples/apps/kanban/src/generated/services/Office365GroupsService.ts +0 -326
  64. package/examples/apps/kanban/src/generated/services/Office365OutlookService.ts +0 -2476
  65. package/examples/apps/kanban/src/generated/services/Office365UsersService.ts +0 -358
  66. package/examples/apps/planning Poker/additional files/AgilePoker_1_0_0_1.zip +0 -0
  67. package/examples/apps/planning Poker/additional files/PokerTables_1_0_0_1.zip +0 -0
  68. package/examples/apps/planning Poker/additional files/customizations (tables).xml +0 -6429
  69. package/examples/apps/planning Poker/additional files/dataverse-tables.json +0 -165
  70. package/examples/apps/planning Poker/additional files/readme.md +0 -122
  71. package/examples/apps/planning Poker/dist/dataverse.js +0 -78
  72. package/examples/apps/planning Poker/dist/index.html +0 -198
  73. package/examples/apps/planning Poker/dist/index.js +0 -955
  74. package/examples/apps/planning Poker/dist/power-apps-data.js +0 -2953
  75. package/examples/apps/planning Poker/dist/styles.css +0 -815
  76. package/examples/apps/planning Poker/power.config.json +0 -50
  77. package/examples/apps/solution explorer/dist/codeapp.js +0 -1098
  78. package/examples/apps/solution explorer/dist/icon-512.png +0 -0
  79. package/examples/apps/solution explorer/dist/index.html +0 -80
  80. package/examples/apps/solution explorer/dist/index.js +0 -735
  81. package/examples/apps/solution explorer/dist/power-apps-data.js +0 -3007
  82. package/examples/apps/solution explorer/dist/styles.css +0 -571
  83. package/examples/apps/solution explorer/power.config.json +0 -151
  84. package/examples/apps/todo/dist/dataverse.js +0 -64
  85. package/examples/apps/todo/dist/icon192.png +0 -0
  86. package/examples/apps/todo/dist/index.html +0 -75
  87. package/examples/apps/todo/dist/index.js +0 -9
  88. package/examples/apps/todo/dist/power-apps-data.js +0 -2953
  89. package/examples/apps/todo/dist/renderer.js +0 -375
  90. package/examples/apps/todo/dist/styles.css +0 -691
  91. package/examples/apps/todo/power.config.json +0 -35
  92. package/examples/combined demo/.power/schemas/appschemas/dataSourcesInfo.ts +0 -6275
  93. package/examples/combined demo/.power/schemas/jira/jira.Schema.json +0 -6903
  94. package/examples/combined demo/.power/schemas/keyvault/keyvault.Schema.json +0 -1600
  95. package/examples/combined demo/.power/schemas/teams/teams.Schema.json +0 -11112
  96. package/examples/combined demo/dist/office365users.js +0 -513
  97. package/examples/combined demo/dist/outlook.js +0 -1393
  98. package/examples/combined demo/src/generated/index.ts +0 -12
  99. package/examples/combined demo/src/generated/models/AzureKeyVaultModel.ts +0 -107
  100. package/examples/combined demo/src/generated/models/JiraModel.ts +0 -501
  101. package/examples/combined demo/src/generated/models/Office365GroupsModel.ts +0 -363
  102. package/examples/combined demo/src/generated/models/Office365OutlookModel.ts +0 -2046
  103. package/examples/combined demo/src/generated/models/Office365UsersModel.ts +0 -254
  104. package/examples/combined demo/src/generated/services/AzureKeyVaultService.ts +0 -257
  105. package/examples/combined demo/src/generated/services/JiraService.ts +0 -1124
  106. package/examples/combined demo/src/generated/services/Office365GroupsService.ts +0 -326
  107. package/examples/combined demo/src/generated/services/Office365OutlookService.ts +0 -2476
  108. package/examples/combined demo/src/generated/services/Office365UsersService.ts +0 -358
  109. package/examples/groups Demo/.power/schemas/appschemas/dataSourcesInfo.ts +0 -613
  110. package/examples/groups Demo/dist/office365groups.js +0 -642
  111. package/examples/groups Demo/src/generated/index.ts +0 -10
  112. package/examples/groups Demo/src/generated/models/Office365GroupsModel.ts +0 -363
  113. package/examples/groups Demo/src/generated/services/Office365GroupsService.ts +0 -326
  114. package/examples/myProfile/dist/office365users.js +0 -517
  115. package/examples/outlook Demo/.power/schemas/appschemas/dataSourcesInfo.ts +0 -6512
  116. package/examples/outlook Demo/dist/outlook.js +0 -1393
  117. package/examples/outlook Demo/src/generated/index.ts +0 -10
  118. package/examples/outlook Demo/src/generated/models/Office365OutlookModel.ts +0 -2046
  119. package/examples/outlook Demo/src/generated/services/Office365OutlookService.ts +0 -2476
  120. package/examples/sharePoint Demo/dist/sharepoint.js +0 -466
  121. package/examples/sharePoint Demo/src/generated/index.ts +0 -14
  122. package/examples/sharePoint Demo/src/generated/models/Office365GroupsModel.ts +0 -363
  123. package/examples/sharePoint Demo/src/generated/models/Office365OutlookModel.ts +0 -2046
  124. package/examples/sharePoint Demo/src/generated/models/Office365UsersModel.ts +0 -254
  125. package/examples/sharePoint Demo/src/generated/services/Office365GroupsService.ts +0 -326
  126. package/examples/sharePoint Demo/src/generated/services/Office365OutlookService.ts +0 -2476
  127. package/examples/sharePoint Demo/src/generated/services/Office365UsersService.ts +0 -358
  128. package/readme.md +0 -590
@@ -1,955 +0,0 @@
1
- import { createItem, listItems, updateItem, getItem, deleteItem } from './dataverse.js';
2
-
3
- /* ═══════════════════════════════════════════
4
- Planning Poker — Main Application Logic
5
- ═══════════════════════════════════════════ */
6
-
7
- // ── Table & Key Constants ──────────────────
8
- const S_TBL_SESSION = 'wd_pokersessions';
9
- const S_TBL_PARTICIPANT = 'wd_pokerparticipants';
10
- const S_TBL_ROUND = 'wd_pokerrounds';
11
- const S_TBL_VOTE = 'wd_pokervotes';
12
- const S_PK_SESSION = 'wd_pokersessionid';
13
- const S_PK_PARTICIPANT = 'wd_pokerparticipantid';
14
- const S_PK_ROUND = 'wd_pokerroundid';
15
- const S_PK_VOTE = 'wd_pokervoteid';
16
-
17
- // ── Card Values (Fibonacci + specials) ─────
18
- const A_CARD_VALUES = [
19
- { sValue: '0', sDisplay: '0' },
20
- { sValue: '1', sDisplay: '1' },
21
- { sValue: '2', sDisplay: '2' },
22
- { sValue: '3', sDisplay: '3' },
23
- { sValue: '5', sDisplay: '5' },
24
- { sValue: '8', sDisplay: '8' },
25
- { sValue: '13', sDisplay: '13' },
26
- { sValue: '21', sDisplay: '21' },
27
- { sValue: '34', sDisplay: '34' },
28
- { sValue: '?', sDisplay: '?' },
29
- { sValue: 'coffee', sDisplay: '\u2615' },
30
- { sValue: 'infinity', sDisplay: '\u221E' }
31
- ];
32
-
33
- // ── App State ──────────────────────────────
34
- let sSessionId = '';
35
- let sSessionCode = '';
36
- let sSessionName = '';
37
- let sParticipantId = '';
38
- let sDisplayName = '';
39
- let bIsOwner = false;
40
- let sCurrentRoundId = '';
41
- let sCurrentRoundIdentifier = '';
42
- let sCurrentRoundDescription = '';
43
- let sSelectedCard = '';
44
- let bHasVoted = false;
45
- let iCurrentRoundStatus = -1;
46
- let iPollInterval = null;
47
- let aParticipants = [];
48
- let aCurrentVotes = [];
49
- let sDismissedRoundId = '';
50
-
51
- // ── DOM References ─────────────────────────
52
- let eViewWelcome, eViewSession, eViewResults;
53
- let eSessionInfo, eSessionBadge, eSessionNameText, eParticipantsCount, eVoteStatus;
54
- let eNewRoundPanel, eWaitingPanel, eActiveRoundPanel;
55
- let eRoundBadge, eRoundTitle, eRoundDescription;
56
- let eParticipantsStrip, eCardHand, eCardSection;
57
- let eBtnSubmit, eBtnReveal, eVotedMessage, eVoteActions;
58
- let eResultsBadge, eResultsTitle, eStatAverage, eStatMedian, eVotesGrid;
59
- let eBtnNewRound, eBtnWaitingNext;
60
- let eFooterInfo, eErrorMessage;
61
- let eBtnEndSession, eBtnShare;
62
- let ePreviousRoundsPanel, ePreviousRoundsBody;
63
- let aPreviousRounds = [];
64
- let sSortCol = '';
65
- let sSortDir = '';
66
-
67
- function cacheDomElements() {
68
- eViewWelcome = document.getElementById('view-welcome');
69
- eViewSession = document.getElementById('view-session');
70
- eViewResults = document.getElementById('view-results');
71
-
72
- eSessionInfo = document.getElementById('session-info');
73
- eSessionBadge = document.getElementById('session-badge');
74
- eSessionNameText = document.getElementById('session-name-text');
75
- eParticipantsCount = document.getElementById('participants-count');
76
- eVoteStatus = document.getElementById('vote-status');
77
-
78
- eNewRoundPanel = document.getElementById('new-round-panel');
79
- eWaitingPanel = document.getElementById('waiting-panel');
80
- eActiveRoundPanel = document.getElementById('active-round-panel');
81
-
82
- eRoundBadge = document.getElementById('round-badge');
83
- eRoundTitle = document.getElementById('round-title');
84
- eRoundDescription = document.getElementById('round-description');
85
-
86
- eParticipantsStrip = document.getElementById('participants-strip');
87
- eCardHand = document.getElementById('card-hand');
88
- eCardSection = document.getElementById('card-section');
89
-
90
- eBtnSubmit = document.getElementById('btn-submit');
91
- eBtnReveal = document.getElementById('btn-reveal');
92
- eVotedMessage = document.getElementById('voted-message');
93
- eVoteActions = document.getElementById('vote-actions');
94
-
95
- eResultsBadge = document.getElementById('results-badge');
96
- eResultsTitle = document.getElementById('results-title');
97
- eStatAverage = document.getElementById('stat-average');
98
- eStatMedian = document.getElementById('stat-median');
99
- eVotesGrid = document.getElementById('votes-grid');
100
-
101
- eBtnNewRound = document.getElementById('btn-new-round');
102
- eBtnWaitingNext = document.getElementById('btn-waiting-next');
103
-
104
- eFooterInfo = document.getElementById('footer-info');
105
- eErrorMessage = document.getElementById('error-message');
106
- eBtnEndSession = document.getElementById('btn-end-session');
107
- eBtnShare = document.getElementById('btn-share');
108
- ePreviousRoundsPanel = document.getElementById('previous-rounds-panel');
109
- ePreviousRoundsBody = document.getElementById('previous-rounds-body');
110
- }
111
-
112
- // ── Boot ───────────────────────────────────
113
- async function boot() {
114
- cacheDomElements();
115
- setupEventListeners();
116
- renderCardHand();
117
-
118
- // Resume session from storage if available
119
- const sStoredSession = sessionStorage.getItem('pp_sessionId');
120
- const sStoredParticipant = sessionStorage.getItem('pp_participantId');
121
-
122
- if (sStoredSession && sStoredParticipant) {
123
- try {
124
- const oSession = await getItem(S_TBL_SESSION, S_PK_SESSION, sStoredSession);
125
- if (oSession && oSession.wd_isactive) {
126
- const oParticipant = await getItem(S_TBL_PARTICIPANT, S_PK_PARTICIPANT, sStoredParticipant);
127
- if (!oParticipant || !oParticipant[S_PK_PARTICIPANT]) {
128
- throw new Error('Participant not found');
129
- }
130
- sSessionId = sStoredSession;
131
- sParticipantId = sStoredParticipant;
132
- bIsOwner = !!(oSession._createdby_value && oParticipant._createdby_value && oSession._createdby_value === oParticipant._createdby_value);
133
- sSessionCode = oSession.wd_sessioncode;
134
- sSessionName = oSession.wd_name;
135
- sDisplayName = sessionStorage.getItem('pp_displayName') || '';
136
- enterSession();
137
- return;
138
- }
139
- } catch (oErr) {
140
- clearSessionStorage();
141
- }
142
- }
143
-
144
- // Check for ?session= param in URL
145
- var oParams = new URLSearchParams(window.location.search);
146
- var sJoinCode = oParams.get('session');
147
- if (sJoinCode) {
148
- document.getElementById('inp-join-code').value = sJoinCode.toUpperCase();
149
- document.getElementById('inp-join-name').focus();
150
- }
151
-
152
- showView('welcome');
153
- loadPreviousRounds();
154
- }
155
-
156
- // ── View Management ────────────────────────
157
- function showView(sView) {
158
- eViewWelcome.style.display = sView === 'welcome' ? '' : 'none';
159
- eViewSession.style.display = sView === 'session' ? '' : 'none';
160
- eViewResults.style.display = sView === 'results' ? '' : 'none';
161
-
162
- if (sView === 'welcome') {
163
- eSessionInfo.style.display = 'none';
164
- eParticipantsCount.style.display = 'none';
165
- }
166
- }
167
-
168
- async function enterSession() {
169
- showView('session');
170
- updateHeaderForSession();
171
-
172
- // Hide all sub-panels — pollSessionState will show the correct one
173
- eActiveRoundPanel.style.display = 'none';
174
- eNewRoundPanel.style.display = 'none';
175
- eWaitingPanel.style.display = 'none';
176
-
177
- await pollSessionState();
178
-
179
- // Fallback: if poll failed silently and no panel was shown, show defaults
180
- if (eActiveRoundPanel.style.display === 'none' &&
181
- eNewRoundPanel.style.display === 'none' &&
182
- eWaitingPanel.style.display === 'none' &&
183
- eViewResults.style.display === 'none') {
184
- if (bIsOwner) {
185
- eNewRoundPanel.style.display = '';
186
- } else {
187
- eWaitingPanel.style.display = '';
188
- }
189
- }
190
-
191
- stopPolling();
192
- iPollInterval = setInterval(pollSessionState, 3000);
193
- }
194
-
195
- function updateHeaderForSession() {
196
- eSessionInfo.style.display = '';
197
- eSessionBadge.textContent = sSessionCode;
198
- eSessionNameText.textContent = sSessionName;
199
- eParticipantsCount.style.display = '';
200
- document.getElementById('btn-leave').style.display = '';
201
- eBtnShare.style.display = '';
202
- eBtnEndSession.style.display = bIsOwner ? '' : 'none';
203
- eFooterInfo.textContent = 'Session: ' + sSessionName + ' \u2022 Code: ' + sSessionCode;
204
- }
205
-
206
- // ── Event Listeners ────────────────────────
207
- function setupEventListeners() {
208
- document.getElementById('btn-create').addEventListener('click', handleCreateSession);
209
- document.getElementById('btn-join').addEventListener('click', handleJoinSession);
210
- document.getElementById('btn-start-round').addEventListener('click', handleStartRound);
211
- document.getElementById('btn-leave').addEventListener('click', handleLeaveSession);
212
- eBtnEndSession.addEventListener('click', handleEndSession);
213
- eBtnShare.addEventListener('click', handleShareSession);
214
-
215
- eCardHand.addEventListener('click', (oEvent) => {
216
- const eTarget = oEvent.target.closest('.poker-card');
217
- if (eTarget && !bHasVoted) {
218
- selectCard(eTarget);
219
- }
220
- });
221
-
222
- eBtnSubmit.addEventListener('click', handleSubmitVote);
223
- eBtnReveal.addEventListener('click', handleRevealVotes);
224
- eBtnNewRound.addEventListener('click', handleNewRound);
225
- }
226
-
227
- // ── Card Rendering & Selection ─────────────
228
- function renderCardHand() {
229
- let sHtml = '';
230
- A_CARD_VALUES.forEach((oCard) => {
231
- const sSpecial = (oCard.sValue === '?' || oCard.sValue === 'coffee' || oCard.sValue === 'infinity') ? ' special' : '';
232
- sHtml = sHtml + '<div class="poker-card' + sSpecial + '" data-value="' + escapeAttr(oCard.sValue) + '">' + escapeHtml(oCard.sDisplay) + '</div>';
233
- });
234
- eCardHand.innerHTML = sHtml;
235
- }
236
-
237
- function selectCard(eCard) {
238
- const aCards = eCardHand.querySelectorAll('.poker-card');
239
- aCards.forEach((eC) => {
240
- eC.classList.remove('selected');
241
- });
242
- eCard.classList.add('selected');
243
- sSelectedCard = eCard.getAttribute('data-value');
244
- eBtnSubmit.disabled = false;
245
- }
246
-
247
- // ── Find or Create Participant (upsert) ────
248
- async function findOrCreateParticipant(sTargetSessionId, sUserName) {
249
- // Create a participant so we can identify the current user via _createdby_value
250
- const oNewParticipant = await createItem(S_TBL_PARTICIPANT, S_PK_PARTICIPANT, {
251
- 'wd_session@odata.bind': '/' + S_TBL_SESSION + '(' + sTargetSessionId + ')',
252
- wd_newcolumn: sUserName,
253
- wd_initials: getInitials(sUserName)
254
- });
255
- const sNewId = oNewParticipant[S_PK_PARTICIPANT];
256
-
257
- // Read it back to get the system _createdby_value
258
- const oFull = await getItem(S_TBL_PARTICIPANT, S_PK_PARTICIPANT, sNewId);
259
- const sUserId = oFull._createdby_value;
260
-
261
- if (!sUserId) {
262
- return sNewId;
263
- }
264
-
265
- // Check if this user already had a participant in this session
266
- const oExisting = await listItems(S_TBL_PARTICIPANT, S_PK_PARTICIPANT, {
267
- filter: '_wd_session_value eq ' + sTargetSessionId
268
- });
269
- const aOthers = (oExisting.entities || []).filter(function(oP) {
270
- return oP._createdby_value === sUserId && oP[S_PK_PARTICIPANT] !== sNewId;
271
- });
272
-
273
- if (aOthers.length > 0) {
274
- // Keep the original participant (it has votes linked to it), update its name
275
- const oKeep = aOthers[0];
276
- await updateItem(S_TBL_PARTICIPANT, S_PK_PARTICIPANT, oKeep[S_PK_PARTICIPANT], {
277
- wd_newcolumn: sUserName,
278
- wd_initials: getInitials(sUserName)
279
- });
280
- // Remove the duplicate we just created
281
- try { await deleteItem(S_TBL_PARTICIPANT, S_PK_PARTICIPANT, sNewId); } catch (e) { /* non-critical */ }
282
- return oKeep[S_PK_PARTICIPANT];
283
- }
284
-
285
- return sNewId;
286
- }
287
-
288
- // ── Create Session ─────────────────────────
289
- async function handleCreateSession() {
290
- const sName = document.getElementById('inp-session-name').value.trim();
291
- const sCode = document.getElementById('inp-session-code').value.trim().toUpperCase();
292
- const sUserName = document.getElementById('inp-create-name').value.trim();
293
-
294
- if (sName === '' || sCode === '' || sUserName === '') {
295
- showError('Please fill in all fields');
296
- return;
297
- }
298
-
299
- try {
300
- const oSession = await createItem(S_TBL_SESSION, S_PK_SESSION, {
301
- wd_name: sName,
302
- wd_sessioncode: sCode,
303
- wd_isactive: true
304
- });
305
-
306
- sSessionId = oSession[S_PK_SESSION];
307
- sSessionCode = sCode;
308
- sSessionName = sName;
309
- sDisplayName = sUserName;
310
- bIsOwner = true;
311
-
312
- sParticipantId = await findOrCreateParticipant(sSessionId, sUserName);
313
- saveSessionStorage();
314
- enterSession();
315
- } catch (oErr) {
316
- showError('Create session: ' + (oErr.message || oErr));
317
- }
318
- }
319
-
320
- // ── Join Session ───────────────────────────
321
- async function handleJoinSession() {
322
- const sCode = document.getElementById('inp-join-code').value.trim().toUpperCase();
323
- const sUserName = document.getElementById('inp-join-name').value.trim();
324
-
325
- if (sCode === '' || sUserName === '') {
326
- showError('Please fill in all fields');
327
- return;
328
- }
329
-
330
- try {
331
- const oResult = await listItems(S_TBL_SESSION, S_PK_SESSION, {
332
- filter: 'wd_sessioncode eq \'' + sCode + '\' and wd_isactive eq true'
333
- });
334
-
335
- if (!oResult.entities || oResult.entities.length === 0) {
336
- showError('Session not found. Check the code and try again.');
337
- return;
338
- }
339
-
340
- const oSession = oResult.entities[0];
341
- sSessionId = oSession[S_PK_SESSION];
342
- sSessionCode = oSession.wd_sessioncode;
343
- sSessionName = oSession.wd_name;
344
- sDisplayName = sUserName;
345
-
346
- sParticipantId = await findOrCreateParticipant(sSessionId, sUserName);
347
-
348
- // Determine ownership by comparing session creator with current user
349
- const oParticipantFull = await getItem(S_TBL_PARTICIPANT, S_PK_PARTICIPANT, sParticipantId);
350
- bIsOwner = !!(oSession._createdby_value && oParticipantFull._createdby_value && oSession._createdby_value === oParticipantFull._createdby_value);
351
-
352
- saveSessionStorage();
353
- enterSession();
354
- } catch (oErr) {
355
- showError('Join session: ' + (oErr.message || oErr));
356
- }
357
- }
358
-
359
- // ── Start Round ────────────────────────────
360
- async function handleStartRound() {
361
- const sRoundId = document.getElementById('inp-round-id').value.trim();
362
- const sDesc = document.getElementById('inp-round-desc').value.trim();
363
-
364
- if (sRoundId === '') {
365
- showError('Please enter a round ID');
366
- return;
367
- }
368
-
369
- try {
370
- const oRound = await createItem(S_TBL_ROUND, S_PK_ROUND, {
371
- 'wd_session@odata.bind': '/' + S_TBL_SESSION + '(' + sSessionId + ')',
372
- wd_roundidentifier: sRoundId,
373
- wd_description: sDesc,
374
- wd_newcolumn: '0'
375
- });
376
-
377
- sCurrentRoundId = oRound[S_PK_ROUND];
378
- sCurrentRoundIdentifier = sRoundId;
379
- sCurrentRoundDescription = sDesc;
380
- iCurrentRoundStatus = 0;
381
- bHasVoted = false;
382
- sSelectedCard = '';
383
- aCurrentVotes = [];
384
-
385
- document.getElementById('inp-round-id').value = '';
386
- document.getElementById('inp-round-desc').value = '';
387
-
388
- showActiveRound();
389
- } catch (oErr) {
390
- showError('Start round: ' + (oErr.message || oErr));
391
- }
392
- }
393
-
394
- function showActiveRound() {
395
- eNewRoundPanel.style.display = 'none';
396
- eWaitingPanel.style.display = 'none';
397
- eActiveRoundPanel.style.display = '';
398
-
399
- eRoundBadge.textContent = sCurrentRoundIdentifier;
400
- eRoundTitle.textContent = sCurrentRoundDescription || sCurrentRoundIdentifier;
401
- eRoundDescription.textContent = sCurrentRoundDescription;
402
-
403
- // Reset voting UI
404
- eCardSection.style.display = '';
405
- eVoteActions.style.display = '';
406
- eVotedMessage.style.display = 'none';
407
- eBtnSubmit.style.display = '';
408
- eBtnSubmit.disabled = true;
409
- eBtnSubmit.textContent = 'Submit Vote';
410
- eBtnReveal.style.display = bIsOwner ? '' : 'none';
411
-
412
- // Reset card selection
413
- const aCards = eCardHand.querySelectorAll('.poker-card');
414
- aCards.forEach((eC) => {
415
- eC.classList.remove('selected');
416
- });
417
- sSelectedCard = '';
418
- bHasVoted = false;
419
- }
420
-
421
- // ── Submit Vote ────────────────────────────
422
- async function handleSubmitVote() {
423
- if (sSelectedCard === '' || bHasVoted) {
424
- return;
425
- }
426
-
427
- try {
428
- // Check for an existing vote by this participant in this round
429
- const oExistingVotes = await listItems(S_TBL_VOTE, S_PK_VOTE, {
430
- filter: '_wd_round_value eq ' + sCurrentRoundId + ' and _wd_participant_value eq ' + sParticipantId
431
- });
432
-
433
- if (oExistingVotes.entities && oExistingVotes.entities.length > 0) {
434
- // Update existing vote
435
- await updateItem(S_TBL_VOTE, S_PK_VOTE, oExistingVotes.entities[0][S_PK_VOTE], {
436
- wd_score: sSelectedCard
437
- });
438
- } else {
439
- // Create new vote
440
- await createItem(S_TBL_VOTE, S_PK_VOTE, {
441
- 'wd_round@odata.bind': '/' + S_TBL_ROUND + '(' + sCurrentRoundId + ')',
442
- 'wd_participant@odata.bind': '/' + S_TBL_PARTICIPANT + '(' + sParticipantId + ')',
443
- wd_score: sSelectedCard
444
- });
445
- }
446
-
447
- bHasVoted = true;
448
- eCardSection.style.display = 'none';
449
- eVoteActions.style.display = bIsOwner ? '' : 'none';
450
- eVotedMessage.style.display = '';
451
- eBtnSubmit.style.display = 'none';
452
- } catch (oErr) {
453
- showError('Submit vote: ' + (oErr.message || oErr));
454
- }
455
- }
456
-
457
- // ── Reveal Votes ───────────────────────────
458
- async function handleRevealVotes() {
459
- if (!bIsOwner) return;
460
-
461
- try {
462
- const oVoteResult = await listItems(S_TBL_VOTE, S_PK_VOTE, {
463
- filter: '_wd_round_value eq ' + sCurrentRoundId
464
- });
465
- aCurrentVotes = oVoteResult.entities || [];
466
-
467
- const oStats = calculateStats(aCurrentVotes);
468
-
469
- await updateItem(S_TBL_ROUND, S_PK_ROUND, sCurrentRoundId, {
470
- wd_newcolumn: '1',
471
- wd_averagescore: oStats.iAverage,
472
- wd_medianscore: oStats.iMedian
473
- });
474
-
475
- iCurrentRoundStatus = 1;
476
- showResults(aCurrentVotes, oStats);
477
- } catch (oErr) {
478
- showError('Reveal votes: ' + (oErr.message || oErr));
479
- }
480
- }
481
-
482
- // ── New Round ──────────────────────────────
483
- function handleNewRound() {
484
- sDismissedRoundId = sCurrentRoundId;
485
- sCurrentRoundId = '';
486
- sCurrentRoundIdentifier = '';
487
- sCurrentRoundDescription = '';
488
- iCurrentRoundStatus = -1;
489
- bHasVoted = false;
490
- sSelectedCard = '';
491
- aCurrentVotes = [];
492
-
493
- showView('session');
494
- updateHeaderForSession();
495
- eActiveRoundPanel.style.display = 'none';
496
-
497
- if (bIsOwner) {
498
- eNewRoundPanel.style.display = '';
499
- eWaitingPanel.style.display = 'none';
500
- } else {
501
- eNewRoundPanel.style.display = 'none';
502
- eWaitingPanel.style.display = '';
503
- }
504
- }
505
-
506
- // ── Results Display ────────────────────────
507
- function showResults(aVotes, oStats) {
508
- showView('results');
509
- updateHeaderForSession();
510
-
511
- eResultsBadge.textContent = sCurrentRoundIdentifier;
512
- eResultsTitle.textContent = sCurrentRoundDescription || sCurrentRoundIdentifier;
513
- eStatAverage.textContent = oStats.iAverage !== null ? oStats.iAverage.toFixed(1) : '\u2014';
514
- eStatMedian.textContent = oStats.iMedian !== null ? oStats.iMedian.toFixed(1) : '\u2014';
515
-
516
- let sHtml = '';
517
- aVotes.forEach((oVote) => {
518
- const oParticipant = aParticipants.find((oP) => oP[S_PK_PARTICIPANT] === oVote._wd_participant_value);
519
- const sName = oParticipant ? oParticipant.wd_newcolumn : 'Unknown';
520
- const sInitials = oParticipant ? oParticipant.wd_initials : '??';
521
- const sScore = formatScoreDisplay(oVote.wd_score);
522
-
523
- sHtml = sHtml + '<div class="vote-card">' +
524
- '<div class="vote-card-avatar">' + escapeHtml(sInitials) + '</div>' +
525
- '<div class="vote-card-info">' +
526
- '<span class="vote-card-name">' + escapeHtml(sName) + '</span>' +
527
- '<span class="vote-card-value">' + escapeHtml(sScore) + '</span>' +
528
- '</div>' +
529
- '</div>';
530
- });
531
- eVotesGrid.innerHTML = sHtml;
532
-
533
- if (bIsOwner) {
534
- eBtnNewRound.style.display = '';
535
- eBtnWaitingNext.style.display = 'none';
536
- } else {
537
- eBtnNewRound.style.display = 'none';
538
- eBtnWaitingNext.style.display = '';
539
- }
540
- }
541
-
542
- // ── Polling ────────────────────────────────
543
- function startPolling() {
544
- stopPolling();
545
- pollSessionState();
546
- iPollInterval = setInterval(pollSessionState, 3000);
547
- }
548
-
549
- function stopPolling() {
550
- if (iPollInterval) {
551
- clearInterval(iPollInterval);
552
- iPollInterval = null;
553
- }
554
- }
555
-
556
- // Determine if a round is still open (no results yet)
557
- function isRoundOpen(oRound) {
558
- // A round is revealed if it has average/median scores OR wd_newcolumn status is '1'
559
- if (oRound.wd_averagescore != null) return false;
560
- if (oRound.wd_medianscore != null) return false;
561
- if (String(oRound.wd_newcolumn) === '1') return false;
562
- return true;
563
- }
564
-
565
- async function pollSessionState() {
566
- try {
567
- // Fetch participants
568
- const oParticipantResult = await listItems(S_TBL_PARTICIPANT, S_PK_PARTICIPANT, {
569
- filter: '_wd_session_value eq ' + sSessionId
570
- });
571
- aParticipants = oParticipantResult.entities || [];
572
-
573
- // Fetch ALL rounds for this session
574
- const oRoundResult = await listItems(S_TBL_ROUND, S_PK_ROUND, {
575
- filter: '_wd_session_value eq ' + sSessionId
576
- });
577
- const aAllSessionRounds = oRoundResult.entities || [];
578
-
579
- // Sort by createdon descending so we always pick the most recent
580
- aAllSessionRounds.sort(function (a, b) {
581
- return new Date(b.createdon) - new Date(a.createdon);
582
- });
583
-
584
- // Split into open rounds (no average/median) and revealed rounds
585
- // Because the array is sorted newest-first, the first match is the most recent
586
- var oOpenRound = null;
587
- var oRevealedRound = null;
588
-
589
- for (var i = 0; i < aAllSessionRounds.length; i++) {
590
- var oR = aAllSessionRounds[i];
591
- if (isRoundOpen(oR)) {
592
- if (!oOpenRound) oOpenRound = oR;
593
- } else {
594
- if (!oRevealedRound) oRevealedRound = oR;
595
- }
596
- }
597
-
598
- // If a new open round appears, clear the dismissed marker
599
- if (oOpenRound) {
600
- sDismissedRoundId = '';
601
- }
602
-
603
- // Skip re-showing a revealed round the user already dismissed
604
- if (!oOpenRound && oRevealedRound && oRevealedRound[S_PK_ROUND] === sDismissedRoundId) {
605
- oRevealedRound = null;
606
- }
607
-
608
- // Prefer showing an open round; otherwise show the latest revealed round
609
- var oRound = oOpenRound || oRevealedRound;
610
-
611
- if (oRound) {
612
- var sRoundId = oRound[S_PK_ROUND];
613
- var bIsOpen = isRoundOpen(oRound);
614
-
615
- if (bIsOpen) {
616
- // ── Active voting round ──
617
- if (sRoundId !== sCurrentRoundId || iCurrentRoundStatus !== 0) {
618
- // New or re-discovered open round — set it up
619
- sCurrentRoundId = sRoundId;
620
- sCurrentRoundIdentifier = oRound.wd_roundidentifier || '';
621
- sCurrentRoundDescription = oRound.wd_description || '';
622
- iCurrentRoundStatus = 0;
623
- bHasVoted = false;
624
- sSelectedCard = '';
625
- aCurrentVotes = [];
626
-
627
- // Check if this user already voted (handles page refresh)
628
- try {
629
- var oExisting = await listItems(S_TBL_VOTE, S_PK_VOTE, {
630
- filter: '_wd_round_value eq ' + sCurrentRoundId + ' and _wd_participant_value eq ' + sParticipantId
631
- });
632
- if (oExisting.entities && oExisting.entities.length > 0) {
633
- bHasVoted = true;
634
- }
635
- } catch (oVoteErr) {
636
- console.error('Vote check error:', oVoteErr);
637
- }
638
-
639
- showView('session');
640
- updateHeaderForSession();
641
- showActiveRound();
642
-
643
- if (bHasVoted) {
644
- eCardSection.style.display = 'none';
645
- eVoteActions.style.display = bIsOwner ? '' : 'none';
646
- eVotedMessage.style.display = '';
647
- eBtnSubmit.style.display = 'none';
648
- }
649
- } else {
650
- // Same open round — just refresh vote counts
651
- var oVoteResult = await listItems(S_TBL_VOTE, S_PK_VOTE, {
652
- filter: '_wd_round_value eq ' + sCurrentRoundId
653
- });
654
- aCurrentVotes = oVoteResult.entities || [];
655
- renderParticipantStrip();
656
- updateVoteCount();
657
- }
658
- } else {
659
- // ── Revealed round (has average/median) ──
660
- if (sRoundId !== sCurrentRoundId || iCurrentRoundStatus !== 1) {
661
- sCurrentRoundId = sRoundId;
662
- sCurrentRoundIdentifier = oRound.wd_roundidentifier || '';
663
- sCurrentRoundDescription = oRound.wd_description || '';
664
- iCurrentRoundStatus = 1;
665
-
666
- var oVoteResult = await listItems(S_TBL_VOTE, S_PK_VOTE, {
667
- filter: '_wd_round_value eq ' + sCurrentRoundId
668
- });
669
- aCurrentVotes = oVoteResult.entities || [];
670
-
671
- var oStats = calculateStats(aCurrentVotes);
672
- showResults(aCurrentVotes, oStats);
673
- } else {
674
- renderParticipantStrip();
675
- }
676
- }
677
- } else {
678
- // No rounds at all — show waiting/new round panel
679
- if (eViewResults.style.display !== 'none') {
680
- // Stay on results view until owner starts new round
681
- renderParticipantStrip();
682
- return;
683
- }
684
-
685
- showView('session');
686
- updateHeaderForSession();
687
- eActiveRoundPanel.style.display = 'none';
688
-
689
- if (bIsOwner) {
690
- eNewRoundPanel.style.display = '';
691
- eWaitingPanel.style.display = 'none';
692
- } else {
693
- eNewRoundPanel.style.display = 'none';
694
- eWaitingPanel.style.display = '';
695
- }
696
-
697
- renderParticipantStrip();
698
- }
699
- } catch (oErr) {
700
- console.error('pollSessionState error:', oErr);
701
- }
702
- }
703
-
704
- // ── Render Participants ────────────────────
705
- function renderParticipantStrip() {
706
- let sHtml = '';
707
- aParticipants.forEach((oP) => {
708
- const bVoted = aCurrentVotes.some((oV) => oV._wd_participant_value === oP[S_PK_PARTICIPANT]);
709
- const sVotedClass = bVoted ? 'voted' : 'waiting';
710
- const sOwnerClass = '';
711
- const sInitials = oP.wd_initials || '??';
712
- const sName = oP.wd_newcolumn || 'Unknown';
713
-
714
- sHtml = sHtml + '<div class="participant">' +
715
- '<div class="participant-avatar ' + sVotedClass + sOwnerClass + '">' + escapeHtml(sInitials) + '</div>' +
716
- '<span class="participant-name">' + escapeHtml(sName) + '</span>' +
717
- '</div>';
718
- });
719
- eParticipantsStrip.innerHTML = sHtml;
720
- }
721
-
722
- function updateVoteCount() {
723
- const iTotal = aParticipants.length;
724
- const iVoted = aCurrentVotes.length;
725
- eVoteStatus.textContent = iVoted + '/' + iTotal + ' voted';
726
- }
727
-
728
- // ── Statistics ─────────────────────────────
729
- function calculateStats(aVotes) {
730
- const aNumeric = aVotes
731
- .map((oV) => parseFloat(oV.wd_score))
732
- .filter((iVal) => !isNaN(iVal));
733
-
734
- if (aNumeric.length === 0) {
735
- return { iAverage: null, iMedian: null };
736
- }
737
-
738
- const iSum = aNumeric.reduce((iAcc, iVal) => iAcc + iVal, 0);
739
- const iAverage = iSum / aNumeric.length;
740
-
741
- const aSorted = aNumeric.slice().sort((iA, iB) => iA - iB);
742
- const iMid = Math.floor(aSorted.length / 2);
743
- let iMedian;
744
- if (aSorted.length % 2 === 0) {
745
- iMedian = (aSorted[iMid - 1] + aSorted[iMid]) / 2;
746
- } else {
747
- iMedian = aSorted[iMid];
748
- }
749
-
750
- return { iAverage, iMedian };
751
- }
752
-
753
- // ── Utility Functions ──────────────────────
754
- function getInitials(sName) {
755
- const aParts = sName.trim().split(new RegExp('\\s+'));
756
- if (aParts.length >= 2) {
757
- return (aParts[0][0] + aParts[aParts.length - 1][0]).toUpperCase();
758
- }
759
- return sName.substring(0, 2).toUpperCase();
760
- }
761
-
762
- function formatScoreDisplay(sScore) {
763
- if (sScore === 'coffee') return '\u2615';
764
- if (sScore === 'infinity') return '\u221E';
765
- return sScore;
766
- }
767
-
768
- function escapeHtml(sText) {
769
- const eDiv = document.createElement('div');
770
- eDiv.textContent = sText;
771
- return eDiv.innerHTML;
772
- }
773
-
774
- function escapeAttr(sText) {
775
- return sText.replace(new RegExp('&', 'g'), '&amp;')
776
- .replace(new RegExp('"', 'g'), '&quot;')
777
- .replace(new RegExp('<', 'g'), '&lt;')
778
- .replace(new RegExp('>', 'g'), '&gt;');
779
- }
780
-
781
- function showError(sMessage) {
782
- if (!eErrorMessage) return;
783
- eErrorMessage.textContent = sMessage;
784
- eErrorMessage.style.display = '';
785
- setTimeout(() => {
786
- eErrorMessage.style.display = 'none';
787
- }, 4000);
788
- }
789
- // ── Share Session ───────────────────────────
790
- async function handleShareSession() {
791
- var oUrl = new URL(window.location.href);
792
- oUrl.searchParams.set('session', sSessionCode);
793
- try {
794
- await navigator.clipboard.writeText(oUrl.toString());
795
- eBtnShare.textContent = '\u2713 Copied!';
796
- setTimeout(function () { eBtnShare.textContent = '\uD83D\uDD17 Share'; }, 2000);
797
- } catch (oErr) {
798
- showError('Could not copy link');
799
- }
800
- }
801
- // ── End Session (Owner) ────────────────────
802
- async function handleEndSession() {
803
- if (!bIsOwner) return;
804
-
805
- try {
806
- await updateItem(S_TBL_SESSION, S_PK_SESSION, sSessionId, {
807
- wd_isactive: false
808
- });
809
- handleLeaveSession();
810
- } catch (oErr) {
811
- showError('End session: ' + (oErr.message || oErr));
812
- }
813
- }
814
-
815
- // ── Leave Session ──────────────────────────
816
- function handleLeaveSession() {
817
- stopPolling();
818
- clearSessionStorage();
819
-
820
- sSessionId = '';
821
- sSessionCode = '';
822
- sSessionName = '';
823
- sParticipantId = '';
824
- sDisplayName = '';
825
- bIsOwner = false;
826
- sCurrentRoundId = '';
827
- sCurrentRoundIdentifier = '';
828
- sCurrentRoundDescription = '';
829
- sSelectedCard = '';
830
- bHasVoted = false;
831
- iCurrentRoundStatus = -1;
832
- aParticipants = [];
833
- aCurrentVotes = [];
834
- sDismissedRoundId = '';
835
-
836
- document.getElementById('btn-leave').style.display = 'none';
837
- eBtnShare.style.display = 'none';
838
- eBtnEndSession.style.display = 'none';
839
- showView('welcome');
840
- }
841
-
842
- function saveSessionStorage() {
843
- sessionStorage.setItem('pp_sessionId', sSessionId);
844
- sessionStorage.setItem('pp_participantId', sParticipantId);
845
- sessionStorage.setItem('pp_displayName', sDisplayName);
846
- }
847
-
848
- function clearSessionStorage() {
849
- sessionStorage.removeItem('pp_sessionId');
850
- sessionStorage.removeItem('pp_participantId');
851
- sessionStorage.removeItem('pp_displayName');
852
- }
853
-
854
- // ── Previous Rounds Table ──────────────────
855
- async function loadPreviousRounds() {
856
- try {
857
- var oRoundResult = await listItems(S_TBL_ROUND, S_PK_ROUND, {});
858
- var aAllRounds = oRoundResult.entities || [];
859
- var aRounds = aAllRounds.filter(function (oR) { return !isRoundOpen(oR); });
860
- if (aRounds.length === 0) {
861
- ePreviousRoundsPanel.style.display = 'none';
862
- return;
863
- }
864
-
865
- // Fetch session names for display
866
- var aSessionIds = [];
867
- aRounds.forEach(function (oR) {
868
- if (oR._wd_session_value && aSessionIds.indexOf(oR._wd_session_value) === -1) {
869
- aSessionIds.push(oR._wd_session_value);
870
- }
871
- });
872
-
873
- var oSessionMap = {};
874
- for (var i = 0; i < aSessionIds.length; i++) {
875
- try {
876
- var oSess = await getItem(S_TBL_SESSION, S_PK_SESSION, aSessionIds[i], [S_PK_SESSION, 'wd_name']);
877
- oSessionMap[aSessionIds[i]] = oSess.wd_name || 'Unknown';
878
- } catch (e) {
879
- oSessionMap[aSessionIds[i]] = 'Unknown';
880
- }
881
- }
882
-
883
- aPreviousRounds = aRounds.map(function (oR) {
884
- return {
885
- session: oSessionMap[oR._wd_session_value] || 'Unknown',
886
- roundName: oR.wd_roundidentifier || '',
887
- description: oR.wd_description || '',
888
- average: oR.wd_averagescore != null ? parseFloat(oR.wd_averagescore) : null,
889
- median: oR.wd_medianscore != null ? parseFloat(oR.wd_medianscore) : null
890
- };
891
- });
892
-
893
- renderPreviousRoundsTable();
894
- ePreviousRoundsPanel.style.display = '';
895
- setupSortButtons();
896
- } catch (oErr) {
897
- showError('Previous rounds: ' + (oErr.message || oErr));
898
- }
899
- }
900
-
901
- function renderPreviousRoundsTable() {
902
- var sHtml = '';
903
- aPreviousRounds.forEach(function (oRow) {
904
- sHtml += '<tr>' +
905
- '<td>' + escapeHtml(oRow.session) + '</td>' +
906
- '<td>' + escapeHtml(oRow.roundName) + '</td>' +
907
- '<td>' + escapeHtml(oRow.description) + '</td>' +
908
- '<td>' + (oRow.average !== null ? oRow.average.toFixed(1) : '\u2014') + '</td>' +
909
- '<td>' + (oRow.median !== null ? oRow.median.toFixed(1) : '\u2014') + '</td>' +
910
- '</tr>';
911
- });
912
- ePreviousRoundsBody.innerHTML = sHtml;
913
- }
914
-
915
- function setupSortButtons() {
916
- var aBtns = document.querySelectorAll('.sort-btn');
917
- aBtns.forEach(function (eBtn) {
918
- eBtn.addEventListener('click', function () {
919
- var sCol = eBtn.getAttribute('data-col');
920
- var sDir = eBtn.getAttribute('data-dir');
921
- var sNewDir = sDir === 'asc' ? 'desc' : 'asc';
922
-
923
- // Reset all buttons
924
- aBtns.forEach(function (eB) {
925
- eB.setAttribute('data-dir', '');
926
- eB.className = 'sort-btn';
927
- });
928
-
929
- eBtn.setAttribute('data-dir', sNewDir);
930
- eBtn.className = 'sort-btn ' + sNewDir;
931
- sSortCol = sCol;
932
- sSortDir = sNewDir;
933
-
934
- sortPreviousRounds();
935
- renderPreviousRoundsTable();
936
- });
937
- });
938
- }
939
-
940
- function sortPreviousRounds() {
941
- var iDir = sSortDir === 'desc' ? -1 : 1;
942
- aPreviousRounds.sort(function (oA, oB) {
943
- var vA = oA[sSortCol];
944
- var vB = oB[sSortCol];
945
- if (vA === null && vB === null) return 0;
946
- if (vA === null) return 1;
947
- if (vB === null) return -1;
948
- if (typeof vA === 'string') {
949
- return iDir * vA.localeCompare(vB);
950
- }
951
- return iDir * (vA - vB);
952
- });
953
- }
954
-
955
- boot();