| Server IP : 173.209.174.21 / Your IP : 216.73.216.89 Web Server : Apache/2.4.58 (Ubuntu) System : Linux wcfs-server 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 13:00:45 UTC 2026 x86_64 User : nodor ( 1000) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/thelittlebigshow/vendor-reg/vendor_spot_assets/ |
Upload File : |
const rows = ['A','B','C','D','E','F','G','H','I','J'];
const cols = [1,2,3,4,5,6];
const walkwaySpots = rows.map(row => `${row}3`);
const bufferSpots = rows.map(row => `${row}6`);
const state = {
mode: window.SPOT_MAP_MODE || 'vendor',
vendors: [],
assignments: [],
allowedSpots: [],
selectedVendorId: null,
selectedSpots: [],
};
const els = {
grid: document.querySelector('#spotGrid'),
vendorSelect: document.querySelector('#vendorSelect'),
vendorDetails: document.querySelector('#vendorDetails'),
assignmentList: document.querySelector('#assignmentList'),
openCount: document.querySelector('#openCount'),
reservedCount: document.querySelector('#reservedCount'),
neededCount: document.querySelector('#neededCount'),
saveState: document.querySelector('#saveState'),
message: document.querySelector('#message'),
clearSelectionBtn: document.querySelector('#clearSelectionBtn'),
saveSelectionBtn: document.querySelector('#saveSelectionBtn'),
printBtn: document.querySelector('#printBtn'),
};
function boot() {
renderSpots();
bindEvents();
loadMap();
}
function bindEvents() {
els.vendorSelect.addEventListener('change', () => {
state.selectedVendorId = Number(els.vendorSelect.value);
loadSelectedVendorAssignments();
renderAll();
});
els.clearSelectionBtn.addEventListener('click', () => {
state.selectedSpots = [];
renderAll();
setMessage(state.mode === 'admin' ? 'Click Save Spots to release this vendor.' : 'Selection cleared.', 'ok');
});
els.saveSelectionBtn.addEventListener('click', saveSelection);
els.printBtn.addEventListener('click', () => window.print());
}
async function loadMap() {
const response = await fetch('vendor_spot_api.php');
const data = await response.json();
if (!data.ok) {
setMessage(data.message || 'Could not load map.', 'error');
return;
}
state.vendors = data.vendors;
state.assignments = data.assignments;
state.allowedSpots = data.allowedSpots;
state.selectedVendorId = data.currentVendorId || Number(state.vendors[0]?.id || 0);
renderVendorOptions();
loadSelectedVendorAssignments();
renderAll();
const vendor = getSelectedVendor();
if (state.mode !== 'admin' && vendor && !isPaid(vendor)) {
setMessage('Payment is needed before spot selection. Use the payment links in the side panel.', 'error');
} else {
setMessage('Choose your spot and save.', 'ok');
}
}
function renderSpots() {
const template = document.querySelector('#spotTemplate');
rows.forEach(row => {
cols.forEach(col => {
const spotId = `${row}${col}`;
const node = template.content.firstElementChild.cloneNode(true);
node.dataset.spotId = spotId;
node.title = `${spotId}: ${getDefaultSpotNote(spotId, col) || '10x10 vendor spot'}`;
node.querySelector('.spot-id').textContent = spotId;
node.addEventListener('click', () => handleSpotClick(spotId));
els.grid.appendChild(node);
});
});
}
function renderVendorOptions() {
els.vendorSelect.innerHTML = state.vendors.map(vendor => (
`<option value="${escapeHtml(vendor.id)}">${escapeHtml(vendor.name)} (${vendor.requested_spots} spot${Number(vendor.requested_spots) === 1 ? '' : 's'})</option>`
)).join('');
els.vendorSelect.value = state.selectedVendorId;
}
function loadSelectedVendorAssignments() {
state.selectedSpots = state.assignments
.filter(item => Number(item.vendor_id) === Number(state.selectedVendorId))
.map(item => item.spot_id);
}
function renderAll() {
const assignmentMap = new Map(state.assignments.map(item => [item.spot_id, item]));
document.querySelectorAll('.spot').forEach(node => {
const spotId = node.dataset.spotId;
const assignment = assignmentMap.get(spotId);
const blocked = !state.allowedSpots.includes(spotId);
const selected = state.selectedSpots.includes(spotId);
node.classList.toggle('blocked', blocked);
node.classList.toggle('reserved', Boolean(assignment) && !selected);
node.classList.toggle('selected', selected);
node.disabled = blocked;
node.querySelector('.spot-vendor').textContent = getSpotLabel(spotId, assignment);
});
renderVendorDetails();
renderStats();
renderAssignments();
}
function renderVendorDetails() {
const vendor = getSelectedVendor();
if (!vendor) {
els.vendorDetails.textContent = 'No vendor selected.';
return;
}
const needed = Number(vendor.requested_spots || 0);
els.vendorDetails.innerHTML = `
<strong>${escapeHtml(vendor.name)}</strong>
<div>Vendor ID: ${escapeHtml(vendor.id)}</div>
<div>${escapeHtml(vendor.category || 'Vendor')} ยท ${needed === 0 ? 'street setup, no park spots' : `needs ${needed} spot${needed === 1 ? '' : 's'}`}</div>
<div class="vendor-description">${escapeHtml(vendor.description || '')}</div>
<div class="chips">
<span class="chip">${state.selectedSpots.length ? escapeHtml(state.selectedSpots.join(', ')) : 'Not picked yet'}</span>
<span class="chip">${escapeHtml(formatPayment(vendor.payment_status))}</span>
</div>
${state.mode !== 'admin' && !isPaid(vendor) ? renderPaymentPrompt(vendor) : ''}
`;
els.neededCount.textContent = needed;
}
function renderStats() {
const assigned = new Set(state.assignments.map(item => item.spot_id));
els.openCount.textContent = state.allowedSpots.filter(spotId => !assigned.has(spotId)).length;
els.reservedCount.textContent = assigned.size;
}
function renderAssignments() {
const visibleAssignments = state.assignments.filter(item => state.mode === 'admin' || Number(item.vendor_id) === Number(state.selectedVendorId));
els.assignmentList.innerHTML = visibleAssignments.length ? visibleAssignments.map(item => `
<div class="assignment">
<strong>${escapeHtml(item.name)}</strong>
<span>${escapeHtml(item.spot_id)}</span>
</div>
`).join('') : '<div class="assignment"><span>No spots assigned yet.</span></div>';
}
function handleSpotClick(spotId) {
const vendor = getSelectedVendor();
if (!vendor || !state.allowedSpots.includes(spotId)) return;
const assignment = state.assignments.find(item => item.spot_id === spotId);
if (state.mode === 'admin' && assignment && Number(assignment.vendor_id) !== Number(state.selectedVendorId)) {
state.selectedVendorId = Number(assignment.vendor_id);
els.vendorSelect.value = state.selectedVendorId;
loadSelectedVendorAssignments();
renderAll();
setMessage(`${assignment.name} is assigned to ${spotId}. Click Release Spots, then Save Spots to remove it.`, 'ok');
return;
}
const needed = Number(vendor.requested_spots || 0);
if (state.mode !== 'admin' && !isPaid(vendor)) {
setMessage('Your registration must be paid before you can pick a spot. Use the payment links in the side panel.', 'error');
return;
}
if (needed < 1) {
setMessage('This vendor is marked street setup only.', 'error');
return;
}
if (assignment && Number(assignment.vendor_id) !== Number(state.selectedVendorId) && state.mode !== 'admin') {
setMessage('That spot has already been taken.', 'error');
return;
}
if (!toggleSingleSpot(spotId, needed)) {
renderAll();
return;
}
renderAll();
setMessage(`${state.selectedSpots.length} of ${needed} spot${needed === 1 ? '' : 's'} selected.`, 'ok');
}
function toggleSingleSpot(spotId, needed) {
if (state.selectedSpots.includes(spotId)) {
state.selectedSpots = state.selectedSpots.filter(id => id !== spotId);
return true;
}
if (state.selectedSpots.length >= needed) {
setMessage(`This vendor only has ${needed} registered spot${needed === 1 ? '' : 's'}. Click a selected spot to remove it first.`, 'error');
return false;
}
state.selectedSpots = [...state.selectedSpots, spotId].sort(compareSpotIds);
return true;
}
async function saveSelection() {
const vendor = getSelectedVendor();
if (!vendor) return;
const needed = Number(vendor.requested_spots || 0);
if (state.mode !== 'admin' && !isPaid(vendor)) {
setMessage('Your registration must be paid before you can save a spot. Use the payment links in the side panel.', 'error');
return;
}
const adminRelease = state.mode === 'admin' && state.selectedSpots.length === 0;
if (!adminRelease && state.selectedSpots.length !== needed) {
setMessage(`Select exactly ${needed} spot${needed === 1 ? '' : 's'} before saving.`, 'error');
return;
}
els.saveState.textContent = 'Saving...';
const response = await fetch('vendor_spot_api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vendor_id: state.selectedVendorId, spot_ids: state.selectedSpots }),
});
const data = await response.json();
if (!data.ok) {
els.saveState.textContent = 'Not saved';
setMessage(data.message || 'Could not save spots.', 'error');
await loadMap();
return;
}
els.saveState.textContent = 'Saved';
setMessage('Spots saved.', 'ok');
await loadMap();
}
function getSelectedVendor() {
return state.vendors.find(vendor => Number(vendor.id) === Number(state.selectedVendorId));
}
function compareSpotIds(a, b) {
const parsedA = /^([A-J])([1-6])$/.exec(a);
const parsedB = /^([A-J])([1-6])$/.exec(b);
if (!parsedA || !parsedB) return a.localeCompare(b);
if (parsedA[1] !== parsedB[1]) return parsedA[1].localeCompare(parsedB[1]);
return Number(parsedA[2]) - Number(parsedB[2]);
}
function getDefaultSpotNote(id, col) {
if (walkwaySpots.includes(id)) return 'Walkway between booth rows';
if (bufferSpots.includes(id)) return 'Buffer inside 68.42 ft width';
if (col === 1 || col === 2) return 'Back-to-back booth row 1';
if (col === 4 || col === 5) return 'Back-to-back booth row 2';
return '';
}
function getSpotLabel(spotId, assignment) {
if (walkwaySpots.includes(spotId)) return 'WALK';
if (bufferSpots.includes(spotId)) return 'BUFFER';
return assignment?.name || '';
}
function formatPayment(value) {
if (String(value) === '1') return 'Paid';
if (String(value) === '2') return 'Pending';
if (String(value) === '0' || String(value).toLowerCase() === 'unpaid') return 'Unpaid';
return 'Unknown';
}
function isPaid(vendor) {
const value = String(vendor.payment_status ?? '').trim().toLowerCase();
return value === '1' || value === 'paid';
}
function renderPaymentPrompt(vendor) {
const spots = Number(vendor.requested_spots || 0);
const isFood = String(vendor.category || '').toLowerCase().includes('food');
return `
<div class="payment-prompt">
<strong>Payment needed before spot selection</strong>
<p>After you pay, your registration may need a little time to be confirmed by the event team before spot selection opens. Please use the same name and email from this registration.</p>
<p>If you have already paid and still cannot choose a spot, email <a href="mailto:info@thelittlebigshow.com">info@thelittlebigshow.com</a> and we will help get it updated.</p>
<div class="payment-actions">
${isFood ? `<a href="https://square.link/u/9suAWD0n?src=embed" target="_blank" rel="noopener">Pay Food Vendor - $35</a>` : ''}
<a href="https://square.link/u/UVVjNjOo?src=embed" target="_blank" rel="noopener">Pay Vendor Spot - $25</a>
</div>
${spots > 1 ? `<p class="payment-note">Registered spots: ${spots}. If Square asks for quantity, use ${spots}.</p>` : ''}
</div>
`;
}
function setMessage(text, type = '') {
els.message.textContent = text;
els.message.className = `message ${type}`.trim();
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, char => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
}[char]));
}
boot();