codeapp-js 1.0.1 → 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 (130) 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/frontend-design/SKILL.md +32 -20
  5. package/AI/skills/keyvault/SKILL.md +139 -0
  6. package/AI/skills/office365-groups/SKILL.md +46 -25
  7. package/AI/skills/office365-outlook/SKILL.md +56 -25
  8. package/AI/skills/office365-users/SKILL.md +41 -36
  9. package/AI/skills/sharepoint/SKILL.md +174 -31
  10. package/AI/skills/start/SKILL.md +38 -30
  11. package/codeApp/dist/connectors/autoreview.js +1654 -0
  12. package/codeApp/dist/connectors/office365groups.js +2995 -432
  13. package/{examples/outlook Demo/.power/schemas/office365/office365.Schema.json → codeApp/dist/connectors/office365outlook.js} +7439 -16
  14. package/codeApp/dist/connectors/office365users.js +2990 -349
  15. package/codeApp/dist/connectors/sharepoint.js +529 -308
  16. package/examples/combined demo/dist/connectors/office365outlook.js +28521 -0
  17. package/examples/combined demo/dist/connectors/office365users.js +3154 -0
  18. package/examples/combined demo/dist/index.js +2 -6
  19. package/examples/combined demo/power.config.json +1 -1
  20. package/examples/groups Demo/{.power/schemas/office365groups/office365groups.Schema.json → dist/connectors/office365groups.js } +3205 -2204
  21. package/examples/groups Demo/dist/index.js +1 -5
  22. package/examples/groups Demo/power.config.json +1 -1
  23. package/examples/myProfile/dist/connectors/office365users.js +3154 -0
  24. package/examples/myProfile/dist/index.js +1 -5
  25. package/examples/myProfile/power.config.json +1 -1
  26. package/examples/outlook Demo/dist/connectors/office365outlook.js +28521 -0
  27. package/examples/outlook Demo/dist/index.js +2 -5
  28. package/examples/outlook Demo/power.config.json +1 -1
  29. package/examples/sharePoint Demo/dist/connectors/sharepoint.js +687 -0
  30. package/examples/sharePoint Demo/dist/index.js +86 -127
  31. package/examples/sharePoint Demo/power.config.json +1 -1
  32. package/package.json +1 -1
  33. package/codeApp/.power/schemas/appschemas/dataSourcesInfo.ts +0 -6275
  34. package/codeApp/.power/schemas/jira/jira.Schema.json +0 -6903
  35. package/codeApp/.power/schemas/keyvault/keyvault.Schema.json +0 -1600
  36. package/codeApp/.power/schemas/office365groups/office365groups.Schema.json +0 -2204
  37. package/codeApp/.power/schemas/teams/teams.Schema.json +0 -11112
  38. package/codeApp/dist/connectors/outlook.js +0 -1393
  39. package/codeApp/src/generated/index.ts +0 -12
  40. package/codeApp/src/generated/models/AzureKeyVaultModel.ts +0 -107
  41. package/codeApp/src/generated/models/JiraModel.ts +0 -501
  42. package/codeApp/src/generated/models/Office365GroupsModel.ts +0 -363
  43. package/codeApp/src/generated/models/Office365OutlookModel.ts +0 -2046
  44. package/codeApp/src/generated/models/Office365UsersModel.ts +0 -254
  45. package/codeApp/src/generated/services/AzureKeyVaultService.ts +0 -257
  46. package/codeApp/src/generated/services/JiraService.ts +0 -1124
  47. package/codeApp/src/generated/services/Office365GroupsService.ts +0 -326
  48. package/codeApp/src/generated/services/Office365OutlookService.ts +0 -2476
  49. package/codeApp/src/generated/services/Office365UsersService.ts +0 -358
  50. package/examples/apps/kanban/dist/dataverse.js +0 -94
  51. package/examples/apps/kanban/dist/environmentVar.js +0 -55
  52. package/examples/apps/kanban/dist/index.css +0 -605
  53. package/examples/apps/kanban/dist/index.html +0 -21
  54. package/examples/apps/kanban/dist/index.js +0 -860
  55. package/examples/apps/kanban/dist/office365groups.js +0 -97
  56. package/examples/apps/kanban/dist/office365users.js +0 -451
  57. package/examples/apps/kanban/dist/outlook.js +0 -162
  58. package/examples/apps/kanban/dist/power-apps-data.js +0 -2953
  59. package/examples/apps/kanban/dist/sharepoint.js +0 -435
  60. package/examples/apps/kanban/power.config.json +0 -35
  61. package/examples/apps/kanban/src/generated/index.ts +0 -14
  62. package/examples/apps/kanban/src/generated/models/Office365GroupsModel.ts +0 -363
  63. package/examples/apps/kanban/src/generated/models/Office365OutlookModel.ts +0 -2046
  64. package/examples/apps/kanban/src/generated/models/Office365UsersModel.ts +0 -254
  65. package/examples/apps/kanban/src/generated/services/Office365GroupsService.ts +0 -326
  66. package/examples/apps/kanban/src/generated/services/Office365OutlookService.ts +0 -2476
  67. package/examples/apps/kanban/src/generated/services/Office365UsersService.ts +0 -358
  68. package/examples/apps/planning Poker/additional files/AgilePoker_1_0_0_1.zip +0 -0
  69. package/examples/apps/planning Poker/additional files/PokerTables_1_0_0_1.zip +0 -0
  70. package/examples/apps/planning Poker/additional files/customizations (tables).xml +0 -6429
  71. package/examples/apps/planning Poker/additional files/dataverse-tables.json +0 -165
  72. package/examples/apps/planning Poker/additional files/readme.md +0 -122
  73. package/examples/apps/planning Poker/dist/dataverse.js +0 -78
  74. package/examples/apps/planning Poker/dist/index.html +0 -198
  75. package/examples/apps/planning Poker/dist/index.js +0 -955
  76. package/examples/apps/planning Poker/dist/power-apps-data.js +0 -2953
  77. package/examples/apps/planning Poker/dist/styles.css +0 -815
  78. package/examples/apps/planning Poker/power.config.json +0 -50
  79. package/examples/apps/solution explorer/dist/codeapp.js +0 -1098
  80. package/examples/apps/solution explorer/dist/icon-512.png +0 -0
  81. package/examples/apps/solution explorer/dist/index.html +0 -80
  82. package/examples/apps/solution explorer/dist/index.js +0 -735
  83. package/examples/apps/solution explorer/dist/power-apps-data.js +0 -3007
  84. package/examples/apps/solution explorer/dist/styles.css +0 -571
  85. package/examples/apps/solution explorer/power.config.json +0 -151
  86. package/examples/apps/todo/dist/dataverse.js +0 -64
  87. package/examples/apps/todo/dist/icon192.png +0 -0
  88. package/examples/apps/todo/dist/index.html +0 -75
  89. package/examples/apps/todo/dist/index.js +0 -9
  90. package/examples/apps/todo/dist/power-apps-data.js +0 -2953
  91. package/examples/apps/todo/dist/renderer.js +0 -375
  92. package/examples/apps/todo/dist/styles.css +0 -691
  93. package/examples/apps/todo/power.config.json +0 -35
  94. package/examples/combined demo/.power/schemas/appschemas/dataSourcesInfo.ts +0 -6275
  95. package/examples/combined demo/.power/schemas/jira/jira.Schema.json +0 -6903
  96. package/examples/combined demo/.power/schemas/keyvault/keyvault.Schema.json +0 -1600
  97. package/examples/combined demo/.power/schemas/teams/teams.Schema.json +0 -11112
  98. package/examples/combined demo/dist/office365users.js +0 -513
  99. package/examples/combined demo/dist/outlook.js +0 -1393
  100. package/examples/combined demo/src/generated/index.ts +0 -12
  101. package/examples/combined demo/src/generated/models/AzureKeyVaultModel.ts +0 -107
  102. package/examples/combined demo/src/generated/models/JiraModel.ts +0 -501
  103. package/examples/combined demo/src/generated/models/Office365GroupsModel.ts +0 -363
  104. package/examples/combined demo/src/generated/models/Office365OutlookModel.ts +0 -2046
  105. package/examples/combined demo/src/generated/models/Office365UsersModel.ts +0 -254
  106. package/examples/combined demo/src/generated/services/AzureKeyVaultService.ts +0 -257
  107. package/examples/combined demo/src/generated/services/JiraService.ts +0 -1124
  108. package/examples/combined demo/src/generated/services/Office365GroupsService.ts +0 -326
  109. package/examples/combined demo/src/generated/services/Office365OutlookService.ts +0 -2476
  110. package/examples/combined demo/src/generated/services/Office365UsersService.ts +0 -358
  111. package/examples/groups Demo/.power/schemas/appschemas/dataSourcesInfo.ts +0 -613
  112. package/examples/groups Demo/dist/office365groups.js +0 -642
  113. package/examples/groups Demo/src/generated/index.ts +0 -10
  114. package/examples/groups Demo/src/generated/models/Office365GroupsModel.ts +0 -363
  115. package/examples/groups Demo/src/generated/services/Office365GroupsService.ts +0 -326
  116. package/examples/myProfile/dist/office365users.js +0 -517
  117. package/examples/outlook Demo/.power/schemas/appschemas/dataSourcesInfo.ts +0 -6512
  118. package/examples/outlook Demo/dist/outlook.js +0 -1393
  119. package/examples/outlook Demo/src/generated/index.ts +0 -10
  120. package/examples/outlook Demo/src/generated/models/Office365OutlookModel.ts +0 -2046
  121. package/examples/outlook Demo/src/generated/services/Office365OutlookService.ts +0 -2476
  122. package/examples/sharePoint Demo/dist/sharepoint.js +0 -466
  123. package/examples/sharePoint Demo/src/generated/index.ts +0 -14
  124. package/examples/sharePoint Demo/src/generated/models/Office365GroupsModel.ts +0 -363
  125. package/examples/sharePoint Demo/src/generated/models/Office365OutlookModel.ts +0 -2046
  126. package/examples/sharePoint Demo/src/generated/models/Office365UsersModel.ts +0 -254
  127. package/examples/sharePoint Demo/src/generated/services/Office365GroupsService.ts +0 -326
  128. package/examples/sharePoint Demo/src/generated/services/Office365OutlookService.ts +0 -2476
  129. package/examples/sharePoint Demo/src/generated/services/Office365UsersService.ts +0 -358
  130. 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();