/**
* FILE NAME: kms-bc-script.js
* PURPOSE: Single Page Application state driver with optimistic UI rendering.
* VERSION: 1.2.8
* GENERATED DATE/TIME: 2026-05-20 16:16:10 (Asia/Kuala_Lumpur)
*/
document.addEventListener('DOMContentLoaded', function () {
const appContainer = document.getElementById('kmsbc-app');
if (!appContainer) return;
const state = {
currentToken: null,
sessionDate: '',
masterRoster: [],
players: [],
games: [],
selectedCourtPlayers: [],
shuttleCount: 1,
pollingInterval: null,
isSubmitting: false,
activeEditingGameId: null
};
const clubLogoUrl = 'https://i.postimg.cc/Bb0fZn67/Screenshot-2026-05-20-145748.jpg';
const getApiBaseUrl = () => {
const currentPath = window.location.pathname;
const appSlugIndex = currentPath.indexOf('/kms-court');
const subFolder = appSlugIndex > 0 ? currentPath.substring(0, appSlugIndex) : '';
return window.location.origin + subFolder + '/wp-json/kms-bc/v1/';
};
function init() {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('session_id');
const emergencyLoaderKill = setTimeout(() => {
showLoader(false);
const wrapper = document.getElementById('kmsbc-screen-wrapper');
if (wrapper && wrapper.innerHTML === '') {
if (token) {
state.currentToken = token;
renderScreenCourt();
} else {
renderScreenSetup();
}
}
}, 1200);
showLoader(true);
fetchMasterRoster().then(() => {
if (token) {
state.currentToken = token;
return fetchSessionStatus().then(() => {
renderScreenCourt();
startPolling();
});
} else {
renderScreenSetup();
}
}).catch(err => {
console.error('Initialization error:', err);
window.history.replaceState({}, document.title, window.location.pathname);
state.currentToken = null;
renderScreenSetup();
}).finally(() => {
clearTimeout(emergencyLoaderKill);
showLoader(false);
});
}
function showLoader(visible) {
const loader = document.getElementById('kmsbc-global-loader');
if (loader) {
loader.style.display = visible ? 'flex' : 'none';
}
}
function apiRequest(endpoint, method = 'GET', data = null) {
const targetUrl = getApiBaseUrl() + endpoint;
const options = {
method: method,
headers: {}
};
if (typeof kmsbc_config !== 'undefined' && kmsbc_config.nonce) {
options.headers['X-WP-Nonce'] = kmsbc_config.nonce;
}
if (data) {
if (method === 'POST') {
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
const formBody = [];
for (const property in data) {
const encodedKey = encodeURIComponent(property);
const encodedValue = encodeURIComponent(data[property]);
formBody.push(encodedKey + "=" + encodedValue);
}
options.body = formBody.join("&");
} else {
options.headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(data);
}
}
return fetch(targetUrl, options).then(response => {
if (!response.ok) return response.json().then(err => { throw err; });
return response.json();
});
}
function fetchMasterRoster() {
return apiRequest('roster/get', 'GET').then(data => {
state.masterRoster = Array.isArray(data) ? data : [];
}).catch(err => {
console.error('Master roster fetch error:', err);
state.masterRoster = [];
});
}
function fetchSessionStatus() {
return apiRequest('session/status?token=' + encodeURIComponent(state.currentToken), 'GET')
.then(data => {
state.sessionDate = data.session_date || '';
state.players = data.players || [];
state.games = data.games || [];
state.selectedCourtPlayers = state.selectedCourtPlayers.filter(p => state.players.includes(p));
});
}
function startPolling() {
if (state.pollingInterval) clearInterval(state.pollingInterval);
state.pollingInterval = setInterval(() => {
if (!state.isSubmitting && state.activeEditingGameId === null) {
fetchSessionStatus().then(() => {
updateLiveDashboardDOM();
}).catch(err => console.error('Polling error:', err));
}
}, 4000);
}
function stopPolling() {
if (state.pollingInterval) {
clearInterval(state.pollingInterval);
state.pollingInterval = null;
}
}
function renderShuttlecockSVG() {
return `
`;
}
function renderScreenSetup() {
stopPolling();
const wrapper = document.getElementById('kmsbc-screen-wrapper');
if (!wrapper) return;
let html = `
KMS BC
Select tonight's active group roster. Minimum 4 players required to unlock fields.
0 / 4 Players Checked In
`;
wrapper.innerHTML = html;
updateSetupScreenDOM();
document.getElementById('btn-add-roster-submit').addEventListener('click', executeMasterRosterAdd);
document.getElementById('txt-new-roster-name').addEventListener('keydown', (e) => { if (e.key === 'Enter') executeMasterRosterAdd(); });
document.getElementById('btn-start-session').addEventListener('click', function () {
const checked = wrapper.querySelectorAll('.kmsbc-setup-chk:checked');
const selectedPlayers = Array.from(checked).map(c => c.value);
showLoader(true);
apiRequest('session/init', 'POST', { players: JSON.stringify(selectedPlayers) })
.then(res => {
state.currentToken = res.token;
const newUrl = window.location.pathname + '?session_id=' + encodeURIComponent(res.token);
window.history.pushState({ path: newUrl }, '', newUrl);
return fetchSessionStatus();
})
.then(() => {
renderScreenCourt();
startPolling();
})
.catch(err => alert(err.error || 'Exception generated starting session.'))
.finally(() => showLoader(false));
});
}
function updateSetupScreenDOM() {
const checkboxContainer = document.getElementById('setup-checkboxes-container');
const rosterContainer = document.getElementById('master-roster-edit-container');
if (!checkboxContainer) return;
let checkHtml = '';
state.masterRoster.forEach((player, index) => {
checkHtml += `
`;
});
checkboxContainer.innerHTML = checkHtml || 'Database pool empty.
';
const checkboxes = checkboxContainer.querySelectorAll('.kmsbc-setup-chk');
const startBtn = document.getElementById('btn-start-session');
const badge = document.getElementById('setup-count-badge');
checkboxes.forEach(chk => {
chk.addEventListener('change', () => {
const checked = checkboxContainer.querySelectorAll('.kmsbc-setup-chk:checked');
const count = checked.length;
const parentTile = chk.closest('.kmsbc-interactive-checkbox-tile');
if (chk.checked) parentTile.classList.add('tile-is-checked');
else parentTile.classList.remove('tile-is-checked');
badge.innerText = `${count} / 4 Players Checked In`;
if (count >= 4) {
startBtn.removeAttribute('disabled');
badge.classList.add('badge-is-valid');
} else {
startBtn.setAttribute('disabled', 'disabled');
badge.classList.remove('badge-is-valid');
}
});
});
let rosterHtml = '';
state.masterRoster.forEach(player => {
rosterHtml += `
`;
});
rosterContainer.innerHTML = rosterHtml;
rosterContainer.querySelectorAll('.kmsbc-btn-save-roster').forEach(btn => {
btn.addEventListener('click', () => {
const id = parseInt(btn.getAttribute('data-id'));
const input = rosterContainer.querySelector(`input[data-id="${id}"]`);
executeMasterRosterEdit(id, input.value.trim(), 'update');
});
});
rosterContainer.querySelectorAll('.kmsbc-btn-delete-roster').forEach(btn => {
btn.addEventListener('click', () => {
const id = parseInt(btn.getAttribute('data-id'));
if (confirm('Permanently wipe this player profile entirely out of the group directory database?')) {
executeMasterRosterEdit(id, '', 'delete');
}
});
});
}
function executeMasterRosterAdd() {
const input = document.getElementById('txt-new-roster-name');
const name = input.value.trim();
if (!name) return;
showLoader(true);
apiRequest('roster/add', 'POST', { player_name: name })
.then(() => {
input.value = '';
return fetchMasterRoster();
})
.then(() => updateSetupScreenDOM())
.catch(err => alert(err.error || 'Failed adding profile.'))
.finally(() => showLoader(false));
}
// FIXED: Uses absolute Optimistic UI overrides to instantly clean components from local state matrices
function executeMasterRosterEdit(id, name, action) {
if (action === 'delete') {
// Optimistic UX: Strip row and record from active screen cache *instantly*
state.masterRoster = state.masterRoster.filter(p => p.id !== id);
updateSetupScreenDOM();
}
showLoader(true);
apiRequest('roster/edit', 'POST', { id: id, player_name: name, action: action })
.then(() => {
// Background refresh to confirm server persistence matching data structures
return fetchMasterRoster();
})
.then(() => updateSetupScreenDOM())
.catch(err => {
console.error('Server sync reject:', err);
return fetchMasterRoster().then(() => updateSetupScreenDOM());
})
.finally(() => showLoader(false));
}
function renderScreenCourt() {
const wrapper = document.getElementById('kmsbc-screen-wrapper');
if (!wrapper) return;
wrapper.innerHTML = `
Stadium Arena Court
Shuttlecocks Consumed:
1
The Active Court Bench (Tap 4 onto court)
Chronological Match History
`;
document.getElementById('btn-back-setup').addEventListener('click', () => {
if (confirm('Exit session view? Active rows remain safely synced.')) {
window.history.replaceState({}, document.title, window.location.pathname);
state.currentToken = null;
renderScreenSetup();
}
});
document.getElementById('btn-shuttle-minus').addEventListener('click', () => {
if (state.shuttleCount > 1) {
state.shuttleCount--;
updateControlsDOM();
}
});
document.getElementById('btn-shuttle-plus').addEventListener('click', () => {
state.shuttleCount++;
updateControlsDOM();
});
document.getElementById('btn-log-game').addEventListener('click', executionLogGameMatch);
document.getElementById('btn-end-session').addEventListener('click', renderScreenSummary);
document.getElementById('btn-cancel-edit-mode').addEventListener('click', () => {
state.activeEditingGameId = null;
state.selectedCourtPlayers = [];
state.shuttleCount = 1;
updateLiveDashboardDOM();
});
updateLiveDashboardDOM();
}
function updateLiveDashboardDOM() {
if (!document.getElementById('court-slots-container')) return;
const cancelEditBtn = document.getElementById('btn-cancel-edit-mode');
const courtHeading = document.getElementById('court-panel-heading-title');
if (state.activeEditingGameId !== null) {
courtHeading.innerHTML = `✏️ Editing Match Log #${state.activeEditingGameId}`;
courtHeading.classList.add('kmsbc-text-warning');
cancelEditBtn.style.display = 'block';
} else {
courtHeading.innerHTML = 'Stadium Arena Court';
courtHeading.classList.remove('kmsbc-text-warning');
cancelEditBtn.style.display = 'none';
}
const courtContainer = document.getElementById('court-slots-container');
let courtHtml = '';
for (let i = 0; i < 4; i++) {
const player = state.selectedCourtPlayers[i];
if (player) {
courtHtml += `
${player}
×
`;
} else {
courtHtml += `
Available Slot
`;
}
}
courtContainer.innerHTML = courtHtml;
courtContainer.querySelectorAll('.position-slot-occupied').forEach(slot => {
slot.addEventListener('click', () => {
const idx = parseInt(slot.getAttribute('data-index'));
state.selectedCourtPlayers.splice(idx, 1);
updateLiveDashboardDOM();
});
});
const benchContainer = document.getElementById('bench-slots-container');
benchContainer.innerHTML = '';
state.players.forEach(player => {
const isSelected = state.selectedCourtPlayers.includes(player);
const btn = document.createElement('button');
btn.type = 'button';
btn.className = `kmsbc-bench-interactive-pill-node ${isSelected ? 'pill-node-is-active' : ''}`;
btn.innerText = player;
btn.addEventListener('click', () => {
if (isSelected) {
state.selectedCourtPlayers = state.selectedCourtPlayers.filter(p => p !== player);
} else {
if (state.selectedCourtPlayers.length < 4) {
state.selectedCourtPlayers.push(player);
} else {
alert('Court full. Clear a slot to replace positions.');
return;
}
}
updateLiveDashboardDOM();
});
benchContainer.appendChild(btn);
});
const addCard = document.createElement('div');
addCard.className = 'kmsbc-bench-latecomer-input-inline-card';
addCard.innerHTML = `
`;
benchContainer.appendChild(addCard);
const inlineInput = document.getElementById('txt-late-name');
const inlineBtn = document.getElementById('btn-late-submit');
const executeLatecomerAdd = () => {
const value = inlineInput.value.trim();
if (!value) return;
if (state.players.some(p => p.toLowerCase() === value.toLowerCase())) {
alert('This user signature already checked in on tonight\'s bench.');
return;
}
state.isSubmitting = true;
showLoader(true);
apiRequest('player/add', 'POST', { token: state.currentToken, player_name: value })
.then(() => {
inlineInput.value = '';
return fetchSessionStatus();
})
.then(() => updateLiveDashboardDOM())
.catch(err => alert(err.error || 'Failed adding latecomer.'))
.finally(() => {
state.isSubmitting = false;
showLoader(false);
});
};
inlineBtn.addEventListener('click', executeLatecomerAdd);
inlineInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') executeLatecomerAdd(); });
const historyContainer = document.getElementById('history-slots-container');
if (state.games.length === 0) {
historyContainer.innerHTML = 'No matches registered yet tonight.
';
} else {
let historyHtml = '';
state.games.forEach(game => {
const isBeingEdited = state.activeEditingGameId === game.id;
let iconTrayStr = '';
for (let s = 0; s < game.shuttles; s++) {
iconTrayStr += renderShuttlecockSVG();
}
historyHtml += `
-
${game.players.join(', ')}
${iconTrayStr}
`;
});
historyHtml += '
';
historyContainer.innerHTML = historyHtml;
historyContainer.querySelectorAll('.kmsbc-btn-action-edit-historical-match').forEach(editBtn => {
editBtn.addEventListener('click', () => {
const gameId = parseInt(editBtn.getAttribute('data-id'));
const targetGame = state.games.find(g => g.id === gameId);
if (targetGame) {
state.activeEditingGameId = gameId;
state.selectedCourtPlayers = [...targetGame.players];
state.shuttleCount = targetGame.shuttles;
updateLiveDashboardDOM();
document.getElementById('kmsbc-app').scrollTop = 0;
}
});
});
historyContainer.querySelectorAll('.kmsbc-btn-action-delete-historical-match').forEach(delBtn => {
delBtn.addEventListener('click', () => {
const gameId = delBtn.getAttribute('data-id');
if (confirm('Permanently purge this match entry record?')) {
state.isSubmitting = true;
showLoader(true);
apiRequest('game/delete', 'POST', { token: state.currentToken, game_id: gameId })
.then(() => {
if (state.activeEditingGameId == gameId) state.activeEditingGameId = null;
return fetchSessionStatus();
})
.then(() => updateLiveDashboardDOM())
.catch(err => alert(err.error || 'Purge exception processing historical drops.'))
.finally(() => {
state.isSubmitting = false;
showLoader(false);
});
}
});
});
}
updateControlsDOM();
}
function updateControlsDOM() {
const logBtn = document.getElementById('btn-log-game');
const shuttleLbl = document.getElementById('lbl-shuttle-count');
if (!logBtn) return;
shuttleLbl.innerText = state.shuttleCount;
const baseCostTotal = state.shuttleCount * 12;
if (state.activeEditingGameId !== null) {
logBtn.innerText = `Update Match Entry (RM ${baseCostTotal})`;
logBtn.className = 'kmsbc-btn-action kmsbc-btn-action-warning-update';
} else {
logBtn.innerText = `Log Active Match (RM ${baseCostTotal} Total)`;
logBtn.className = 'kmsbc-btn-action kmsbc-btn-action-success';
}
if (state.selectedCourtPlayers.length === 4) {
logBtn.removeAttribute('disabled');
} else {
logBtn.setAttribute('disabled', 'disabled');
}
}
function executionLogGameMatch() {
if (state.selectedCourtPlayers.length !== 4) return;
state.isSubmitting = true;
showLoader(true);
const dataPayload = {
token: state.currentToken,
players: JSON.stringify(state.selectedCourtPlayers),
shuttles: state.shuttleCount
};
if (state.activeEditingGameId !== null) {
dataPayload.game_id = state.activeEditingGameId;
}
apiRequest('game/log', 'POST', dataPayload)
.then(() => {
state.selectedCourtPlayers = [];
state.shuttleCount = 1;
state.activeEditingGameId = null;
return fetchSessionStatus();
})
.then(() => updateLiveDashboardDOM())
.catch(err => alert(err.error || 'Server error logging match layout.'))
.finally(() => {
state.isSubmitting = false;
showLoader(false);
});
}
function renderScreenSummary() {
if (!confirm('Close session tracking and output individual dynamic split-billing totals?')) {
return;
}
stopPolling();
showLoader(true);
fetchSessionStatus().then(() => {
const wrapper = document.getElementById('kmsbc-screen-wrapper');
const userTabs = {};
let globalTotalShuttles = 0;
state.players.forEach(p => {
userTabs[p] = { shuttlesFractional: 0, cashOwedRM: 0 };
});
state.games.forEach(game => {
globalTotalShuttles += game.shuttles;
const matchCost = game.shuttles * 12;
const playerCount = game.players.length;
const costPerHead = matchCost / playerCount;
const shuttlesPerHead = game.shuttles / playerCount;
game.players.forEach(p => {
if (userTabs[p]) {
userTabs[p].shuttlesFractional += shuttlesPerHead;
userTabs[p].cashOwedRM += costPerHead;
}
});
});
const reportingList = Object.keys(userTabs).map(name => {
return {
name: name,
shuttles: userTabs[name].shuttlesFractional,
cost: userTabs[name].cashOwedRM
};
});
reportingList.sort((a, b) => b.cost - a.cost);
let html = `
| # |
Player |
Shuttles |
Fee Owed |
`;
reportingList.forEach((row, i) => {
const displayShuttles = Number(row.shuttles.toFixed(2)).toString();
const displayCost = row.cost.toFixed(2);
html += `
| ${i + 1} |
${row.name} |
${displayShuttles} |
RM ${displayCost} |
`;
});
html += `
`;
wrapper.innerHTML = html;
document.getElementById('btn-restart-app').addEventListener('click', () => {
window.history.replaceState({}, document.title, window.location.pathname);
state.currentToken = null;
state.selectedCourtPlayers = [];
state.shuttleCount = 1;
renderScreenSetup();
});
document.getElementById('btn-share-whatsapp').addEventListener('click', () => {
let textBlock = `🏸 *Badminton Session Summary*\n`;
textBlock += `📅 Date: ${state.sessionDate}\n`;
textBlock += `Total Shuttles Used: ${globalTotalShuttles}\n\n`;
textBlock += `*Fee Breakdown:*\n`;
reportingList.forEach((row, i) => {
const displayShuttles = Number(row.shuttles.toFixed(2)).toString();
const displayCost = row.cost.toFixed(2);
textBlock += `${i + 1}. ${row.name} (${displayShuttles} shuttles) = RM ${displayCost}\n`;
});
textBlock += `\nPlease QR the payment to our admin (Me). Thanks for the games tonight! 🙏`;
const whatsappUrl = `https://api.whatsapp.com/send?text=` + encodeURIComponent(textBlock);
window.open(whatsappUrl, '_blank');
});
}).catch(err => {
alert(err.error || 'Failed generating spreadsheet calculation sheets.');
}).finally(() => {
showLoader(false);
});
}
init();
});
https://gbit.durianuncle.com.my/wp-sitemap-posts-post-1.xmlhttps://gbit.durianuncle.com.my/wp-sitemap-posts-page-1.xmlhttps://gbit.durianuncle.com.my/wp-sitemap-taxonomies-category-1.xmlhttps://gbit.durianuncle.com.my/wp-sitemap-users-1.xml