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.
- package/AI/skills/autoreview/SKILL.md +69 -0
- package/AI/skills/connections/SKILL.md +4 -4
- package/AI/skills/dataverse/SKILL.md +4 -2
- package/AI/skills/keyvault/SKILL.md +139 -0
- package/AI/skills/office365-groups/SKILL.md +46 -25
- package/AI/skills/office365-outlook/SKILL.md +56 -25
- package/AI/skills/office365-users/SKILL.md +41 -36
- package/AI/skills/sharepoint/SKILL.md +174 -31
- package/AI/skills/start/SKILL.md +1 -0
- package/codeApp/dist/connectors/autoreview.js +1654 -0
- package/codeApp/dist/connectors/office365groups.js +2995 -432
- package/{examples/outlook Demo/.power/schemas/office365/office365.Schema.json → codeApp/dist/connectors/office365outlook.js} +7439 -16
- package/codeApp/dist/connectors/office365users.js +2990 -349
- package/codeApp/dist/connectors/sharepoint.js +529 -308
- package/examples/combined demo/dist/connectors/office365outlook.js +28521 -0
- package/examples/combined demo/dist/connectors/office365users.js +3154 -0
- package/examples/combined demo/dist/index.js +2 -6
- package/examples/combined demo/power.config.json +1 -1
- package/examples/groups Demo/{.power/schemas/office365groups/office365groups.Schema.json → dist/connectors/office365groups.js } +3205 -2204
- package/examples/groups Demo/dist/index.js +1 -5
- package/examples/groups Demo/power.config.json +1 -1
- package/examples/myProfile/dist/connectors/office365users.js +3154 -0
- package/examples/myProfile/dist/index.js +1 -5
- package/examples/myProfile/power.config.json +1 -1
- package/examples/outlook Demo/dist/connectors/office365outlook.js +28521 -0
- package/examples/outlook Demo/dist/index.js +2 -5
- package/examples/outlook Demo/power.config.json +1 -1
- package/examples/sharePoint Demo/dist/connectors/sharepoint.js +687 -0
- package/examples/sharePoint Demo/dist/index.js +86 -127
- package/examples/sharePoint Demo/power.config.json +1 -1
- package/package.json +1 -1
- package/codeApp/.power/schemas/appschemas/dataSourcesInfo.ts +0 -6275
- package/codeApp/.power/schemas/jira/jira.Schema.json +0 -6903
- package/codeApp/.power/schemas/keyvault/keyvault.Schema.json +0 -1600
- package/codeApp/.power/schemas/office365groups/office365groups.Schema.json +0 -2204
- package/codeApp/.power/schemas/teams/teams.Schema.json +0 -11112
- package/codeApp/dist/connectors/outlook.js +0 -1393
- package/codeApp/src/generated/models/AzureKeyVaultModel.ts +0 -107
- package/codeApp/src/generated/models/JiraModel.ts +0 -501
- package/codeApp/src/generated/models/Office365GroupsModel.ts +0 -363
- package/codeApp/src/generated/models/Office365OutlookModel.ts +0 -2046
- package/codeApp/src/generated/models/Office365UsersModel.ts +0 -254
- package/codeApp/src/generated/services/AzureKeyVaultService.ts +0 -257
- package/codeApp/src/generated/services/JiraService.ts +0 -1124
- package/codeApp/src/generated/services/Office365GroupsService.ts +0 -326
- package/codeApp/src/generated/services/Office365OutlookService.ts +0 -2476
- package/codeApp/src/generated/services/Office365UsersService.ts +0 -358
- package/examples/apps/kanban/dist/dataverse.js +0 -94
- package/examples/apps/kanban/dist/environmentVar.js +0 -55
- package/examples/apps/kanban/dist/index.css +0 -605
- package/examples/apps/kanban/dist/index.html +0 -21
- package/examples/apps/kanban/dist/index.js +0 -860
- package/examples/apps/kanban/dist/office365groups.js +0 -97
- package/examples/apps/kanban/dist/office365users.js +0 -451
- package/examples/apps/kanban/dist/outlook.js +0 -162
- package/examples/apps/kanban/dist/power-apps-data.js +0 -2953
- package/examples/apps/kanban/dist/sharepoint.js +0 -435
- package/examples/apps/kanban/power.config.json +0 -35
- package/examples/apps/kanban/src/generated/index.ts +0 -14
- package/examples/apps/kanban/src/generated/models/Office365GroupsModel.ts +0 -363
- package/examples/apps/kanban/src/generated/models/Office365OutlookModel.ts +0 -2046
- package/examples/apps/kanban/src/generated/models/Office365UsersModel.ts +0 -254
- package/examples/apps/kanban/src/generated/services/Office365GroupsService.ts +0 -326
- package/examples/apps/kanban/src/generated/services/Office365OutlookService.ts +0 -2476
- package/examples/apps/kanban/src/generated/services/Office365UsersService.ts +0 -358
- package/examples/apps/planning Poker/additional files/AgilePoker_1_0_0_1.zip +0 -0
- package/examples/apps/planning Poker/additional files/PokerTables_1_0_0_1.zip +0 -0
- package/examples/apps/planning Poker/additional files/customizations (tables).xml +0 -6429
- package/examples/apps/planning Poker/additional files/dataverse-tables.json +0 -165
- package/examples/apps/planning Poker/additional files/readme.md +0 -122
- package/examples/apps/planning Poker/dist/dataverse.js +0 -78
- package/examples/apps/planning Poker/dist/index.html +0 -198
- package/examples/apps/planning Poker/dist/index.js +0 -955
- package/examples/apps/planning Poker/dist/power-apps-data.js +0 -2953
- package/examples/apps/planning Poker/dist/styles.css +0 -815
- package/examples/apps/planning Poker/power.config.json +0 -50
- package/examples/apps/solution explorer/dist/codeapp.js +0 -1098
- package/examples/apps/solution explorer/dist/icon-512.png +0 -0
- package/examples/apps/solution explorer/dist/index.html +0 -80
- package/examples/apps/solution explorer/dist/index.js +0 -735
- package/examples/apps/solution explorer/dist/power-apps-data.js +0 -3007
- package/examples/apps/solution explorer/dist/styles.css +0 -571
- package/examples/apps/solution explorer/power.config.json +0 -151
- package/examples/apps/todo/dist/dataverse.js +0 -64
- package/examples/apps/todo/dist/icon192.png +0 -0
- package/examples/apps/todo/dist/index.html +0 -75
- package/examples/apps/todo/dist/index.js +0 -9
- package/examples/apps/todo/dist/power-apps-data.js +0 -2953
- package/examples/apps/todo/dist/renderer.js +0 -375
- package/examples/apps/todo/dist/styles.css +0 -691
- package/examples/apps/todo/power.config.json +0 -35
- package/examples/combined demo/.power/schemas/appschemas/dataSourcesInfo.ts +0 -6275
- package/examples/combined demo/.power/schemas/jira/jira.Schema.json +0 -6903
- package/examples/combined demo/.power/schemas/keyvault/keyvault.Schema.json +0 -1600
- package/examples/combined demo/.power/schemas/teams/teams.Schema.json +0 -11112
- package/examples/combined demo/dist/office365users.js +0 -513
- package/examples/combined demo/dist/outlook.js +0 -1393
- package/examples/combined demo/src/generated/index.ts +0 -12
- package/examples/combined demo/src/generated/models/AzureKeyVaultModel.ts +0 -107
- package/examples/combined demo/src/generated/models/JiraModel.ts +0 -501
- package/examples/combined demo/src/generated/models/Office365GroupsModel.ts +0 -363
- package/examples/combined demo/src/generated/models/Office365OutlookModel.ts +0 -2046
- package/examples/combined demo/src/generated/models/Office365UsersModel.ts +0 -254
- package/examples/combined demo/src/generated/services/AzureKeyVaultService.ts +0 -257
- package/examples/combined demo/src/generated/services/JiraService.ts +0 -1124
- package/examples/combined demo/src/generated/services/Office365GroupsService.ts +0 -326
- package/examples/combined demo/src/generated/services/Office365OutlookService.ts +0 -2476
- package/examples/combined demo/src/generated/services/Office365UsersService.ts +0 -358
- package/examples/groups Demo/.power/schemas/appschemas/dataSourcesInfo.ts +0 -613
- package/examples/groups Demo/dist/office365groups.js +0 -642
- package/examples/groups Demo/src/generated/index.ts +0 -10
- package/examples/groups Demo/src/generated/models/Office365GroupsModel.ts +0 -363
- package/examples/groups Demo/src/generated/services/Office365GroupsService.ts +0 -326
- package/examples/myProfile/dist/office365users.js +0 -517
- package/examples/outlook Demo/.power/schemas/appschemas/dataSourcesInfo.ts +0 -6512
- package/examples/outlook Demo/dist/outlook.js +0 -1393
- package/examples/outlook Demo/src/generated/index.ts +0 -10
- package/examples/outlook Demo/src/generated/models/Office365OutlookModel.ts +0 -2046
- package/examples/outlook Demo/src/generated/services/Office365OutlookService.ts +0 -2476
- package/examples/sharePoint Demo/dist/sharepoint.js +0 -466
- package/examples/sharePoint Demo/src/generated/index.ts +0 -14
- package/examples/sharePoint Demo/src/generated/models/Office365GroupsModel.ts +0 -363
- package/examples/sharePoint Demo/src/generated/models/Office365OutlookModel.ts +0 -2046
- package/examples/sharePoint Demo/src/generated/models/Office365UsersModel.ts +0 -254
- package/examples/sharePoint Demo/src/generated/services/Office365GroupsService.ts +0 -326
- package/examples/sharePoint Demo/src/generated/services/Office365OutlookService.ts +0 -2476
- package/examples/sharePoint Demo/src/generated/services/Office365UsersService.ts +0 -358
- 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'), '&')
|
|
776
|
-
.replace(new RegExp('"', 'g'), '"')
|
|
777
|
-
.replace(new RegExp('<', 'g'), '<')
|
|
778
|
-
.replace(new RegExp('>', 'g'), '>');
|
|
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();
|