PageSourceSearch

https://donovancarrillo.com/wp-content/plugins/live-event-seating/public/js/seating-plan-selector.js

js donovancarrillo.com collected 2026-10-07 00:03:23 UTC 124,595 bytes, 1,920 lines download raw bytes

1(function($) {
2    $(function() {
3        if (typeof seatingPlanData === 'undefined' || $('#seating-plan-app').length === 0) { return; }
4        
5        const config = seatingPlanData;
6        const i18n = config.i18n || {};
7        const svgNS = "http://www.w3.org/2000/svg";
8        let softLockedSeats = new Set(); 
9        let bookedSeatsByParent = new Map();
10        let gaBookings = {};
11        let selectedItems = []; 
12        let isBooking = false; 
13        let currentPriceFilter = null; 
14        let currentAccessibilityMap = null; // set when ♿ filter is active
15        
16        let view = { 
17            x: 0, y: 0, w: 1000, h: 800, 
18            isPanning: false, 
19            panStart: { x: 0, y: 0 },
20            isPinching: false,
21            startPinchDist: 0
22        };
23        
24        let fullMapBounds = { x: 0, y: 0, w: 1000, h: 800 }; 
25        let activeDetailSectionId = null;
26        let isAnimating = false;
27        let addonModalOpen = false;
28        let currentAddons = [];
29        let addonSelections = {};
30        let pollingInterval;
31        let renderedSections = new Set(); 
32        let tooltipHideTimeout;
33
34        function rebuildBookedSeatIndex() {
35            bookedSeatsByParent = new Map();
36            Object.keys(config.layout.bookings || {}).forEach(seatId => {
37                const parentId = seatId.split('-')[0];
38                if (!bookedSeatsByParent.has(parentId)) bookedSeatsByParent.set(parentId, new Set());
39                bookedSeatsByParent.get(parentId).add(seatId);
40            });
41        }
42
43        const $svg = $('#seating-map-svg');
44        const $summaryList = $('#selected-seats-list');
45        const $totalPrice = $('#total-price');
46        const $addToCartBtn = $('#add-to-cart-btn');
47        const $errorMessage = $('#booking-error-message');
48        const $tooltip = $('#les-seat-tooltip');
49        const $mapWrapper = $('.seating-map-wrapper');
50
51        $('head').append('<style>.les-dimmed { opacity: 0.15; filter: grayscale(100%); transition: opacity 0.3s ease; } .les-highlighted { opacity: 1; filter: none; }</style>');
52
53        function numberToAlpha(num) {
54            let s = '', t;
55            while (num > 0) { t = (num - 1) % 26; s = String.fromCharCode(65 + t) + s; num = (num - 1) / 26 | 0; }
56            return s || undefined;
57        }
58
59        function debounce(func, wait) {
60            let timeout;
61            return function() {
62                const context = this, args = arguments;
63                clearTimeout(timeout);
64                timeout = setTimeout(() => func.apply(context, args), wait);
65            };
66        }
67
68        function init() {
69            $svg.html(`<text x="50%" y="50%" text-anchor="middle">${i18n.loading || 'Loading...'}</text>`);
70            attachEventListeners();
71            
72            $.ajax({
73                url: config.ajax_url, type: 'POST',
74                data: { action: 'les_get_locked_seats', nonce: config.nonce, event_id: config.event_id },
75                success: function(response) {
76                    if ( response.success === false && response.data.license_inactive === true ) return;
77                    if (response.success) {
78                        softLockedSeats = new Set(response.data.locked_seats || []);
79                        gaBookings = response.data.ga_bookings || {};
80                    }
81                },
82                complete: function(xhr) {
83                    if (xhr.responseJSON && xhr.responseJSON.success === false && xhr.responseJSON.data.license_inactive === true) return;
84                    
85                    rebuildBookedSeatIndex();
86                    const dynamicView = findDynamicViewBox(config.layout.elements);
87                    view.x = dynamicView.x; view.y = dynamicView.y; view.w = dynamicView.width; view.h = dynamicView.height;
88                    fullMapBounds = { ...view };
89
90                    renderBaseLayer(); 
91                    updateVisibleSectors(); 
92                    initPriceFilter();
93                    initAccessibilityFilter();
94                    updateSummaryAndValidation();
95                    
96                    if (pollingInterval) clearInterval(pollingInterval);
97                    pollingInterval = setInterval(pollForUpdates, 20000);
98
99                    // --- GROUP BOOKING INTEGRATION: Announce Map is Ready ---
100                    $(document).trigger('les_map_ready', [ config.event_id ]);
101                }
102            });
103            setupMobileUI();
104        }
105    
106        function setupMobileUI() {
107            const $wrapper = $('.seating-summary-wrapper');
108            if (!$wrapper.length) return;
109
110            if ($('.les-mobile-cart-toggle').length === 0) {
111                $('body').append('<button type="button" class="les-mobile-cart-toggle" aria-expanded="false" aria-label=""><span class="dashicons dashicons-tickets-alt" aria-hidden="true"></span><span class="les-mobile-cart-count">0</span><span class="les-mobile-cart-total"></span></button>');
112            }
113            const $toggle = $('.les-mobile-cart-toggle');
114            $toggle.attr('aria-label', i18n.showDetails || 'Show Details');
115
116            $toggle.off('click.lesMobileSummary').on('click.lesMobileSummary', function() {
117                const isExpanded = !$wrapper.hasClass('expanded');
118                $wrapper.toggleClass('expanded', isExpanded);
119                $toggle.attr('aria-expanded', isExpanded ? 'true' : 'false');
120                $toggle.attr('aria-label', isExpanded ? (i18n.hideDetails || 'Hide Details') : (i18n.showDetails || 'Show Details'));
121            });
122
123            updateMobileCartToggle();
124        }
125
126        function updateMobileCartToggle() {
127            const $toggle = $('.les-mobile-cart-toggle');
128            if (!$toggle.length) return;
129
130            const totalSelected = getTotalSelectedSeats();
131            $toggle.find('.les-mobile-cart-count').text(totalSelected);
132            $toggle.find('.les-mobile-cart-total').text(formatPrice(selectedItems.reduce((total, item) => total + getSelectedItemPrice(item), 0)));
133            $toggle.toggleClass('has-items', totalSelected > 0);
134        }
135
136        function getPinchDistance(touches) {
137            return Math.hypot(
138                touches[0].clientX - touches[1].clientX,
139                touches[0].clientY - touches[1].clientY
140            );
141        }
142
143        function isElementMatchingFilter(element, filterValue) {
144            if (!filterValue || !element || !element.variation_id) return false;
145            const parts = filterValue.split('_');
146            const fType = parts[0];
147            const fVid = parts[1];
148            
149            if (fType === 'table') {
150                const fSeats = parseInt(parts[2], 10);
151                return element.type === 'table' && element.whole_table && String(element.variation_id) === String(fVid) && parseInt(element.seats, 10) === fSeats;
152            } else {
153                return String(element.variation_id) === String(fVid) && !(element.type === 'table' && element.whole_table);
154            }
155        }
156
157        function initPriceFilter() {
158            const filterOptionsMap = new Map();
159
160            config.layout.elements.forEach(el => {
161                if (!el.variation_id) return;
162
163                const vid = el.variation_id;
164                const variationData = config.variation_prices || {};
165                const rawData = variationData[vid];
166                let priceVal = 0;
167                let baseLabel = `Tier ${vid}`;
168
169                if (rawData !== undefined && rawData !== null) {
170                    if (typeof rawData === 'object' && rawData.price !== undefined) {
171                        priceVal = parseFloat(rawData.price) || 0;
172                        if (rawData.name) baseLabel = rawData.name;
173                    } else {
174                        priceVal = parseFloat(rawData) || 0;
175                    }
176                }
177
178                if (el.type === 'table' && el.whole_table) {
179                    const seats = parseInt(el.seats, 10) || 0;
180                    const totalTablePrice = priceVal * seats;
181                    const filterKey = `table_${vid}_${seats}`;
182                    if (!filterOptionsMap.has(filterKey)) {
183                        filterOptionsMap.set(filterKey, {
184                            key: filterKey,
185                            price: totalTablePrice,
186                            label: `${baseLabel} (Whole Table - ${seats} Seats) - ${formatPrice(totalTablePrice)}`
187                        });
188                    }
189                } else {
190                    const filterKey = `std_${vid}`;
191                    if (!filterOptionsMap.has(filterKey)) {
192                        filterOptionsMap.set(filterKey, {
193                            key: filterKey,
194                            price: priceVal,
195                            label: `${baseLabel} - ${formatPrice(priceVal)}`
196                        });
197                    }
198                }
199            });
200
201            if (filterOptionsMap.size === 0) return;
202
203            const optionsArray = Array.from(filterOptionsMap.values()).sort((a, b) => a.price - b.price);
204
205            let filterHtml = `<div class="les-filter-wrapper">`;
206            filterHtml += `<select id="les-price-filter-select" class="les-form-control">`;
207            filterHtml += `<option value="">${i18n.filterByPrice || 'Filter by Price Tier'}</option>`;
208            filterHtml += `<option value="">${i18n.showAll || 'Show All'}</option>`;
209
210            optionsArray.forEach(opt => {
211                filterHtml += `<option value="${opt.key}">${opt.label}</option>`;
212            });
213
214            filterHtml += `</select></div>`;
215            $('.les-filter-wrapper').remove();
216            $('.les-view-controls').prepend(filterHtml);
217
218            $('#les-price-filter-select').on('change', function() {
219                const val = $(this).val();
220                currentPriceFilter = val === "" ? null : val;
221                applyPriceFilter();
222            });
223        }
224
225        function applyPriceFilter() {
226            const $elements = $svg.find('g[data-element-id]');
227            if (currentPriceFilter === null) {
228                $elements.removeClass('les-dimmed les-highlighted');
229                $svg.find('.section-preview-layer g').removeClass('les-dimmed les-highlighted');
230            } else {
231                $elements.each(function() {
232                    const $el = $(this);
233                    const elId = $el.attr('data-element-id');
234                    const elementData = config.layout.elements.find(e => e.id === elId);
235                    if (!elementData) return;
236
237                    if (elementData.type === 'section') {
238                        const hasMatchingChild = config.layout.elements.some(child => 
239                            child.parent_id === elId && isElementMatchingFilter(child, currentPriceFilter)
240                        );
241                        if (hasMatchingChild) {
242                            $el.removeClass('les-dimmed').addClass('les-highlighted');
243                            const $previewItems = $el.find('.section-preview-layer g');
244                            $previewItems.each(function() {
245                                const $pItem = $(this);
246                                const pId = $pItem.attr('data-element-id');
247                                const pData = config.layout.elements.find(e => e.id === pId);
248                                if (pData && isElementMatchingFilter(pData, currentPriceFilter)) {
249                                    $pItem.removeClass('les-dimmed').addClass('les-highlighted');
250                                } else {
251                                    $pItem.removeClass('les-highlighted').addClass('les-dimmed');
252                                }
253                            });
254                        } else {
255                            $el.removeClass('les-highlighted').addClass('les-dimmed');
256                        }
257                    } else {
258                        if (isElementMatchingFilter(elementData, currentPriceFilter)) {
259                            $el.removeClass('les-dimmed').addClass('les-highlighted');
260                        } else {
261                            $el.removeClass('les-highlighted').addClass('les-dimmed');
262                        }
263                    }
264                });
265            }
266        }
267
268        // ── Accessibility filter ──────────────────────────────────────────────
269        function initAccessibilityFilter() {
270            const acc = (config.accessibility) || {};
271            if (!acc.enabled) return;
272
273            const hasAny = (config.layout.elements || []).some(el =>
274                el.accessible_seats && el.accessible_seats.length > 0
275            );
276            if (!hasAny) return;
277
278            // Build a fast lookup: seatId → 'wheelchair' | 'companion'
279            const accessibleSeatMap = {};
280            (config.layout.elements || []).forEach(el => {
281                (el.accessible_seats || []).forEach(a => {
282                    accessibleSeatMap[a.id] = a.type || 'wheelchair';
283                });
284            });
285
286            const filterLabel = acc.filterLabel || '♿ Show Accessible Seats Only';
287            const $toggle = $(`
288                <label class="les-accessible-filter-wrap" id="les-accessible-filter-label">
289                    <input type="checkbox" id="les-accessible-filter-cb" />
290                    ${filterLabel}
291                </label>
292            `);
293            $('.les-filter-wrapper').after($toggle);
294            if ($('.les-filter-wrapper').length === 0) {
295                $('.les-view-controls').prepend($toggle);
296            }
297
298            $('#les-accessible-filter-cb').on('change', function () {
299                if (this.checked) {
300                    currentAccessibilityMap = accessibleSeatMap;
301                    applyAccessibilityFilter(accessibleSeatMap);
302                    $('#les-accessible-filter-label').addClass('active');
303                } else {
304                    currentAccessibilityMap = null;
305                    clearAccessibilityFilter();
306                    $('#les-accessible-filter-label').removeClass('active');
307                }
308            });
309        }
310
311        function applyAccessibilityFilter(accessibleSeatMap) {
312            const svgEl = document.getElementById('seating-map-svg');
313            if (!svgEl) return;
314
315            const svgNS = 'http://www.w3.org/2000/svg';
316
317            // ── Build section → accessible seat info map ──────────────────
318            // For each section, count wheelchair + companion seats among its children
319            const sectionAccInfo = {}; // sectionId → { wheelchair: n, companion: n }
320            config.layout.elements.forEach(function(el) {
321                if (!el.parent_id) return; // only child elements
322                (el.accessible_seats || []).forEach(function(a) {
323                    const type = a.type || 'wheelchair';
324                    // Walk up: el.parent_id is a section id
325                    if (!sectionAccInfo[el.parent_id]) {
326                        sectionAccInfo[el.parent_id] = { wheelchair: 0, companion: 0 };
327                    }
328                    sectionAccInfo[el.parent_id][type] = (sectionAccInfo[el.parent_id][type] || 0) + 1;
329                });
330            });
331
332            // Also handle top-level elements (no section parent) that have accessible seats
333            config.layout.elements.forEach(function(el) {
334                if (el.parent_id) return;
335                (el.accessible_seats || []).forEach(function(a) {
336                    const type = a.type || 'wheelchair';
337                    if (!sectionAccInfo['__toplevel__']) {
338                        sectionAccInfo['__toplevel__'] = { wheelchair: 0, companion: 0 };
339                    }
340                    sectionAccInfo['__toplevel__'][type] = (sectionAccInfo['__toplevel__'][type] || 0) + 1;
341                });
342            });
343
344            const isOverview = svgEl.classList.contains('zoom-level-overview');
345
346            if (isOverview) {
347                // ── OVERVIEW MODE: operate on section groups ──────────────
348                const allSectionGroups = svgEl.querySelectorAll('g[id^="section-group-"]');
349
350                allSectionGroups.forEach(function(sectionG) {
351                    const sectionId = sectionG.id.replace('section-group-', '');
352                    const info = sectionAccInfo[sectionId];
353
354                    if (!info || (info.wheelchair + info.companion) === 0) {
355                        // No accessible seats → dim the whole section group
356                        sectionG.setAttribute('data-acc-prev-opacity', sectionG.style.opacity || '');
357                        sectionG.style.opacity = '0.15';
358                        sectionG.setAttribute('data-acc-dimmed', '1');
359                    } else {
360                        // Has accessible seats → highlight boundary + add badge
361                        const boundary = sectionG.querySelector('.section-boundary-guide');
362                        if (boundary) {
363                            boundary.setAttribute('data-acc-prev-stroke', boundary.style.stroke || '');
364                            boundary.setAttribute('data-acc-prev-sw', boundary.getAttribute('stroke-width') || '');
365                            boundary.style.stroke = '#1a73e8';
366                            boundary.style.strokeWidth = '4px';
367                            boundary.style.filter = 'drop-shadow(0 0 6px rgba(26,115,232,0.7))';
368                            boundary.setAttribute('data-acc-highlighted', '1');
369                        }
370
371                        // Build badge text: e.g. "♿ 4  👤 2"
372                        let badgeParts = [];
373                        if (info.wheelchair > 0) badgeParts.push('♿ ' + info.wheelchair);
374                        if (info.companion > 0) badgeParts.push('👤 ' + info.companion);
375                        const badgeText = badgeParts.join('   ');
376
377                        // Get section element data for badge positioning
378                        const sectionEl = config.layout.elements.find(function(e) { return e.id === sectionId; });
379                        const badgeY = sectionEl ? ((sectionEl.height || 300) / 2) - 20 : 120;
380
381                        // Create SVG badge group
382                        const badgeG = document.createElementNS(svgNS, 'g');
383                        badgeG.setAttribute('class', 'acc-filter-badge');
384                        badgeG.setAttribute('pointer-events', 'none');
385
386                        // Badge pill background
387                        const badgeRect = document.createElementNS(svgNS, 'rect');
388                        badgeRect.setAttribute('x', '-52');
389                        badgeRect.setAttribute('y', String(badgeY - 14));
390                        badgeRect.setAttribute('width', '104');
391                        badgeRect.setAttribute('height', '28');
392                        badgeRect.setAttribute('rx', '14');
393                        badgeRect.setAttribute('fill', '#1a73e8');
394                        badgeRect.setAttribute('fill-opacity', '0.92');
395
396                        // Badge text
397                        const badgeTxt = document.createElementNS(svgNS, 'text');
398                        badgeTxt.setAttribute('x', '0');
399                        badgeTxt.setAttribute('y', String(badgeY + 5));
400                        badgeTxt.setAttribute('text-anchor', 'middle');
401                        badgeTxt.setAttribute('dominant-baseline', 'central');
402                        badgeTxt.setAttribute('font-size', '14');
403                        badgeTxt.setAttribute('fill', '#ffffff');
404                        badgeTxt.setAttribute('font-weight', 'bold');
405                        badgeTxt.textContent = badgeText;
406
407                        badgeG.appendChild(badgeRect);
408                        badgeG.appendChild(badgeTxt);
409                        sectionG.appendChild(badgeG);
410                    }
411                });
412
413            } else {
414                // ── DETAIL MODE: force-render all sections, dim/restore individual seats ──
415                const sections = config.layout.elements.filter(function(el) {
416                    return !el.parent_id && el.type === 'section';
417                });
418                sections.forEach(function(section) {
419                    if (!renderedSections.has(section.id)) {
420                        const seatContainer = document.getElementById('seats-' + section.id);
421                        if (seatContainer) {
422                            renderSectionChildren(seatContainer, section, section.id === activeDetailSectionId);
423                            renderedSections.add(section.id);
424                        }
425                    }
426                });
427
428                // Dim all seats, GA areas, tables
429                svgEl.querySelectorAll('circle.seat').forEach(function(c) {
430                    c.setAttribute('data-acc-prev-opacity', c.style.opacity || '');
431                    c.style.opacity = '0.1';
432                });
433                svgEl.querySelectorAll('text.seat-label').forEach(function(t) {
434                    t.setAttribute('data-acc-prev-opacity', t.style.opacity || '');
435                    t.style.opacity = '0.1';
436                });
437                svgEl.querySelectorAll('.ga-area-shape, .ga-area-label').forEach(function(el) {
438                    el.setAttribute('data-acc-prev-opacity', el.style.opacity || '');
439                    el.style.opacity = '0.1';
440                });
441                svgEl.querySelectorAll('.table-shape, .table-label').forEach(function(el) {
442                    el.setAttribute('data-acc-prev-opacity', el.style.opacity || '');
443                    el.style.opacity = '0.1';
444                });
445
446                // Restore accessible seat circles
447                svgEl.querySelectorAll('circle.seat').forEach(function(circle) {
448                    const seatId = circle.getAttribute('data-seat-id');
449                    if (!seatId || !accessibleSeatMap[seatId]) return;
450                    circle.style.opacity = '1';
451                    circle.setAttribute('data-acc-prev-stroke', circle.style.stroke || '');
452                    circle.setAttribute('data-acc-prev-sw', circle.style.strokeWidth || '');
453                    const isCompanion = accessibleSeatMap[seatId] === 'companion';
454                    circle.style.stroke = isCompanion ? '#1565c0' : '#0d47a1';
455                    circle.style.strokeWidth = '2.5px';
456                    const parentG = circle.parentNode;
457                    if (parentG) {
458                        parentG.querySelectorAll('text.seat-label').forEach(function(t) {
459                            t.style.opacity = '1';
460                        });
461                    }
462                });
463            }
464        }
465
466        function clearAccessibilityFilter() {
467            const svgEl = document.getElementById('seating-map-svg');
468            if (!svgEl) return;
469
470            // Remove all injected badge groups
471            svgEl.querySelectorAll('.acc-filter-badge').forEach(function(el) {
472                el.parentNode && el.parentNode.removeChild(el);
473            });
474
475            // Restore dimmed section groups
476            svgEl.querySelectorAll('g[data-acc-dimmed]').forEach(function(g) {
477                g.style.opacity = g.getAttribute('data-acc-prev-opacity') || '';
478                g.removeAttribute('data-acc-prev-opacity');
479                g.removeAttribute('data-acc-dimmed');
480            });
481
482            // Restore highlighted section boundaries
483            svgEl.querySelectorAll('.section-boundary-guide[data-acc-highlighted]').forEach(function(b) {
484                b.style.stroke = b.getAttribute('data-acc-prev-stroke') || '';
485                b.style.strokeWidth = b.getAttribute('data-acc-prev-sw') || '';
486                b.style.filter = '';
487                b.removeAttribute('data-acc-prev-stroke');
488                b.removeAttribute('data-acc-prev-sw');
489                b.removeAttribute('data-acc-highlighted');
490            });
491
492            // Restore individually dimmed elements (detail mode)
493            svgEl.querySelectorAll('[data-acc-prev-opacity]').forEach(function(el) {
494                el.style.opacity = el.getAttribute('data-acc-prev-opacity') || '';
495                el.removeAttribute('data-acc-prev-opacity');
496            });
497
498            // Restore stroke overrides on accessible seat circles
499            svgEl.querySelectorAll('circle.seat[data-acc-prev-stroke]').forEach(function(circle) {
500                circle.style.stroke = circle.getAttribute('data-acc-prev-stroke') || '';
501                circle.style.strokeWidth = circle.getAttribute('data-acc-prev-sw') || '';
502                circle.removeAttribute('data-acc-prev-stroke');
503                circle.removeAttribute('data-acc-prev-sw');
504            });
505        }
506
507        function pollForUpdates() {
508            $.ajax({
509                url: config.ajax_url, type: 'POST',
510                data: { action: 'les_get_locked_seats', nonce: config.nonce, event_id: config.event_id },
511                success: function(response) {
512                    if (response.success) {
513                        const newLocked = response.data.locked_seats || [];
514                        const newGa = response.data.ga_bookings || {};
515                        const newBookings = response.data.bookings || {};
516
517                        const lockedChanged = newLocked.length !== softLockedSeats.size;
518                        const gaChanged = JSON.stringify(newGa) !== JSON.stringify(gaBookings);
519                        const bookingsChanged = JSON.stringify(newBookings) !== JSON.stringify(config.layout.bookings || {});
520
521                        if (lockedChanged || gaChanged || bookingsChanged) {
522                            softLockedSeats = new Set(newLocked);
523                            gaBookings = newGa;
524                            if (bookingsChanged) {
525                                config.layout.bookings = newBookings;
526                                rebuildBookedSeatIndex();
527                            }
528                            refreshSeatStatus();
529                            updateSummaryAndValidation();
530                        }
531                    }
532                }
533            });
534        }
535
536        function renderBaseLayer() {
537            $svg.empty();
538            renderedSections.clear();
539            const defs = createSVGElement('defs');
540            $svg.append(defs);
541            $svg.attr('viewBox', `${view.x} ${view.y} ${view.w} ${view.h}`);
542            const mainLayer = createSVGElement('g', { id: 'main-layer' });
543
544            // --- Background Image ---
545            const settings = config.layout && config.layout.settings;
546            if (settings && settings.backgroundImage) {
547                const bgW = settings.backgroundImageWidth  || settings.canvasWidth  || view.w;
548                const bgH = settings.backgroundImageHeight || settings.canvasHeight || view.h;
549                const bgImg = createSVGElement('image', {
550                    href: settings.backgroundImage,
551                    x: 0,
552                    y: 0,
553                    width:  bgW,
554                    height: bgH,
555                    preserveAspectRatio: 'xMidYMid meet'
556                });
557                mainLayer.appendChild(bgImg);
558            }
559            // --- END Background Image ---
560
561            let topLevelElements = config.layout.elements.filter(el => !el.parent_id);
562            topLevelElements.sort((a, b) => {
563                if (a.type === 'section' && b.type !== 'section') return -1;
564                if (a.type !== 'section' && b.type === 'section') return 1;
565                return 0;
566            });
567
568            topLevelElements.forEach(element => {
569                const secScaleX = element.flipX ? -1 : 1;
570                const secScaleY = element.flipY ? -1 : 1;
571                const group = createSVGElement('g', { 
572                    transform: `translate(${element.x}, ${element.y}) rotate(${element.rotation || 0}) scale(${secScaleX}, ${secScaleY})`,
573                    id: `section-group-${element.id}`, 
574                    'data-element-id': element.id,
575                    'data-variation-id': element.variation_id || '' 
576                });
577                
578                if (element.type === 'section') {
579                    drawSectionOverview(group, element);
580                } else {
581                    drawElement(group, element); 
582                }
583                
584                if (currentPriceFilter !== null) {
585                    if (isElementMatchingFilter(element, currentPriceFilter)) {
586                        group.classList.add('les-highlighted');
587                    } else {
588                        group.classList.add('les-dimmed');
589                    }
590                }
591                mainLayer.appendChild(group);
592            });
593            $svg.append(mainLayer);
594
595            if ($('#les-zoom-back-btn').length === 0) {
596                $('.les-view-controls').prepend('<button type="button" class="les-zoom-btn" id="les-zoom-back-btn" title="Reset View" style="display: none;"><span class="dashicons dashicons-arrow-left-alt2"></span></button>');
597            }
598            if (currentPriceFilter !== null) applyPriceFilter();
599            if (currentAccessibilityMap !== null) {
600                clearAccessibilityFilter();
601                applyAccessibilityFilter(currentAccessibilityMap);
602            }
603        }
604
605
606        function drawSectionOverview(group, section) {
607            let boundaryShape;
608            const shapeAttrs = { 'data-section-id': section.id, class: 'section-boundary-guide' };
609            if (section.shape === 'polygon' && section.points) {
610                const pointsString = section.points.map(p => `${p.x},${p.y}`).join(' ');
611                boundaryShape = createSVGElement('polygon', { points: pointsString, ...shapeAttrs });
612            } else {
613                const width = section.width || 400; const height = section.height || 300;
614                boundaryShape = createSVGElement('rect', { x: -width / 2, y: -height / 2, width: width, height: height, rx: 5, ...shapeAttrs });
615            }
616            const secColor = section.color || '#e2e4e7';
617            boundaryShape.setAttribute('style', `fill: ${secColor}`);
618            group.appendChild(boundaryShape);
619
620            const childElements = config.layout.elements.filter(el => el.parent_id === section.id);
621            if (childElements.length > 0) {
622                const previewGroup = createSVGElement('g', { class: 'section-preview-layer', 'pointer-events': 'none' });
623                childElements.forEach(child => {
624                    const visibleTypes = ['line', 'table', 'stage', 'image', 'text', 'ga_area', 'polygon', 'seat_block'];
625                    if(visibleTypes.includes(child.type)) {
626                        const childScaleX = child.flipX ? -1 : 1; 
627                        const childScaleY = child.flipY ? -1 : 1;
628                        const childG = createSVGElement('g', { 
629                            transform: `translate(${child.x}, ${child.y}) rotate(${child.rotation || 0}) scale(${childScaleX}, ${childScaleY})`,
630                            'data-element-id': child.id,
631                            'data-variation-id': child.variation_id || ''
632                        });
633                        
634                        if(child.type === 'line') drawLineElement(childG, child);
635                        else if (child.type === 'text') drawTextElement(childG, child);
636                        else if (child.type === 'image') drawImageElement(childG, child);
637                        else if (child.type === 'stage') drawStage(childG, child);
638                        else if (child.type === 'ga_area') drawGAArea(childG, child); 
639                        else if (child.type === 'polygon') drawPolygonShape(childG, child);
640                        else if (child.type === 'seat_block') {
641                            const rows = child.rows || 5;
642                            const seatsPerRow = child.seats_per_row || 10;
643                            const blockHeight = child.height || 100;
644                            const blockWidth = child.width || 200;
645                            const rowSpacing = blockHeight / rows;
646                             const colorAvailable = child.seatColor || child.color || '#4CAF50';
647                             const colorBlocked = config.color_booked || '#BDBDBD'; 
648                             const STROKE_WIDTH = 2; 
649                            const rowSkewX = child.row_skew || 0;
650                            const rowSkewY = child.row_skew_y || 0;
651
652                            for(let r=1; r<=rows; r++) {
653                                 const rowY = (-blockHeight / 2) + (r * rowSpacing) - (rowSpacing / 2);
654                                 let visualRowLabel = r;
655                                 if (child.row_label_style === 'alpha_upper') { visualRowLabel = numberToAlpha(r); }
656                                 visualRowLabel = (child.row_label_prefix || '') + visualRowLabel;
657                                 const skewOffsetX = (r - 1) * rowSkewX;
658                                 let currentPoints = [];
659                                 let currentType = null; 
660
661                                 const flushSegment = () => {
662                                     if (currentPoints.length === 0) return;
663                                     const strokeColor = (currentType === 'blocked') ? colorBlocked : colorAvailable;
664                                     if (currentPoints.length === 1) {
665                                         childG.appendChild(createSVGElement('circle', { cx: currentPoints[0].x, cy: currentPoints[0].y, r: 2, fill: strokeColor }));
666                                     } else {
667                                         const d = currentPoints.map((p, i) => (i === 0 ? 'M' : 'L') + `${p.x},${p.y}`).join(' ');
668                                         childG.appendChild(createSVGElement('path', { d: d, stroke: strokeColor, 'stroke-width': STROKE_WIDTH, fill: 'none', 'vector-effect': 'non-scaling-stroke' }));
669                                     }
670                                     return currentPoints[currentPoints.length - 1]; 
671                                 };
672
673                                 const curve = child.curve || 0;
674                                 const seatSpacing = blockWidth / seatsPerRow;
675                                 const absCurve = Math.abs(curve);
676                                 const theta = absCurve * (Math.PI / 180);
677                                 const radius = (blockWidth / 2) / Math.sin(theta / 2);
678                                 const startAngle = -theta / 2;
679                                 const angleStep = seatsPerRow > 1 ? theta / (seatsPerRow - 1) : 0;
680
681                                 for(let s=1; s<=seatsPerRow; s++) {
682                                     const seatId = `${child.id}-R${visualRowLabel}-S${s}`;
683                                     if (child.killed_seats && child.killed_seats.includes(seatId)) {
684                                         flushSegment(); currentPoints = []; currentType = null; continue;
685                                     }
686                                     const isBlocked = (child.unavailable_seats && child.unavailable_seats.includes(seatId)) || child.status === 'booked';
687                                     const thisType = isBlocked ? 'blocked' : 'available';
688                                     const skewOffsetY = (s - 1) * rowSkewY;
689                                     let sx, sy;
690                                     if(absCurve < 1) { 
691                                         sx = (-blockWidth / 2) + (s * seatSpacing) - (seatSpacing / 2); sy = rowY;
692                                     } else { 
693                                         const currentAngle = startAngle + (s - 1) * angleStep;
694                                         sx = radius * Math.sin(currentAngle);
695                                         const bulge = radius * (Math.cos(currentAngle) - Math.cos(theta / 2));
696                                         if (curve > 0) sy = rowY - bulge; else sy = rowY + bulge;
697                                     }
698                                     const newPoint = { x: sx + skewOffsetX, y: sy + skewOffsetY };
699                                     if (currentType !== null && currentType !== thisType) {
700                                         const lastPoint = flushSegment();
701                                         currentPoints = [lastPoint, newPoint];
702                                     } else {
703                                         currentPoints.push(newPoint);
704                                     }
705                                     currentType = thisType;
706                                 }
707                                 flushSegment();
708                             }
709                        }
710                        else if (child.type === 'table') {
711                            const tShapeAttrs = { style: 'fill: none; stroke: #888; stroke-width: 1;' };
712                            if (child.shape === 'circle') {
713                                childG.appendChild(createSVGElement('circle', { r: child.radius || 60, ...tShapeAttrs }));
714                            } else {
715                                const w = child.width||180, h = child.height||70;
716                                childG.appendChild(createSVGElement('rect', { x: -w/2, y: -h/2, width: w, height: h, ...tShapeAttrs }));
717                            }
718                        }
719                        previewGroup.appendChild(childG);
720                    }
721                });
722                group.appendChild(previewGroup);
723            }
724
725            const secScaleX = section.flipX ? -1 : 1;
726            const secScaleY = section.flipY ? -1 : 1;
727            const labelAttrs = { class: 'section-label', 'pointer-events': 'none', 'text-anchor': 'middle', 'dominant-baseline': 'central' };
728            if (section.textColor) labelAttrs.fill = section.textColor;
729            const label = createSVGElement('text', { ...labelAttrs, transform: `scale(${secScaleX}, ${secScaleY})` });
730            label.textContent = section.label;
731            group.appendChild(label);
732            const seatLayer = createSVGElement('g', { class: 'section-seats-layer', id: `seats-${section.id}` });
733            group.appendChild(seatLayer);
734        }
735
736        function updateVisibleSectors() {
737            const isSingleLayout = config.layout.settings.mode === 'single';
738            const detailThreshold = fullMapBounds.w * 0.3;
739            const isFocusedDetailMode = activeDetailSectionId && view.w < (fullMapBounds.w * 0.95);
740            const isDetailMode = isSingleLayout || isFocusedDetailMode || (view.w < detailThreshold);
741            
742            if (!isDetailMode) {
743                if (!$svg.hasClass('zoom-level-overview')) {
744                    $svg.addClass('zoom-level-overview').removeClass('zoom-level-detail');
745                    $('#les-zoom-back-btn').fadeOut();
746                    $('.section-seats-layer').empty(); 
747                    renderedSections.clear();
748                }
749                return; 
750            } else {
751                if (!$svg.hasClass('zoom-level-detail')) {
752                    $svg.addClass('zoom-level-detail').removeClass('zoom-level-overview');
753                    if (!isSingleLayout) {
754                        $('#les-zoom-back-btn').fadeIn();
755                    } else {
756                        $('#les-zoom-back-btn').hide();
757                    }
758                }
759            }
760
761            const topLevelElements = config.layout.elements.filter(el => !el.parent_id && el.type === 'section');
762            
763            topLevelElements.forEach(section => {
764                const seatContainer = document.getElementById(`seats-${section.id}`);
765                if (!seatContainer) return;
766
767                if (!renderedSections.has(section.id)) {
768                    renderSectionChildren(seatContainer, section, section.id === activeDetailSectionId);
769                    renderedSections.add(section.id);
770                }
771            });
772            updateSelectionVisuals();
773            if (currentPriceFilter !== null) applyPriceFilter();
774            if (currentAccessibilityMap !== null) {
775                clearAccessibilityFilter();
776                applyAccessibilityFilter(currentAccessibilityMap);
777            }
778        }
779
780        function renderSectionChildren(container, section, includeDecorativeElements = true) {
781            const childElements = config.layout.elements.filter(el => el.parent_id === section.id);
782            const frag = document.createDocumentFragment();
783            childElements.forEach(child => {
784            if (!includeDecorativeElements && !['table', 'seat_block', 'ga_area'].includes(child.type)) return;
785                const childScaleX = child.flipX ? -1 : 1; const childScaleY = child.flipY ? -1 : 1;
786                const childGroup = createSVGElement('g', { 
787                    transform: `translate(${child.x}, ${child.y}) rotate(${child.rotation || 0}) scale(${childScaleX}, ${childScaleY})`,
788                    'data-element-id': child.id,
789                    'data-variation-id': child.variation_id || '' 
790                });
791                drawElement(childGroup, child); 
792                
793                if (currentPriceFilter !== null) {
794                    if (isElementMatchingFilter(child, currentPriceFilter)) {
795                        childGroup.classList.add('les-highlighted');
796                    } else {
797                        childGroup.classList.add('les-dimmed');
798                    }
799                }
800                
801                frag.appendChild(childGroup);
802            });
803            container.appendChild(frag);
804        }
805
806        function refreshSeatStatus() {
807            // Update individual seats
808            $svg.find('.seat').each(function() {
809                const $el = $(this);
810                const seatId = $el.data('seat-id');
811                const parentId = $el.data('parent-id');
812                const parentElement = config.layout.elements.find(el => el.id === parentId);
813                const isManuallyBooked = parentElement && parentElement.status === 'booked';
814                const isPermanentlyBooked = config.layout.bookings && Boolean(config.layout.bookings[seatId]);
815                const isSoftLocked = softLockedSeats.has(seatId);
816                const isUnavailable = parentElement && parentElement.unavailable_seats && parentElement.unavailable_seats.includes(seatId);
817                const isBooked = isManuallyBooked || isPermanentlyBooked || isSoftLocked || isUnavailable;
818
819                const accessEntry = parentElement ? (parentElement.accessible_seats || []).find(a => a.id === seatId) : null;
820                let seatClass = isBooked ? 'seat booked' : 'seat available';
821                if (accessEntry) {
822                    seatClass += (accessEntry.type === 'companion') ? ' accessible-companion' : ' accessible-wheelchair';
823                }
824                if ($el.hasClass('selected')) {
825                    seatClass += ' selected';
826                }
827                $el.attr('class', seatClass);
828
829                if (isBooked) {
830                    $el.removeAttr('style');
831                } else if (accessEntry) {
832                    $el.attr('style', (accessEntry.type === 'companion') ? 'fill: #64b5f6' : 'fill: #1a73e8');
833                } else if (parentElement && parentElement.seatColor) {
834                    $el.attr('style', `fill: ${parentElement.seatColor}`);
835                } else {
836                    $el.removeAttr('style');
837                }
838            });
839
840            // Update whole tables
841            $svg.find('.whole-table-booking').each(function() {
842                const $group = $(this);
843                const tableId = $group.data('element-id') || $group.find('.table-shape').data('table-id');
844                const table = config.layout.elements.find(el => el.id === tableId);
845                if (!table || !table.whole_table) return;
846                const firstSeatId = `${table.id}-S1`;
847                const isManuallyBooked = table.status === 'booked';
848                const isPermanentlyBooked = config.layout.bookings && (config.layout.bookings[firstSeatId] || config.layout.bookings[table.id]);
849                const isSoftLocked = softLockedSeats.has(firstSeatId) || softLockedSeats.has(table.id);
850                const isBooked = isManuallyBooked || isPermanentlyBooked || isSoftLocked;
851                const $shape = $group.find('.table-shape');
852                if (isBooked) {
853                    $group.addClass('booked').removeClass('available');
854                    $shape.addClass('booked').removeClass('available');
855                } else {
856                    $group.removeClass('booked').addClass('available');
857                    $shape.removeClass('booked').addClass('available');
858                }
859            });
860
861            // Update GA areas
862            $svg.find('.ga-area-shape').each(function() {
863                const $shape = $(this);
864                const areaId = $shape.data('area-id');
865                const area = config.layout.elements.find(el => el.id === areaId);
866                if (!area) return;
867                const sold = gaBookings[area.id] || 0;
868                const remaining = area.capacity - sold;
869                const isSoldOut = remaining <= 0;
870                const isManuallyBooked = area.status === 'booked';
871                const isBooked = isSoldOut || isManuallyBooked;
872                if (isBooked) {
873                    $shape.addClass('booked').removeClass('available');
874                } else {
875                    $shape.removeClass('booked').addClass('available');
876                }
877                const $group = $shape.closest('g');
878                const $capText = $group.find('.ga-capacity-text');
879                if ($capText.length) {
880                    if (isManuallyBooked) {
881                        $capText.text(i18n.unavailable || 'Unavailable');
882                    } else if (isSoldOut) {
883                        $capText.text(i18n.soldOut || 'Sold Out');
884                    } else {
885                        $capText.text((i18n.gaAvailable || '%1$d / %2$d Available').replace('%1$d', remaining).replace('%2$d', area.capacity));
886                    }
887                }
888            });
889        }
890
891        function handleSectionClick(e) {
892            if (!$svg.hasClass('zoom-level-overview')) return;
893            handleTooltipHide();
894            const sectionId = $(e.currentTarget).data('section-id');
895            const sectionElement = config.layout.elements.find(el => el.id === sectionId);
896            if (!sectionElement) return;
897
898            const childElements = config.layout.elements.filter(el => el.parent_id === sectionId);
899            if (childElements.length === 0) return;
900
901            activeDetailSectionId = sectionId;
902
903            let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
904            childElements.forEach(el => {
905                const sectionOffsetX = parseFloat(sectionElement.x) || 0;
906                const sectionOffsetY = parseFloat(sectionElement.y) || 0;
907                const localX = (parseFloat(el.x) || 0) + sectionOffsetX;
908                const localY = (parseFloat(el.y) || 0) + sectionOffsetY;
909                const w = parseFloat(el.width) || 0;
910                const h = parseFloat(el.height) || 0;
911                const r = (parseFloat(el.radius) || 0) * 2;
912                const halfSize = Math.max(w, h, r) / 2;
913
914                minX = Math.min(minX, localX - halfSize);
915                minY = Math.min(minY, localY - halfSize);
916                maxX = Math.max(maxX, localX + halfSize);
917                maxY = Math.max(maxY, localY + halfSize);
918            });
919
920            const buffer = 40;
921            const contentW = Math.max(maxX - minX, 1);
922            const contentH = Math.max(maxY - minY, 1);
923            const maxDetailW = Math.max(fullMapBounds.w * 0.28, 1);
924            const maxDetailH = Math.max(fullMapBounds.h * 0.28, 1);
925
926            let targetW = Math.min(contentW + (buffer * 2), maxDetailW);
927            let targetH = Math.min(contentH + (buffer * 2), maxDetailH);
928            let targetX = minX - buffer;
929            let targetY = minY - buffer;
930
931            const svgWidth = $svg.width();
932            const svgHeight = $svg.height();
933            if (svgWidth > 0 && svgHeight > 0) {
934                const containerAspect = svgWidth / svgHeight;
935                const targetAspect = targetW / targetH;
936                if (targetAspect > containerAspect) {
937                    const oldH = targetH;
938                    targetH = targetW / containerAspect;
939                    const centerY = targetY + oldH / 2;
940                    targetY = centerY - targetH / 2;
941                } else {
942                    const oldW = targetW;
943                    targetW = targetH * containerAspect;
944                    const centerX = targetX + oldW / 2;
945                    targetX = centerX - targetW / 2;
946                }
947            }
948
949            const centerX = (minX + maxX) / 2;
950            const centerY = (minY + maxY) / 2;
951            targetX = centerX - (targetW / 2);
952            targetY = centerY - (targetH / 2);
953
954            animateViewBox(targetX, targetY, targetW, targetH, 600, function() {
955                updateVisibleSectors();
956            });
957        }
958
959        function handlePanMove(e) {
960            const type = e.type;
961            const isTouch = type === 'touchmove';
962
963            if (e.cancelable) e.preventDefault(); 
964            
965            if (isTouch && e.touches.length === 2 && view.isPinching) {
966                const newDist = getPinchDistance(e.touches);
967                if (view.startPinchDist > 0) {
968                    const zoomFactor = newDist / view.startPinchDist;
969                    applyZoom(zoomFactor);
970                    view.startPinchDist = newDist;
971                    requestAnimationFrame(debouncedUpdateSectors);
972                }
973                return;
974            }
975
976            if (!view.isPanning) return;
977            
978            let clientX, clientY;
979            if (isTouch) {
980                clientX = e.touches[0].clientX;
981                clientY = e.touches[0].clientY;
982            } else {
983                clientX = e.clientX;
984                clientY = e.clientY;
985            }
986
987            const dx = clientX - view.panStart.x;
988            const dy = clientY - view.panStart.y;
989            
990            const svgDeltaX = dx * (view.w / $svg.width());
991            const svgDeltaY = dy * (view.h / $svg.height());
992            
993            view.x -= svgDeltaX; 
994            view.y -= svgDeltaY;
995            
996            view.panStart.x = clientX; 
997            view.panStart.y = clientY; 
998            
999            requestAnimationFrame(() => {
1000                $svg.attr('viewBox', `${view.x} ${view.y} ${view.w} ${view.h}`);
1001            });
1002            debouncedUpdateSectors(); 
1003        }
1004        
1005        const debouncedUpdateSectors = debounce(updateVisibleSectors, 100);
1006
1007        function handleZoomReset() {
1008            activeDetailSectionId = null;
1009            animateViewBox(fullMapBounds.x, fullMapBounds.y, fullMapBounds.w, fullMapBounds.h, 500, function() {
1010                updateVisibleSectors();
1011            });
1012        }
1013
1014        function drawElement(group, element) { 
1015            switch(element.type) { 
1016                case 'table': drawTableAndSeats(group, element); break; 
1017                case 'seat_block': drawSeatBlockDetail(group, element); break; 
1018                case 'ga_area': drawGAArea(group, element); break; 
1019                case 'stage': drawStage(group, element); break; 
1020                case 'image': drawImageElement(group, element); break; 
1021                case 'polygon': drawPolygonShape(group, element); break; 
1022                case 'text': drawTextElement(group, element); break; 
1023                case 'line': drawLineElement(group, element); break; 
1024            } 
1025        }
1026
1027        function drawLineElement(group, element) {
1028            const w = element.width || 100;
1029            const strokeWidth = element.strokeWidth || 2;
1030            const color = element.color || '#000000';
1031            group.appendChild(createSVGElement('line', { x1: -w / 2, y1: 0, x2: w / 2,  y2: 0, stroke: color, 'stroke-width': strokeWidth, 'pointer-events': 'none' }));
1032        }
1033
1034        function drawTextElement(group, element) { const labelAttrs = { x: 0, y: 0, class: 'static-text-element', 'pointer-events': 'none', 'text-anchor': 'middle', 'dominant-baseline': 'central' }; if (element.textColor) labelAttrs.fill = element.textColor; if (element.fontSize) labelAttrs['font-size'] = `${element.fontSize}px`; const label = createSVGElement('text', labelAttrs); label.textContent = element.label; group.appendChild(label); }
1035
1036        function drawSeatBlockDetail(group, block) {
1037             const blockWidth = block.width || 200, blockHeight = block.height || 100; 
1038             const rows = block.rows || 5, seatsPerRow = block.seats_per_row || 10; 
1039             const seatSize = 8; 
1040             const rowSpacing = blockHeight / rows; 
1041             const curve = block.curve || 0; 
1042             const seatLabelStyle = block.seat_label_style || 'numeric'; 
1043             const startAt = block.seat_start_at || 1; 
1044             const rowLabelStyle = block.row_label_style || 'numeric'; 
1045             const rowStartAt = block.row_start_at || 1;
1046             const rowLabelPrefix = block.row_label_prefix || '';
1047             const rowLabelPosition = block.row_label_position || 'left';
1048             const rowSkewX = block.row_skew || 0;
1049             const rowSkewY = block.row_skew_y || 0;
1050
1051             let parentFlipX = 1; let parentFlipY = 1;
1052             if (block.parent_id) {
1053                const parent = config.layout.elements.find(el => el.id === block.parent_id);
1054                if (parent) { if (parent.flipX) parentFlipX = -1; if (parent.flipY) parentFlipY = -1; }
1055             }
1056             const tsX = parentFlipX * (block.flipX ? -1 : 1);
1057             const tsY = parentFlipY * (block.flipY ? -1 : 1);
1058             const getTextTransform = (x, y) => `translate(${x}, ${y}) scale(${tsX}, ${tsY})`;
1059
1060             const labelAttrs = { class: 'table-label', 'pointer-events': 'none' }; 
1061             if (block.textColor) labelAttrs.fill = block.textColor; 
1062             if (block.fontSize) labelAttrs['font-size'] = `${block.fontSize}px`; 
1063             const position = block.label_position || 'top'; 
1064             let labelX = 0, labelY = 0; 
1065             let anchor = 'middle';
1066             switch (position) { 
1067                 case 'bottom': labelY = (blockHeight / 2) + 20; break; 
1068                 case 'left': anchor = 'end'; labelX = (-blockWidth / 2) - 10; break; 
1069                 case 'right': anchor = 'start'; labelX = (blockWidth / 2) + 10; break; 
1070                 default: labelY = (-blockHeight / 2) - 10; 
1071             } 
1072             const labelAttrsWithTitle = { ...labelAttrs, 'text-anchor': anchor, transform: getTextTransform(labelX, labelY), title: block.label };
1073             if (block.label_hidden) {
1074                 labelAttrsWithTitle.display = 'none';
1075             }
1076             const label = createSVGElement('text', labelAttrsWithTitle); 
1077             label.textContent = block.label; 
1078             group.appendChild(label); 
1079             
1080             for (let r = 1; r <= rows; r++) { 
1081                const rowY = (-blockHeight / 2) + (r * rowSpacing) - (rowSpacing / 2); 
1082                let currentRowVal = rowStartAt + (r - 1);
1083                let visualRowLabel = currentRowVal;
1084                if (rowLabelStyle === 'alpha_upper') { visualRowLabel = numberToAlpha(currentRowVal); } 
1085                visualRowLabel = rowLabelPrefix + visualRowLabel;
1086                const skewOffsetX = (r - 1) * rowSkewX;
1087
1088                let sideLabelY = rowY;
1089                if (Math.abs(curve) >= 1) {
1090                    const theta = Math.abs(curve) * (Math.PI / 180);
1091                    const radius = (blockWidth / 2) / Math.sin(theta / 2);
1092                    const startAngle = -theta / 2;
1093                    const bulge = radius * (Math.cos(startAngle) - Math.cos(theta / 2));
1094                    if (curve > 0) sideLabelY = rowY - bulge; else sideLabelY = rowY + bulge;
1095                }
1096                let sideLabelX = (-blockWidth / 2) + skewOffsetX - 25;
1097                let rowLabelAnchor = 'end';
1098                if (rowLabelPosition === 'right') {
1099                    sideLabelX = (blockWidth / 2) + skewOffsetX + 25;
1100                    rowLabelAnchor = 'start';
1101                } else if (rowLabelPosition === 'top') {
1102                    sideLabelX = skewOffsetX;
1103                    sideLabelY -= 15;
1104                    rowLabelAnchor = 'middle';
1105                } else if (rowLabelPosition === 'bottom') {
1106                    sideLabelX = skewOffsetX;
1107                    sideLabelY += 15;
1108                    rowLabelAnchor = 'middle';
1109                }
1110                const rowLabelText = createSVGElement('text', { class: 'row-name-label', 'text-anchor': rowLabelAnchor, 'dominant-baseline': 'central', 'font-size': '12px', fill: '#888', 'pointer-events': 'none', transform: `translate(${sideLabelX}, ${sideLabelY}) scale(${tsX}, ${tsY})` });
1111                if (block.row_label_hidden) rowLabelText.setAttribute('display', 'none');
1112                rowLabelText.textContent = visualRowLabel;
1113                group.appendChild(rowLabelText);
1114
1115                if (Math.abs(curve) < 1) { 
1116                    const seatSpacing = blockWidth / seatsPerRow; 
1117                    for (let s = 1; s <= seatsPerRow; s++) { 
1118                        const seatX = (-blockWidth / 2) + (s * seatSpacing) - (seatSpacing / 2); 
1119                        const seatId = `${block.id}-R${visualRowLabel}-S${s}`; 
1120                        if (block.killed_seats && block.killed_seats.includes(seatId)) continue; 
1121                        let visualSeatLabel = startAt + (s - 1); 
1122                        if (seatLabelStyle === 'alpha_upper') { visualSeatLabel = numberToAlpha(startAt + (s - 1)); } 
1123                        const skewOffsetY = (s - 1) * rowSkewY;
1124                        const finalX = seatX + skewOffsetX;
1125                        const finalY = rowY + skewOffsetY;
1126                        const seat = createSeatElement(seatId, finalX, finalY, block, seatSize, visualSeatLabel);  
1127                        group.appendChild(seat);
1128                        const sl = createSVGElement('text', { class: 'seat-label', 'text-anchor': 'middle', 'dominant-baseline': 'central', dy: '0.09em', transform: getTextTransform(finalX, finalY) });
1129                        sl.textContent = visualSeatLabel;
1130                        group.appendChild(sl);
1131                    } 
1132                } else { 
1133                    const absCurve = Math.abs(curve);
1134                    const theta = absCurve * (Math.PI / 180);
1135                    const radius = (blockWidth / 2) / Math.sin(theta / 2);
1136                    const startAngle = -theta / 2;
1137                    const angleStep = seatsPerRow > 1 ? theta / (seatsPerRow - 1) : 0;
1138                    for (let s = 1; s <= seatsPerRow; s++) {
1139                        const currentAngle = startAngle + (s - 1) * angleStep;
1140                        const seatX = radius * Math.sin(currentAngle);
1141                        const bulge = radius * (Math.cos(currentAngle) - Math.cos(theta / 2));
1142                        let seatY, seatRotation;
1143                        if (curve > 0) { seatY = rowY - bulge; seatRotation = currentAngle * (180 / Math.PI); } 
1144                        else { seatY = rowY + bulge; seatRotation = -currentAngle * (180 / Math.PI); }
1145                        const seatId = `${block.id}-R${visualRowLabel}-S${s}`;
1146                        if (block.killed_seats && block.killed_seats.includes(seatId)) continue;
1147                        let visualSeatLabel = startAt + (s - 1);
1148                        if (seatLabelStyle === 'alpha_upper') { visualSeatLabel = numberToAlpha(startAt + (s - 1)); }
1149                        const skewOffsetY = (s - 1) * rowSkewY;
1150                        const finalX = seatX + skewOffsetX;
1151                        const finalY = seatY + skewOffsetY;
1152                        const seat = createSeatElement(seatId, 0, 0, block, seatSize, visualSeatLabel);
1153                        seat.setAttribute('transform', `translate(${finalX}, ${finalY}) rotate(${seatRotation})`);
1154                        group.appendChild(seat);
1155                        const sl = createSVGElement('text', { class: 'seat-label', 'text-anchor': 'middle', 'dominant-baseline': 'central', dy: '0.09em', transform: `translate(${finalX}, ${finalY}) rotate(${seatRotation}) scale(${tsX}, ${tsY})` });
1156                        sl.textContent = visualSeatLabel;
1157                        group.appendChild(sl);
1158                    } 
1159                } 
1160            } 
1161        }
1162        function drawTableAndSeats(group, table) { 
1163            let tableShape, label; 
1164            const tableShapeAttrs = { 'data-table-id': table.id, class: 'table-shape' }; 
1165            if (table.color) tableShapeAttrs.style = `fill: ${table.color}`; 
1166            if (table.whole_table) group.classList.add('whole-table-booking'); 
1167            const seatLabelStyle = table.seat_label_style || 'numeric'; 
1168            const startAt = table.seat_start_at || 1; 
1169
1170            let parentFlipX = 1; let parentFlipY = 1;
1171            if (table.parent_id) {
1172                const parent = config.layout.elements.find(el => el.id === table.parent_id);
1173                if (parent) { if (parent.flipX) parentFlipX = -1; if (parent.flipY) parentFlipY = -1; }
1174            }
1175            const tsX = parentFlipX * (table.flipX ? -1 : 1);
1176            const tsY = parentFlipY * (table.flipY ? -1 : 1);
1177            const getTextTransform = (x, y) => `translate(${x}, ${y}) scale(${tsX}, ${tsY})`;
1178
1179            if (table.shape === 'circle') { 
1180                const radius = table.radius || 60; 
1181                tableShape = createSVGElement('circle', { cx: 0, cy: 0, r: radius, ...tableShapeAttrs }); 
1182            } else { 
1183                const tableWidth = table.width || 180, tableHeight = table.height || 70; 
1184                tableShape = createSVGElement('rect', { x: -tableWidth / 2, y: -tableHeight / 2, width: tableWidth, height: tableHeight, rx: 5, ...tableShapeAttrs }); 
1185            } 
1186            const labelAttrs = { class: 'table-label', 'pointer-events': 'none', 'text-anchor': 'middle', 'dominant-baseline': 'central' }; 
1187            if (table.textColor) labelAttrs.fill = table.textColor; 
1188            if (table.fontSize) labelAttrs['font-size'] = `${table.fontSize}px`; 
1189            label = createSVGElement('text', { ...labelAttrs, transform: getTextTransform(0,0) }); 
1190            label.textContent = table.label; 
1191            group.appendChild(tableShape); 
1192            group.appendChild(label); 
1193            
1194            if (table.whole_table) { 
1195                const firstSeatId = `${table.id}-S1`; 
1196                const isManuallyBooked = table.status === 'booked'; 
1197                const isPermanentlyBooked = config.layout.bookings && config.layout.bookings[firstSeatId]; 
1198                const isSoftLocked = softLockedSeats.has(firstSeatId); 
1199                if (isManuallyBooked || isPermanentlyBooked || isSoftLocked) { 
1200                    group.classList.add('booked'); 
1201                    tableShape.classList.add('booked'); 
1202                    if (isPermanentlyBooked && config.customer_names && config.customer_names[isPermanentlyBooked]) {
1203                        const custName = config.customer_names[isPermanentlyBooked];
1204                        const nameLabelAttrs = { class: 'table-customer-name', 'pointer-events': 'none', 'text-anchor': 'middle', 'dominant-baseline': 'central', fill: table.textColor || '#888', 'font-size': (parseInt(table.fontSize)||14) * 0.8 + 'px' }; 
1205                        const nameLabel = createSVGElement('text', { ...nameLabelAttrs, transform: getTextTransform(0, 15) }); 
1206                        nameLabel.textContent = custName;
1207                        group.appendChild(nameLabel);
1208                        label.setAttribute('transform', getTextTransform(0, -10));
1209                    }
1210                } 
1211                else { tableShape.classList.add('available'); } 
1212            } else { 
1213                if (table.shape === 'circle') { 
1214                    const radius = table.radius || 60; 
1215                    for (let i = 1; i <= table.seats; i++) { 
1216                        const angle = ((i - 1) / table.seats) * 2 * Math.PI; 
1217                        const orbitRadius = radius + 10; 
1218                        const seatId = `${table.id}-S${i}`; 
1219                        if (table.killed_seats && table.killed_seats.includes(seatId)) continue; 
1220                        let visualLabel = startAt + (i - 1); 
1221                        if (seatLabelStyle === 'alpha_upper') { visualLabel = numberToAlpha(startAt + (i - 1)); } 
1222                        const sx = orbitRadius * Math.cos(angle);
1223                        const sy = orbitRadius * Math.sin(angle);
1224                        group.appendChild(createSeatElement(seatId, sx, sy, table, 8, visualLabel)); 
1225                        const sl = createSVGElement('text', { class: 'seat-label', 'text-anchor': 'middle', 'dominant-baseline': 'central', dy: '0.05em', transform: getTextTransform(sx, sy) });
1226                        sl.textContent = visualLabel;
1227                        group.appendChild(sl);
1228                    } 
1229                } else { 
1230                    const tableWidth = table.width || 180, tableHeight = table.height || 70; 
1231                    const arrangement = table.seat_arrangement || 'top_bottom';
1232                    const padding = 10;
1233                    const seats = table.seats || 0;
1234                    let asTop = 0, asBottom = 0, asLeft = 0, asRight = 0;
1235                    if (arrangement === 'custom') {
1236                        asTop    = table.seats_top    || 0;
1237                        asBottom = table.seats_bottom || 0;
1238                        asLeft   = table.seats_left   || 0;
1239                        asRight  = table.seats_right  || 0;
1240                    } else if (arrangement === 'all_sides') {
1241                        const base = Math.floor(seats / 4); const rem = seats % 4;
1242                        asTop = base + (rem >= 1 ? 1 : 0); asBottom = base + (rem >= 2 ? 1 : 0); asRight = base + (rem >= 3 ? 1 : 0); asLeft = base;
1243                    } else if (arrangement === 'top_left_right' || arrangement === 'three_sides') {
1244                        const base = Math.floor(seats / 3); const rem = seats % 3;
1245                        asTop = base + (rem >= 1 ? 1 : 0); asRight = base + (rem >= 2 ? 1 : 0); asLeft = base; asBottom = 0;
1246                    } else if (arrangement === 'bottom_left_right') {
1247                        const base = Math.floor(seats / 3); const rem = seats % 3;
1248                        asBottom = base + (rem >= 1 ? 1 : 0); asRight = base + (rem >= 2 ? 1 : 0); asLeft = base; asTop = 0;
1249                    } else if (arrangement === 'top_bottom_left') {
1250                        const base = Math.floor(seats / 3); const rem = seats % 3;
1251                        asTop = base + (rem >= 1 ? 1 : 0); asBottom = base + (rem >= 2 ? 1 : 0); asLeft = base; asRight = 0;
1252                    } else if (arrangement === 'top_bottom_right') {
1253                        const base = Math.floor(seats / 3); const rem = seats % 3;
1254                        asTop = base + (rem >= 1 ? 1 : 0); asBottom = base + (rem >= 2 ? 1 : 0); asRight = base; asLeft = 0;
1255                    }
1256                    const isDistributedAround = ['all_sides', 'top_left_right', 'three_sides', 'bottom_left_right', 'top_bottom_left', 'top_bottom_right', 'custom'].includes(arrangement);
1257                    for (let i = 1; i <= seats; i++) { 
1258                        let x, y; const idx = i - 1; 
1259                        if (isDistributedAround) {
1260                            if (idx < asTop) { const spacing = tableWidth / (asTop + 1); x = -tableWidth / 2 + (idx + 1) * spacing; y = -tableHeight / 2 - padding; } 
1261                            else if (idx < (asTop + asRight)) { const sideIdx = idx - asTop; const spacing = tableHeight / (asRight + 1); x = tableWidth / 2 + padding; y = -tableHeight / 2 + (sideIdx + 1) * spacing; } 
1262                            else if (idx < (asTop + asRight + asBottom)) { const sideIdx = idx - (asTop + asRight); const spacing = tableWidth / (asBottom + 1); x = -tableWidth / 2 + (sideIdx + 1) * spacing; y = tableHeight / 2 + padding; } 
1263                            else { const sideIdx = idx - (asTop + asRight + asBottom); const spacing = tableHeight / (asLeft + 1); x = -tableWidth / 2 - padding; y = -tableHeight / 2 + (sideIdx + 1) * spacing; }
1264                        }
1265                        else if (arrangement === 'top') { const spacing = tableWidth / (seats + 1); x = -tableWidth / 2 + (i) * spacing; y = -tableHeight / 2 - padding; } 
1266                        else if (arrangement === 'bottom') { const spacing = tableWidth / (seats + 1); x = -tableWidth / 2 + (i) * spacing; y = tableHeight / 2 + padding; } 
1267                        else if (arrangement === 'left') { const spacing = tableHeight / (seats + 1); x = -tableWidth / 2 - padding; y = -tableHeight / 2 + (i) * spacing; } 
1268                        else if (arrangement === 'right') { const spacing = tableHeight / (seats + 1); x = tableWidth / 2 + padding; y = -tableHeight / 2 + (i) * spacing; } 
1269                        else if (arrangement === 'left_right') {
1270                            const seatsPerSide = Math.ceil(seats / 2); const spacing = tableHeight / (seatsPerSide + 1);
1271                            if (idx < seatsPerSide) { x = -tableWidth / 2 - padding; y = -tableHeight / 2 + (idx + 1) * spacing; } 
1272                            else { const sideIdx = idx - seatsPerSide; x = tableWidth / 2 + padding; y = -tableHeight / 2 + (sideIdx + 1) * spacing; }
1273                        } 
1274                        else { 
1275                            const seatsPerSide = Math.ceil(seats / 2); const spacing = tableWidth / (seatsPerSide + 1); 
1276                            if (idx < seatsPerSide) { x = -tableWidth / 2 + (idx + 1) * spacing; y = -tableHeight / 2 - padding; } 
1277                            else { const sideIdx = idx - seatsPerSide; x = -tableWidth / 2 + (sideIdx + 1) * spacing; y = tableHeight / 2 + padding; } 
1278                        }
1279                        const seatId = `${table.id}-S${i}`; 
1280                        if (table.killed_seats && table.killed_seats.includes(seatId)) continue; 
1281                        let visualLabel = startAt + (i - 1); 
1282                        if (seatLabelStyle === 'alpha_upper') { visualLabel = numberToAlpha(startAt + (i - 1)); } 
1283                        group.appendChild(createSeatElement(seatId, x, y, table, 8, visualLabel)); 
1284                        const sl = createSVGElement('text', { class: 'seat-label', 'text-anchor': 'middle', 'dominant-baseline': 'central', dy: '0.09em', transform: getTextTransform(x, y) });
1285                        sl.textContent = visualLabel;
1286                        group.appendChild(sl);
1287                    } 
1288                } 
1289            } 
1290        }
1291        function drawGAArea(group, area) { const sold = gaBookings[area.id] || 0; const remaining = area.capacity - sold; const isSoldOut = remaining <= 0; const isManuallyBooked = area.status === 'booked'; const shapeClass = (isSoldOut || isManuallyBooked) ? 'booked' : 'available'; const shapeAttrs = { 'data-area-id': area.id, class: 'ga-area-shape ' + shapeClass, style: area.color ? `fill: ${area.color}` : 'fill: transparent;' }; const labelAttrs = { class: 'ga-area-label', 'pointer-events': 'none', 'text-anchor': 'middle', 'dominant-baseline': 'middle' }; if (area.textColor) labelAttrs.fill = area.textColor; const label = createSVGElement('text', labelAttrs); const textLine1 = createSVGElement('tspan', { x: 0, dy: '-0.5em' }); textLine1.textContent = area.label; 
1292        const textLine2 = createSVGElement('tspan', { x: 0, dy: '1.2em', 'font-size': '12px', class: 'ga-capacity-text' }); 
1293        if (isManuallyBooked) { textLine2.textContent = i18n.unavailable || 'Unavailable'; } 
1294        else if (isSoldOut) { textLine2.textContent = i18n.soldOut || 'Sold Out'; } 
1295        else { textLine2.textContent = (i18n.gaAvailable || '%1$d / %2$d Available').replace('%1$d', remaining).replace('%2$d', area.capacity); } 
1296        label.appendChild(textLine1); label.appendChild(textLine2); 
1297        if (area.shape === 'rect') { const width = area.width || 200, height = area.height || 100; group.appendChild(createSVGElement('rect', { x: -width/2, y: -height/2, width: width, height: height, ...shapeAttrs })); } 
1298        else { const pointsString = area.points.map(p => `${p.x},${p.y}`).join(' '); group.appendChild(createSVGElement('polygon', { points: pointsString, ...shapeAttrs })); const centroid = calculatePolygonCentroid(area.points); label.setAttribute('x', centroid.x); label.setAttribute('y', centroid.y); } group.appendChild(label); }
1299        function drawStage(group, stage) { 
1300            const stageWidth = stage.width || 200, stageHeight = stage.height || 40; 
1301            const shapeAttrs = { class: 'stage-shape', style: stage.color ? `fill: ${stage.color}` : 'fill: #f0f0f1;' }; 
1302            let parentFlipX = 1; let parentFlipY = 1;
1303            if (stage.parent_id) {
1304               const parent = config.layout.elements.find(el => el.id === stage.parent_id);
1305               if (parent) { if (parent.flipX) parentFlipX = -1; if (parent.flipY) parentFlipY = -1; }
1306            }
1307            const tsX = parentFlipX * (stage.flipX ? -1 : 1);
1308            const tsY = parentFlipY * (stage.flipY ? -1 : 1);
1309            const getTextTransform = (x, y) => `translate(${x}, ${y}) scale(${tsX}, ${tsY})`;
1310            group.appendChild(createSVGElement('rect', { x: -stageWidth/2, y: -stageHeight/2, width: stageWidth, height: stageHeight, ...shapeAttrs })); 
1311            const labelAttrs = { class: 'stage-label', 'pointer-events': 'none', 'text-anchor': 'middle', 'dominant-baseline': 'middle' }; 
1312            if (stage.textColor) labelAttrs.fill = stage.textColor;
1313            if (stage.fontSize) { labelAttrs['font-size'] = `${stage.fontSize}px`; } 
1314            const label = createSVGElement('text', { ...labelAttrs, transform: getTextTransform(0,0) }); 
1315            label.textContent = stage.label; 
1316            group.appendChild(label); 
1317        }
1318
1319        function drawImageElement(group, element) { if (!element.image_url) return; const imageWidth = element.width || 80, imageHeight = element.height || 80; group.appendChild(createSVGElement('image', { href: element.image_url, x: -imageWidth/2, y: -imageHeight/2, width: imageWidth, height: imageHeight, 'pointer-events': 'none' })); }
1320        function drawPolygonShape(group, element) { const pointsString = element.points.map(p => `${p.x},${p.y}`).join(' '); const shapeAttrs = { points: pointsString, class: 'polygon-shape', style: element.color ? `fill: ${element.color}` : 'fill: transparent;' };
1320 group.appendChild(createSVGElement('polygon', shapeAttrs)); const labelAttrs = { class: 'stage-label', 'pointer-events': 'none' }; if (element.textColor) labelAttrs.fill = element.textColor; const label = createSVGElement('text', labelAttrs); label.textContent = element.label; const centroid = calculatePolygonCentroid(element.points); label.setAttribute('x', centroid.x); label.setAttribute('y', centroid.y); group.appendChild(label); }
1321        
1322        function isElementBookable(element) { const isVariableProduct = config.variation_prices && Object.keys(config.variation_prices).length > 0; if (isVariableProduct && !element.variation_id) { return false; } return true; }
1323
1324        function attachEventListeners() {
1325            $mapWrapper.on('click', '.section-boundary-guide', handleSectionClick);
1326            $mapWrapper.on('click', '.seat.available', handleSeatClick);
1327            $mapWrapper.on('click', '.table-shape.available', handleTableClick);
1328            $mapWrapper.on('click', '.ga-area-shape.available', handleGAAreaClick);
1329            
1330            // Mouse events
1331            $mapWrapper.on('mouseenter', '.seat, .table-shape, .ga-area-shape, .section-boundary-guide', handleTooltipShow);
1332            $mapWrapper.on('mouseleave', '.seat, .table-shape, .ga-area-shape, .section-boundary-guide', handleTooltipHide);
1333            $mapWrapper.on('mousemove', handleTooltipMove);
1334            
1335            // Buttons
1336            $('.les-view-controls').on('click', '#les-zoom-back-btn', handleZoomReset);
1337            $('#les-zoom-in-btn').on('click', function() { applyZoom(1.25); updateVisibleSectors(); });
1338            $('#les-zoom-out-btn').on('click', function() { applyZoom(0.8); updateVisibleSectors(); });
1339            $('#les-zoom-reset-btn').on('click', handleZoomReset);
1340            $addToCartBtn.on('click', handleAddToCart);
1341            
1342            // Inputs/Remove
1343            $summaryList.on('input', '.ga-quantity-input', handleGAQuantityChange);
1344            $summaryList.on('click', '.les-remove-item-btn', function(e) {
1345                e.preventDefault(); e.stopPropagation(); 
1346                const id = $(this).data('id'); const type = $(this).data('type');
1347                removeSelectedItem(type, id); updateSummaryAndValidation();
1348                if (type === 'ga_area') { $svg.find(`.ga-area-shape[data-area-id="${id}"]`).removeClass('selected'); } 
1349                else if (type === 'table') { $svg.find(`.table-shape[data-table-id="${id}"]`).removeClass('selected'); } 
1350                else { $svg.find(`.seat[data-seat-id="${id}"]`).removeClass('selected'); }
1351            });
1352
1353            const svgEl = document.getElementById('seating-map-svg');
1354            if (svgEl) {
1355                // Desktop Mouse
1356                $svg.on('mousedown', handlePanStart);
1357                $svg.on('mousemove', function(e) { if (view.isPanning) handlePanMove(e); });
1358                $svg.on('mouseup mouseleave', function(e) { if (view.isPanning) handlePanEnd(e); });
1359                $svg.on('wheel', function(e) { e.preventDefault(); const direction = e.originalEvent.deltaY > 0 ? 0.9 : 1.1; applyZoom(direction); requestAnimationFrame(debouncedUpdateSectors); });
1360
1361                // Mobile Touch (Native)
1362                svgEl.addEventListener('touchstart', handlePanStart, { passive: false });
1363                svgEl.addEventListener('touchmove', handlePanMove, { passive: false });
1364                svgEl.addEventListener('touchend', handlePanEnd, { passive: false });
1365                svgEl.addEventListener('touchcancel', handlePanEnd, { passive: false });
1366            }
1367
1368            // --- GROUP BOOKING INTEGRATION: Listeners ---
1369            
1370            // 1. Auto-select incoming group seats
1371            $(document).on('les_auto_select_seats', function(e, eventId, seatsArray) {
1372                if (String(config.event_id) !== String(eventId)) return;
1373
1374                // Clear current selection to ensure accurate group numbers
1375                selectedItems = [];
1376
1377                seatsArray.forEach(function(seatId) {
1378                    const parts = seatId.split('-');
1379                    const parentElementId = parts[0];
1380                    const parentElement = config.layout.elements.find(el => el.id === parentElementId);
1381
1382                    if (parentElement && isElementBookable(parentElement)) {
1383                        addSelectedItem('seat', seatId, parentElement.variation_id || '', 
13831);
1384                    }
1385                });
1386
1387                updateSummaryAndValidation();
1388            });
1389
1390            // 2. Clear auto-selection
1391            $(document).on('les_clear_selection', function(e, eventId) {
1392                if (String(config.event_id) !== String(eventId)) return;
1393                selectedItems = [];
1394                updateSummaryAndValidation();
1395            });
1396
1397            // --- Custom Quantity Plus/Minus Button Logic ---
1398            $(document).on('click', '.les-qty-btn', function(e) {
1399                e.preventDefault();
1400                const $btn = $(this);
1401                const isPlus = $btn.hasClass('plus');
1402                const $wrapper = $btn.closest('.les-qty-wrapper');
1403                const $input = $wrapper.find('input[type="number"]');
1404                
1405                if (!$input.length) return;
1406                
1407                let val = parseInt($input.val(), 10) || 0;
1408                const min = parseInt($input.attr('min'), 10) || 0;
1409                const maxAttr = $input.attr('max');
1410                const max = (maxAttr !== undefined && maxAttr !== false) ? parseInt(maxAttr, 10) : 9999;
1411                
1412                if (isPlus && val < max) {
1413                    $input.val(val + 1).trigger('input'); // Update value and trigger change
1414                } else if (!isPlus && val > min) {
1415                    $input.val(val - 1).trigger('input'); // Update value and trigger change
1416                }
1417            });
1418            // --- End Custom Quantity Logic ---
1419        }
1420
1421        function handleTooltipShow(e) { clearTimeout(tooltipHideTimeout);const $target = $(e.currentTarget); let htmlContent = ''; if ($target.hasClass('section-boundary-guide') && $svg.hasClass('zoom-level-overview')) { htmlContent = generateSectionTooltipContent($target.data('section-id')); } else { const seatId = $target.data('seat-id'); const tableId = $target.data('table-id'); const areaId = $target.data('area-id'); if (seatId) { htmlContent = generateSeatTooltipContent($target, seatId); } else if (tableId) { htmlContent = generateTableTooltipContent($target, tableId); } else if (areaId) { htmlContent = generateAreaTooltipContent($target, areaId); } } if (htmlContent) { $tooltip.html(htmlContent).show(); setTimeout(() => { $tooltip.css({ 'opacity': '1', 'transform': 'translateY(0)' }); }, 10); } }
1422
1423        function generateSeatTooltipContent($target, seatId) { const parentElement = config.layout.elements.find(el => el.id === $target.data('parent-id')); if (!parentElement) return ''; const isSelected = selectedItems.some(item => item.id === seatId); let statusText = getStatusText($target); let html = `<div class="les-tooltip-content"><div class="tooltip-title">${getPrettySeatName(seatId)}</div><div class="tooltip-row"><span class="tooltip-label">${i18n.seatPrice || 'Price'}:</span><span class="tooltip-value">${formatPrice(getElementPrice(parentElement))}</span></div><div class="tooltip-row"><span class="tooltip-label">${i18n.statusLabel || 'Status:'}</span><span class="tooltip-value">${statusText}</span></div>`;
1424            // Accessibility badge in tooltip
1425            const acc = config.accessibility || {};
1426            if (acc.enabled) {
1427                const accessEntry = (parentElement.accessible_seats || []).find(a => a.id === seatId);
1428                if (accessEntry) {
1429                    const badgeText = (accessEntry.type === 'companion')
1430                        ? (acc.companionLabel || 'Companion Seat')
1431                        : (acc.wheelchairLabel || 'Wheelchair Accessible');
1432                    const badgeIcon = (accessEntry.type === 'companion') ? '👤' : '♿';
1433                    html += `<div class="tooltip-row" style="margin-top:4px;"><span style="background:#1a73e8;color:#fff;border-radius:4px;padding:2px 6px;font-size:11px;font-weight:600;">${badgeIcon} ${badgeText}</span></div>`;
1434                }
1435            }
1436            if (isSelected) { html += `<div class="tooltip-status">${i18n.selected || 'Selected'}</div>`; } html += `</div>`; return html; }
1437        function generateTableTooltipContent($target, tableId) { const element = config.layout.elements.find(el => el.id === tableId); if (!element || !element.whole_table) return '';
1437 const isSelected = selectedItems.some(item => item.id === tableId); const totalPrice = getElementPrice(element) * element.seats; const $shape = $target.hasClass('table-shape') ? $target : $target.find('.table-shape');
1438let statusText = getStatusText($shape);
1439// Double safety: if class-based check still misses, read directly from element.status
1440if (!statusText || statusText === 'N/A') {
1441    statusText = (element.status === 'booked') ? (i18n.statusBooked || 'Booked') : (i18n.statusAvailable || 'Available');
1442} let html = `<div class="les-tooltip-content"><div class="tooltip-title">${element.label}</div><div class="tooltip-row"><span class="tooltip-label">${i18n.tablePrice || 'Total Price'}:</span><span class="tooltip-value">${formatPrice(totalPrice)}</span></div><div class="tooltip-row"><span class="tooltip-label">Seats:</span><span class="tooltip-value">${element.seats}</span></div><div class="tooltip-row"><span class="tooltip-label">${i18n.statusLabel || 'Status:'}</span><span class="tooltip-value">${statusText}</span></div>`; 
1443const firstSeatId = `${element.id}-S1`; 
1444const isPermanentlyBooked = config.layout.bookings && config.layout.bookings[firstSeatId];
1445if (isPermanentlyBooked && config.customer_names && config.customer_names[isPermanentlyBooked]) { html += `<div class="tooltip-row"><span class="tooltip-label">Booked By:</span><span class="tooltip-value" style="font-weight:bold;">${config.customer_names[isPermanentlyBooked]}</span></div>`; }
1446if (isSelected) { html += `<div class="tooltip-status">${i18n.selected || 'Selected'}</div>`; } html += `</div>`; return html; }
1447        function generateAreaTooltipContent($target, areaId) { const element = config.layout.elements.find(el => el.id === areaId); if (!element) return ''; const isSelected = selectedItems.some(item => item.id === areaId); let statusText = getStatusText($target); let html = `<div class="les-tooltip-content"><div class="tooltip-title">${element.label}</div><div class="tooltip-row"><span class="tooltip-label">${i18n.seatPrice || 'Price'}:</span><span class="tooltip-value">${formatPrice(getElementPrice(element))} ${i18n.perPerson || ''}</span></div><div class="tooltip-row"><span class="tooltip-label">${i18n.statusLabel || 'Status:'}</span><span class="tooltip-value">${statusText}</span></div>`; if (isSelected) { html += `<div class="tooltip-status">${i18n.selected || 'Selected'}</div>`; } html += `</div>`; return html; }
1448
1449        function generateSectionTooltipContent(sectionId) { 
1450            const section = config.layout.elements.find(el => el.id === sectionId); 
1451            if (!section) return ''; 
1452            let availableCount = 0; let totalCount = 0; 
1453            const childElements = config.layout.elements.filter(el => el.parent_id === sectionId); 
1454            childElements.forEach(el => { 
1455                if (el.type === 'ga_area') {
1456                    const capacity = parseInt(el.capacity || 0);
1457                    if (el.status !== 'booked') { totalCount += capacity; const sold = gaBookings[el.id] || 0; availableCount += Math.max(0, capacity - sold); }
1458                    return;
1459                }
1460                if (el.type === 'seat_block' || el.type === 'table') {
1461                    if (el.status === 'booked') {
1462                        let rawTotal = 0;
1463                        if (el.type === 'table') rawTotal = parseInt(el.seats || 0); else rawTotal = parseInt(el.rows || 0) * parseInt(el.seats_per_row || 0);
1464                        const killed = (el.killed_seats || []).length;
1465                        totalCount += Math.max(0, rawTotal - killed);
1466                        return;
1467                    }
1468                    if (el.type === 'table') {
1469                        if (el.whole_table) {
1470                            const killed = (el.killed_seats || []).length;
1471                            const netSeats = parseInt(el.seats || 0) - killed;
1472                            totalCount += netSeats;
1473                            const firstSeatId = `${el.id}-S1`;
1474                            const isBooked = config.layout.bookings[firstSeatId] || softLockedSeats.has(firstSeatId) || (el.unavailable_seats && el.unavailable_seats.includes(firstSeatId));
1475                            if (!isBooked) availableCount += netSeats;
1476                        } else {
1477                            const startAt = parseInt(el.seat_start_at || 1);
1478                            for(let i=0; i < (el.seats || 0); i++) {
1479                                const seatIndex = i + 1; const seatId = `${el.id}-S${seatIndex}`;
1480                                if (el.killed_seats && el.killed_seats.includes(seatId)) continue;
1481                                totalCount++;
1482                                const isBooked = config.layout.bookings[seatId] || softLockedSeats.has(seatId) || (el.unavailable_seats && el.unavailable_seats.includes(seatId));
1483                                if (!isBooked) availableCount++;
1484                            }
1485                        }
1486                    } else if (el.type === 'seat_block') {
1487                        const rows = parseInt(el.rows || 0); const perRow = parseInt(el.seats_per_row || 0); const rowStartAt = parseInt(el.row_start_at || 1); const rowLabelPrefix = el.row_label_prefix || '';
1488                        for(let r=0; r < rows; r++) {
1489                            let currentRowVal = rowStartAt + r; let visualRowLabel = currentRowVal;
1490                            if (el.row_label_style === 'alpha_upper') { visualRowLabel = numberToAlpha(currentRowVal); }
1491                            visualRowLabel = rowLabelPrefix + visualRowLabel;
1492                            for(let s=1; s <= perRow; s++) {
1493                                const seatId = `${el.id}-R${visualRowLabel}-S${s}`;
1494                                if (el.killed_seats && el.killed_seats.includes(seatId)) continue;
1495                                totalCount++;
1496                                const isBooked = config.layout.bookings[seatId] || softLockedSeats.has(seatId) || (el.unavailable_seats && el.unavailable_seats.includes(seatId));
1497                                if (!isBooked) availableCount++;
1498                            }
1499                        }
1500                    }
1501                }
1502            }); 
1503            return `<div class="les-tooltip-content"><div class="tooltip-title">${section.label}</div><div class="tooltip-row"><span class="tooltip-label">${i18n.availableSeats.split(':')[0] || 'Availability'}:</span><span class="tooltip-value">${availableCount} / ${totalCount}</span></div></div>`; 
1504        }
1505
1506        function getStatusText($el) { if ($el.hasClass('booked')) return i18n.statusBooked || 'Booked'; if ($el.hasClass('in-progress')) return i18n.statusInCart || 'In Another Cart'; if ($el.hasClass('unavailable')) return i18n.statusUnavailable || 'Unavailable'; if ($el.hasClass('available')) return i18n.statusAvailable || 'Available'; return 'N/A'; }
1507        function handleTooltipHide() { $tooltip.css({ 'opacity': '0', 'transform': 'translateY(5px)' }); tooltipHideTimeout = setTimeout(() => { $tooltip.hide(); }, 200);  }
1508        function handleTooltipMove(e) { const parentOffset = $mapWrapper.offset(); if (!parentOffset) return; const relX = e.pageX - parentOffset.left, relY = e.pageY - parentOffset.top; const tooltipWidth = $tooltip.outerWidth(), tooltipHeight = $tooltip.outerHeight();
1508 const wrapperWidth = $mapWrapper.width(), wrapperHeight = $mapWrapper.height(); const offset = 15; let newLeft = relX + offset, newTop = relY + offset; if (newLeft + tooltipWidth > wrapperWidth) newLeft = relX - tooltipWidth - offset; if (newTop + tooltipHeight > wrapperHeight) newTop = relY - tooltipHeight - offset; if (newLeft < 0) newLeft = offset; if (newTop < 0) newTop = offset; $tooltip.css({ left: `${newLeft}px`, top: `${newTop}px` }); }
1509        function handleSeatClick(e) { const seat = $(e.currentTarget); const seatId = seat.data('seat-id'); if (!seatId) return; if (selectedItems.some(item => item.id === seatId)) { removeSelectedItem('seat', seatId); } else { const parentElement = config.layout.elements.find(el => el.id === seat.data('parent-id')); if (!parentElement || !isElementBookable(parentElement)) { $errorMessage.text(i18n.itemNotConfigured || 'This item is not available for booking.').stop(true, true).fadeIn().delay(3000).fadeOut(); return; }addSelectedItem('seat', seatId, parentElement ? parentElement.variation_id || '' : '', 1); } updateSummaryAndValidation(); handleTooltipShow(e); }
1510        function handleTableClick(e) { const tableId = $(e.currentTarget).data('table-id'); const element = config.layout.elements.find(el => el.id === tableId); if (!element) return;if (!isElementBookable(element)) { $errorMessage.text(i18n.itemNotConfigured || 'This item is not available for booking.').stop(true, true).fadeIn().delay(3000).fadeOut(); return; } if (element.whole_table) { if (selectedItems.some(item => item.id === tableId)) { removeSelectedItem('table', tableId); } else { addSelectedItem('table', tableId, element.variation_id || '', element.seats); } } else { const availableSeats = $svg.find(`.seat.available[data-table-id='${tableId}']:not(.selected)`); handleFullTableSelection(tableId, element, availableSeats); } updateSummaryAndValidation(); handleTooltipShow(e); }
1511        function handleGAAreaClick(e) { const areaId = $(e.currentTarget).data('area-id'); if (selectedItems.some(item => item.id === areaId)) { removeSelectedItem('ga_area', areaId); } else { const area = config.layout.elements.find(el => el.id === areaId); if (area.status === 'booked') { $errorMessage.text(i18n.itemNotConfigured || 'This area is currently unavailable.').stop(true, true).fadeIn().delay(2000).fadeOut(); return; } if (!isElementBookable(area)) { $errorMessage.text(i18n.itemNotConfigured || 'This item is not available for booking.').stop(true, true).fadeIn().delay(3000).fadeOut(); return; } if (area) { if ((area.capacity - (gaBookings[area.id] || 0)) > 0) { addSelectedItem('ga_area', areaId, area.variation_id || '', 1); } else { $errorMessage.text((i18n.areaSoldOut || '"%s" is sold out.').replace('%s', area.label)).fadeIn().delay(2000).fadeOut(); } } } updateSummaryAndValidation(); handleTooltipShow(e); }
1512
1513        function handleFullTableSelection(tableId, element, seatsToSelect) { const variationId = element ? element.variation_id || '' : ''; seatsToSelect.each(function() { addSelectedItem('seat', $(this).data('seat-id'), variationId, 1); }); }
1514        function handleGAQuantityChange(e) { const $input = $(e.currentTarget), areaId = $input.data('area-id'); let quantity = parseInt($input.val(), 10) || 0; const area = config.layout.elements.find(el => el.id === areaId); if (!area) return; const remaining = area.capacity - (gaBookings[area.id] || 0); if (quantity < 0) quantity = 0; if (quantity > remaining) { quantity = remaining; $input.val(remaining); } if (quantity === 0) removeSelectedItem('ga_area', areaId); else { const item = selectedItems.find(i => i.id === areaId); if (item) item.quantity = quantity; } updateSummaryAndValidation(); }
1515        function updateSummary() { let summaryHTML = '', currentTotal = 0; selectedItems.forEach(item => { currentTotal += getSelectedItemPrice(item); }); if (selectedItems.length === 0) { summaryHTML = `<li>${i18n.clickToBegin || 'Click a seat or area to begin.'}</li>`; } else { selectedItems.forEach(item => { const elementId = item.type === 'seat' ? item.id.split('-')[0] : item.id; const element = config.layout.elements.find(el => el.id === elementId); if (!element) return; const removeBtn = `<button type="button" class="les-remove-item-btn" data-id="${item.id}" data-type="${item.type}" title="Remove" aria-label="Remove seat">×</button>`; if (item.type === 'ga_area') { const remaining = element.capacity - (gaBookings[item.id] || 0); summaryHTML += `<li class="ga-summary-item">
1516    <span class="ga-info">${element.label} (${formatPrice(getElementPrice(element))})</span>
1517    <div class="les-qty-wrapper">
1518        <button type="button" class="les-qty-btn minus">-</button>
1519        <input type="number" class="ga-quantity-input" data-area-id="${item.id}" value="${item.quantity}" min="0" max="${remaining}" />
1520        <button type="button" class="les-qty-btn plus">+</button>
1521    </div>
1522    ${removeBtn}
1523</li>`; } else { summaryHTML += `<li>${getPrettyItemName(item)} - ${formatPrice(getSelectedItemPrice(item))}${removeBtn}</li>`; } }); } $summaryList.html(summaryHTML); $totalPrice.text(formatPrice(currentTotal)); updateMobileCartToggle(); }
1524        function updateSelectionVisuals() { $svg.find('.selected').removeClass('selected'); selectedItems.forEach(item => { let selector = ''; if (item.type === 'ga_area') selector = `.ga-area-shape[data-area-id="${item.id}"]`; else if (item.type === 'table') selector = `.table-shape[data-table-id="${item.id}"]`; else selector = `.seat[data-seat-id="${item.id}"]`; if (selector) $svg.find(selector).addClass('selected'); }); }
1525        function updateSummaryAndValidation() { updateSummary(); validateSelection(); updateSelectionVisuals(); }
1526        function createSeatElement(seatId, cx, cy, parentElement, radius = 10, visualLabel = '') { 
1527            const isManuallyBooked = parentElement.status === 'booked'; 
1528            const isPermanentlyBooked = config.layout.bookings[seatId]; 
1529            const isSoftLocked = softLockedSeats.has(seatId); 
1530            const isUnavailable = parentElement.unavailable_seats && parentElement.unavailable_seats.includes(seatId); 
1531            const isBooked = isManuallyBooked || isPermanentlyBooked || isSoftLocked || isUnavailable; 
1532            let seatClass = isBooked ? 'seat booked' : 'seat available'; 
1533
1534            // Accessibility overlay
1535            const accessEntry = (parentElement.accessible_seats || []).find(a => a.id === seatId);
1536            if (accessEntry) {
1537                seatClass += (accessEntry.type === 'companion') ? ' accessible-companion' : ' accessible-wheelchair';
1538            }
1539            
1540            const seatGroupAttrs = {};
1541            if (accessEntry) {
1542                seatGroupAttrs.class = (accessEntry.type === 'companion') ? 'seat-group-accessible-companion' : 'seat-group-accessible-wheelchair';
1543            }
1544            const seatGroup = createSVGElement('g', seatGroupAttrs); 
1545            const seatCircleAttrs = { 'data-seat-id': seatId, 'data-parent-id': parentElement.id, 'data-table-id': parentElement.id, cx: cx || 0, cy: cy || 0, r: radius, class: seatClass }; 
1546            
1547            if (accessEntry && !isBooked) {
1548                seatCircleAttrs.style = (accessEntry.type === 'companion') ? 'fill: #64b5f6' : 'fill: #1a73e8';
1549            } else if (parentElement.seatColor && !isBooked) { 
1550                seatCircleAttrs.style = `fill: ${parentElement.seatColor}`; 
1551            } 
1552            
1553            const seatCircle = createSVGElement('circle', seatCircleAttrs); 
1554            seatGroup.appendChild(seatCircle);
1555
1556            // ♿ icon as SVG text overlay on wheelchair seats (only when seat radius is large enough)
1557            if (accessEntry && accessEntry.type === 'wheelchair' && !isBooked && radius >= 7) {
1558                const iconText = createSVGElement('text', {
1559                    x: cx || 0, y: cy || 0,
1560                    'text-anchor': 'middle', 'dominant-baseline': 'central',
1561                    'font-size': Math.round(radius * 1.2),
1562                    'pointer-events': 'none',
1563                    style: 'fill:#fff; user-select:none;'
1564                });
1565                iconText.textContent = '♿';
1566                seatGroup.appendChild(iconText);
1567            }
1568            
1569            return seatGroup; 
1570        }
1571        function addSelectedItem(type, id, variationId, quantity) { if (!selectedItems.some(item => item.id === id)) selectedItems.push({ type, id, variation_id: variationId, quantity }); }
1572        function removeSelectedItem(type, id) { selectedItems = selectedItems.filter(item => item.id !== id); }
1573        function getSelectedSummaryLabel() {
1574            if (selectedItems.length === 0) return '';
1575            const tableItems = selectedItems.filter(item => item.type === 'table');
1576            const seatItems = selectedItems.filter(item => item.type === 'seat');
1577            if (tableItems.length === 1 && selectedItems.length === 1) {
1578                return tableItems[0].id;
1579            }
1580            if (seatItems.length === 1 && selectedItems.length === 1) {
1581                return getPrettySeatName(seatItems[0].id);
1582            }
1583            return `${selectedItems.length} ${i18n.bookButtonCount ? (i18n.bookButtonCount.replace('%d', selectedItems.length)) : 'seats'}`;
1584        }
1585        function openAddonsModal(addons) {
1586            currentAddons = addons || [];
1587            addonSelections = {};
1588            const $modal = $('#les-addons-modal');
1589            const $content = $modal.find('.les-addons-modal-content');
1590            $content.html(buildAddonsModalHtml());
1591            updateAddonsSubtotal();
1592            $modal.show();
1593            addonModalOpen = true;
1594            isBooking = false; // Allow proceeding from modal
1595            $addToCartBtn.text(i18n.bookButton || 'Book Selected Seats').prop('disabled', false); // Reset button
1596        }
1597        function closeAddonsModal() {
1598            $('#les-addons-modal').hide();
1599            addonModalOpen = false;
1600            currentAddons = [];
1601            addonSelections = {};
1602        }
1603        function buildAddonsModalHtml() {
1604            const selectedLabel = getSelectedSummaryLabel();
1605            let html = `<h2>${i18n.addonsTitle || 'Choose Add-ons'}</h2>`;
1606            html += `<div class="les-addons-summary">${i18n.selectedSummary || 'You selected:'} <strong>${selectedLabel}</strong></div>`;
1607            if (!currentAddons.length) {
1608                html += `<p>${i18n.noAddonsAvailable || 'No add-ons are available for this event.'}</p>`;
1609            } else {
1610                html += '<div class="les-addons-list">';
1611                currentAddons.forEach(addon => {
1612                    const qty = addon.type === 'per_seat' ? (addon.quantity || getTotalSelectedSeats()) : (addon.quantity || 1);
1613                    addonSelections[addon.product_id] = qty;
1614                    const labelText = addon.display_label || addon.name;
1615                    html += `
1616                        <div class="les-addon-item" data-product-id="${addon.product_id}">
1617                            <img src="${addon.image}" alt="${addon.name}" />
1618                            <div class="les-addon-main">
1619                                <p class="les-addon-name">${labelText}</p>
1620                                <p class="les-addon-desc">${addon.description || ''}</p>
1621                                <div class="les-addon-meta"><span>${addon.price_html || ''}</span><span>${addon.type === 'per_seat' ? (i18n.perSeatNote || 'Quantity will auto-suggest per seat.') : (i18n.perOrderNote || 'Flat quantity for the booking.')}</span></div>
1622                            </div>
1623                            <div class="les-addon-qty">
1624                                <label>${i18n.quantityLabel || 'Quantity'}</label>
1625                                <div class="les-qty-wrapper">
1626                                    <button type="button" class="les-qty-btn minus">-</button>
1627                                    <input type="number" min="0" value="${qty}" data-product-id="${addon.product_id}" class="les-addon-qty-input" />
1628                                    <button type="button" class="les-qty-btn plus">+</button>
1629                                </div>
1630                            </div>
1631                        </div>`;
1632                });
1633                html += '</div>';
1634            }
1635            html += `<div class="les-addons-subtotal"><span>${i18n.addonsSubtotal || 'Add-ons subtotal:'}</span><strong id="les-addons-subtotal">${formatPrice(0)}</strong></div>`;
1636            html += '<div class="les-addons-actions">';
1637            html += `<button type="button" class="button" id="les-skip-addons-btn">${i18n.skipAddonsButton || 'Skip Add-ons & Book'}</button>`;
1638            html += `<button type="button" class="button button-primary" id="les-confirm-addons-btn">${i18n.confirmAddonsButton || i18n.bookButton || 'Book Selected Seats'}</button>`;
1639            html += '</div>';
1640            html += `<p class="les-addons-note">${i18n.addonsModalNote || 'You can review your add-ons in the cart before checkout.'}</p>`;
1641            return html;
1642        }
1643        function updateAddonsSubtotal() {
1644            let subtotal = 0;
1645            currentAddons.forEach(addon => {
1646                const qty = addonSelections[addon.product_id] || 0;
1647                const price = parseFloat(addon.price) || 0;
1648                subtotal += price * qty;
1649            });
1650            $('#les-addons-subtotal').text(formatPrice(subtotal));
1651        }
1652        function fetchAddonsForEvent() {
1653            return $.ajax({
1654                url: config.ajax_url,
1655                type: 'POST',
1656                data: { action: 'les_get_addons', nonce: config.nonce, event_id: config.event_id }
1657            });
1658        }
1659        function handleBookingWithoutAddons() {
1660            closeAddonsModal();
1661            return handleAddToCartRequest({});
1662        }
1663        function handleBookingWithAddons(addons) {
1664            closeAddonsModal();
1665            return handleAddToCartRequest({ addons });
1666        }
1667        function handleConfirmAddons() {
1668            const selectedAddons = [];
1669            currentAddons.forEach(addon => {
1670                const qty = parseInt(addonSelections[addon.product_id], 10) || 0;
1671                if (qty > 0) {
1672                    selectedAddons.push({
1673                        product_id: addon.product_id,
1674                        quantity: qty,
1675                        type: addon.type,
1676                        display_label: addon.display_label || addon.name
1677                    });
1678                }
1679            });
1680            if (selectedAddons.length === 0) {
1681                return handleBookingWithoutAddons();
1682            }
1683            handleBookingWithAddons(selectedAddons);
1684        }
1685        function handleAddToCartRequest(extraData) {
1686            if (selectedItems.length === 0) return;
1687            if ( ! isBooking ) {
1688                isBooking = true;
1689                $addToCartBtn.text(i18n.booking || 'Booking...').prop('disabled', true);
1690            }
1691            const requestData = {
1692                action: 'les_add_to_cart',
1693                nonce: config.nonce,
1694                product_id: config.product_id,
1695                event_id: config.event_id,
1696                source_url: window.location.href,
1697                seats: JSON.stringify(selectedItems)
1698            };
1699            if (extraData.addons) {
1700                requestData.addons = JSON.stringify(extraData.addons);
1701            }
1702            $.ajax({
1703                url: config.ajax_url,
1704                type: 'POST',
1705                data: requestData,
1706                success: (response) => {
1707                    if (response.success) {
1708                        window.location.href = config.cart_url || config.wc_cart_url || window.location.origin;
1709                    } else {
1710                        $errorMessage.text(response.data.message || (i18n.unknownError || 'An unknown error occurred.')).fadeIn().delay(3000).fadeOut();
1711                        if (response.data.reload) {
1712                            setTimeout(() => window.location.reload(), 2500);
1713                        } else {
1714                            isBooking = false;
1715                            updateSummaryAndValidation();
1716                        }
1717                    }
1718                },
1719                error: () => {
1720                    $errorMessage.text(i18n.requestFailed || 'Request failed. Please try again.').fadeIn().delay(3000).fadeOut();
1721                    isBooking = false;
1722                    updateSummaryAndValidation();
1723                }
1724            });
1725        }
1726        function attachAddonModalEvents() {
1727            $(document).off('click.lesAddons', '#les-skip-addons-btn').on('click.lesAddons', '#les-skip-addons-btn', function() {
1728                handleBookingWithoutAddons();
1729            });
1730            $(document).off('click.lesConfirmAddons', '#les-confirm-addons-btn').on('click.lesConfirmAddons', '#les-confirm-addons-btn', function() {
1731                handleConfirmAddons();
1732            });
1733            $(document).off('click.lesCloseAddons', '.les-addons-modal-close, .les-addons-modal-backdrop').on('click.lesCloseAddons', '.les-addons-modal-close, .les-addons-modal-backdrop', function() {
1734                closeAddonsModal();
1735                isBooking = false;
1736                updateSummaryAndValidation();
1737            });
1738            $(document).off('input.lesAddonQty', '.les-addon-qty-input').on('input.lesAddonQty', '.les-addon-qty-input', function() {
1739                const productId = $(this).data('product-id');
1740                const qty = Math.max(0, parseInt($(this).val(), 10) || 0);
1741                addonSelections[productId] = qty;
1742                $(this).val(qty);
1743                updateAddonsSubtotal();
1744            });
1745        }
1746        attachAddonModalEvents();
1747        function handleAddToCart() { if (isBooking || selectedItems.length === 0) return; isBooking = true;
1747 $addToCartBtn.text(i18n.booking || 'Booking...').prop('disabled', true); fetchAddonsForEvent().done(function(response) { if (response.success && response.data && response.data.length > 0) { openAddonsModal(response.data); } else { handleAddToCartRequest({}); } }).fail(function() { handleAddToCartRequest({}); }); }
1748        function getElementPrice(element) {
1749            let price = config.product_price; 
1750            if (element.variation_id && config.variation_prices && config.variation_prices[element.variation_id]) {
1751                const varData = config.variation_prices[element.variation_id];
1752                if (typeof varData === 'object' && varData !== null && varData.price !== undefined) { price = varData.price; } 
1753                else { price = varData; }
1754            }
1755            return parseFloat(price || 0);
1756        }
1757        function getSelectedItemPrice(item) { return getElementPrice({ variation_id: item.variation_id }) * item.quantity; }
1758        function getPrettyItemName(item) { const elementId = item.type === 'seat' ? item.id.split('-')[0] : item.id; const element = config.layout.elements.find(el => el.id === elementId); const name = element ? element.label : ''; if (item.type === 'ga_area') return `${name} (x${item.quantity})`; if (item.type === 'table') return `${name} ${i18n.tableSuffix || '(Table)'}`; return getPrettySeatName(item.id); }
1759        function validateSelection() { const totalSelected = getTotalSelectedSeats(); const maxSeats = config.maxSeatsPerOrder || 0; let errorMessage = ''; if (maxSeats > 0 && totalSelected > maxSeats) { errorMessage = (i18n.maxTicketsError || 'You cannot select more than %d tickets.').replace('%d', maxSeats); } if (!errorMessage && !config.isOrphanRuleDisabled && totalSelected > 0) { if (checkForOrphanSeats()) { errorMessage = i18n.orphanSeatError || 'This selection would leave a single empty seat.'; } } if (errorMessage) { $errorMessage.text(errorMessage).show(); } else { $errorMessage.hide(); } const isInvalid = !!errorMessage; $addToCartBtn.text(totalSelected === 0 ? (i18n.bookButton || 'Book Selected Seats') : (i18n.bookButtonCount || 'Book %d Seats').replace('%d', totalSelected)).prop('disabled', isInvalid || totalSelected === 0); }
1760
1761
1762        function checkForOrphanSeats() { 
1763            const selectedSeatIds = selectedItems.filter(i => i.type === 'seat').map(i => i.id);
1764            const parentElementIds = new Set(selectedSeatIds.map(id => id.split('-')[0]));
1765            const unavailableSeatIds = new Set(selectedSeatIds);
1766            parentElementIds.forEach(parentId => {
1767                const bookedSeats = bookedSeatsByParent.get(parentId);
1768                if (bookedSeats) bookedSeats.forEach(seatId => unavailableSeatIds.add(seatId));
1769            });
1770            softLockedSeats.forEach(seatId => {
1771                if (parentElementIds.has(seatId.split('-')[0])) unavailableSeatIds.add(seatId);
1772            });
1773            const blockedSeatIds = new Set();
1774            const killedSeatIds = new Set(); // ← NEW: Track killed seats separately
1775
1776            const relevantElements = config.layout.elements.filter(el => parentElementIds.has(el.id));
1777
1778            for (const element of relevantElements) {
1779                // ← Track killed seats separately - they should be IGNORED in orphan logic
1780                if(element.killed_seats) element.killed_seats.forEach(sid => killedSeatIds.add(sid));
1781                
1782                // ← Only add NON-KILLED unavailable seats to blocked set
1783                if(element.unavailable_seats) {
1784                    element.unavailable_seats.forEach(sid => {
1785                        if (!killedSeatIds.has(sid)) {
1786                            blockedSeatIds.add(sid);
1787                        }
1788                    });
1789                }
1790                if(element.status === 'booked') { continue; }
1791
1792                let seatsToCheck = []; 
1793                if (element.type === 'seat_block') { 
1794                    for (let r = 1; r <= element.rows; r++) { 
1795                        seatsToCheck = []; 
1796                        let visualRowLabel = r;
1797                        if(element.row_label_style === 'alpha_upper') visualRowLabel = numberToAlpha(r);
1798                        visualRowLabel = (element.row_label_prefix || '') + visualRowLabel;
1799                        for (let s = 1; s <= element.seats_per_row; s++) { 
1800                            const seatId = `${element.id}-R${visualRowLabel}-S${s}`;
1801                            // ← Skip killed seats entirely - they don't exist for adjacency/orphan logic
1802                            if (!killedSeatIds.has(seatId)) {
1803                                seatsToCheck.push(seatId); 
1804                            }
1805                        } 
1806                        if (hasOrphanInArray(seatsToCheck, unavailableSeatIds, blockedSeatIds, false)) return true; 
1807                    } 
1808                } else if (element.type === 'table' && !element.whole_table) { 
1809                    for (let i = 1; i <= element.seats; i++) { 
1810                        const seatId = `${element.id}-S${i}`;
1811                        if (!killedSeatIds.has(seatId)) {
1812                            seatsToCheck.push(seatId); 
1813                        }
1814                    } 
1815                    const isCircle = element.shape === 'circle'; 
1816                    if (hasOrphanInArray(seatsToCheck, unavailableSeatIds, blockedSeatIds, isCircle)) return true; 
1817                } 
1818            } 
1819            return false; 
1820        }
1821
1822        function hasOrphanInArray(seatArray, unavailableSet, blockedSet, isCircular = false) { 
1823            const isFree = (id) => !unavailableSet.has(id) && !blockedSet.has(id);
1824            for (let i = 0; i < seatArray.length; i++) { 
1825                const currentSeatId = seatArray[i]; 
1826                if (isFree(currentSeatId)) {
1827                    let leftIdx = i - 1; let isLeftTaken = false;
1828                    if (leftIdx < 0) { if (isCircular) { leftIdx = seatArray.length - 1; isLeftTaken = !isFree(seatArray[leftIdx]); } else { isLeftTaken = true; } } else { isLeftTaken = !isFree(seatArray[leftIdx]); }
1829
1830                    let rightIdx = i + 1; let isRightTaken = false;
1831                    if (rightIdx >= seatArray.length) { if (isCircular) { rightIdx = 0; isRightTaken = !isFree(seatArray[rightIdx]); } else { isRightTaken = true; } } else { isRightTaken = !isFree(seatArray[rightIdx]); }
1832
1833                    if (isLeftTaken && isRightTaken) { return true; }
1834                }
1835            } 
1836            return false; 
1837        }
1838
1839        function getTotalSelectedSeats() { return selectedItems.reduce((total, item) => total + item.quantity, 0); }
1840        function getPrettySeatName(seatId) { if (!seatId) return ''; const elementId = seatId.split('-')[0]; const element = config.layout.elements.find(el => el.id === elementId); if (!element) return seatId; const elementName = element.label || elementId; let seatIdentifier = ''; const startAt = element.seat_start_at || 1; const seatLabelStyle = element.seat_label_style || 'numeric'; const rowMatch = seatId.match(/-R(.+)-S(\d+)$/); const tableMatch = seatId.match(/-S(\d+)$/); if (rowMatch) { const rowIdentifier = rowMatch[1]; const seatIndex = parseInt(rowMatch[2], 10); let visualSeatLabelValue = startAt + (seatIndex - 1); let visualSeatLabel = visualSeatLabelValue; if (seatLabelStyle === 'alpha_upper') { visualSeatLabel = numberToAlpha(visualSeatLabelValue); } seatIdentifier = (i18n.rowSeat || 'Row %1$s, Seat %2$s').replace('%1$s', rowIdentifier).replace('%2$s', visualSeatLabel); } else if (tableMatch) { const seatIndex = parseInt(tableMatch[1], 10); let visualLabelValue = startAt + (seatIndex - 1); let visualLabel = visualLabelValue; if (seatLabelStyle === 'alpha_upper') { visualLabel = numberToAlpha(visualLabelValue); } seatIdentifier = (i18n.seat || 'Seat %s').replace('%s', visualLabel); } else { seatIdentifier = seatId; } let sectionPrefix = ''; if (element.parent_id) { const section = config.layout.elements.find(el => el.id === element.parent_id); if (section && section.label) { sectionPrefix = section.label + ' › '; } } return `${sectionPrefix}${elementName} - ${seatIdentifier}`; }
1841        function createSVGElement(tag, attrs) { const el = document.createElementNS(svgNS, tag); for (let key in attrs) { if (tag === 'image' && (key === 'href' || key === 'xlink:href')) { el.setAttribute('href', attrs[key]); el.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', attrs[key]); } else { el.setAttribute(key, attrs[key]); } } return el; }
1842        function formatPrice(price) { return (config.currency_symbol || '$') + parseFloat(price).toFixed(config.price_decimals ?? 2); }
1843        function animateViewBox(x, y, w, h, duration, callback) { isAnimating = true; const startViewBox = { ...view }; const startTime = performance.now(); function frame(currentTime) { const progress = Math.min((currentTime - startTime) / duration, 1); const easedProgress = 0.5 - 0.5 * Math.cos(progress * Math.PI); view.x = startViewBox.x + (x - startViewBox.x) * easedProgress; view.y = startViewBox.y + (y - startViewBox.y) * easedProgress; view.w = startViewBox.w + (w - startViewBox.w) * easedProgress; view.h = startViewBox.h + (h - startViewBox.h) * easedProgress; $svg.attr('viewBox', `${view.x} ${view.y} ${view.w} ${view.h}`); if (progress < 1) { requestAnimationFrame(frame); } else { isAnimating = false; if (callback) { callback(); } } } requestAnimationFrame(frame); }
1844        function calculatePolygonCentroid(pts) { if(!pts || pts.length === 0) return {x:0, y:0}; let first = pts[0], last = pts[pts.length - 1]; if (first.x != last.x || first.y != last.y) pts.push(first); let twicearea = 0, x = 0, y = 0, nPts = pts.length, p1, p2, f;
1844 for (let i = 0, j = nPts - 1; i < nPts; j = i++) { p1 = pts[i]; p2 = pts[j]; f = p1.x * p2.y - p2.x * p1.y; twicearea += f; x += (p1.x + p2.x) * f; y += (p1.y + p2.y) * f; } f = twicearea * 3; if (Math.abs(f) < 1e-10) return {x:0, y:0}; return { x: x / f, y: y / f }; }
1845        function findDynamicViewBox(elements, padding = 50) { const defaults = { viewBox: `0 0 ${config.layout.settings.canvasWidth || 1000} ${config.layout.settings.canvasHeight || 800}`, x: 0, y: 0, width: config.layout.settings.canvasWidth || 1000, height: config.layout.settings.canvasHeight || 800 }; if (!elements || elements.length === 0) return defaults; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; elements.forEach(element => { const elX = parseFloat(element.x), elY = parseFloat(element.y); if (isNaN(elX) || isNaN(elY)) return; const buffer = Math.max(parseFloat(element.width) || 0, parseFloat(element.height) || 0, (parseFloat(element.radius) || 0) * 2) / 2 + 30; minX = Math.min(minX, elX - buffer); minY = Math.min(minY, elY - buffer); maxX = Math.max(maxX, elX + buffer); maxY = Math.max(maxY, elY + buffer); }); if (minX === Infinity) return defaults; minX -= padding; minY -= padding; maxX += padding; maxY += padding; const viewBoxWidth = maxX - minX, viewBoxHeight = maxY - minY; return { viewBox: `${minX} ${minY} ${viewBoxWidth} ${viewBoxHeight}`, x: minX, y: minY, width: viewBoxWidth, height: viewBoxHeight }; }
1846        function applyZoom(zoomFactor) {  $tooltip.hide().css('opacity', 0); const newWidth = view.w / zoomFactor; const newHeight = view.h / zoomFactor; view.x += (view.w - newWidth) / 2; view.y += (view.h - newHeight) / 2; view.w = newWidth; view.h = newHeight; $svg.attr('viewBox', `${view.x} ${view.y} ${view.w} ${view.h}`); }
1847        
1848        function handlePanStart(e) { 
1849            if (isAnimating) return; 
1850            $tooltip.hide().css('opacity', 0); 
1851            
1852            if ($(e.target).closest('.seat, .table-shape, .ga-area-shape').length > 0) return; 
1853            if ($svg.hasClass('zoom-level-overview') && $(e.target).hasClass('section-boundary-guide')) return; 
1854
1855            const type = e.type;
1856            const isTouch = type === 'touchstart';
1857
1858            if (isTouch && e.touches.length === 2) {
1859                view.isPanning = false; 
1860                view.isPinching = true;
1861                view.startPinchDist = getPinchDistance(e.touches);
1862                e.preventDefault(); 
1863                return;
1864            }
1865
1866            let clientX, clientY;
1867            if (isTouch) {
1868                clientX = e.touches[0].clientX;
1869                clientY = e.touches[0].clientY;
1870                if (e.cancelable) e.preventDefault();
1871            } else {
1872                e.preventDefault(); 
1873                clientX = e.clientX;
1874                clientY = e.clientY;
1875            }
1876
1877            view.isPanning = true; 
1878            view.panStart.x = clientX; 
1879            view.panStart.y = clientY; 
1880            $svg.addClass('is-panning'); 
1881        }
1882
1883        function handlePanEnd(e) { 
1884            if (view.isPanning) { 
1885                view.isPanning = false; 
1886                $svg.removeClass('is-panning'); 
1887            } 
1888            if (view.isPinching) {
1889                view.isPinching = false;
1890                view.startPinchDist = 0;
1891            }
1892        }
1893        
1894        function getElementBBox(el) { let bbox = {}; const buffer = 0; switch(el.type) { case 'section': if (el.shape === 'polygon' && el.points) { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; el.points.forEach(p => { minX = Math.min(minX, p.x); minY = Math.min(minY, p.y); maxX = Math.max(maxX, p.x); maxY = Math.max(maxY, p.y); }); bbox = { x: el.x + minX, y: el.y + minY, width: maxX - minX, height: maxY - minY }; } else { const w = el.width || 400; const h = el.height || 300; bbox = { x: el.x - w / 2, y: el.y - h / 2, width: w, height: h }; } break; default: const w = el.width || 200; const h = el.height || 100; bbox = { x: el.x - w / 2, y: el.y - h / 2, width: w, height: h }; } return bbox; }
1895
1896        function getSectionVisibilityBBox(section) {
1897            const bounds = getElementBBox(section);
1898            const children = config.layout.elements.filter(el => el.parent_id === section.id);
1899
1900            children.forEach(child => {
1901                const childBounds = getElementBBox(child);
1902                const childX = section.x + childBounds.x;
1903                const childY = section.y + childBounds.y;
1904                const childRight = childX + childBounds.width;
1905                const childBottom = childY + childBounds.height;
1906                const boundsRight = bounds.x + bounds.width;
1907                const boundsBottom = bounds.y + bounds.height;
1908
1909                bounds.x = Math.min(bounds.x, childX);
1910                bounds.y = Math.min(bounds.y, childY);
1911                bounds.width = Math.max(boundsRight, childRight) - bounds.x;
1912                bounds.height = Math.max(boundsBottom, childBottom) - bounds.y;
1913            });
1914
1915            return bounds;
1916        }
1917
1918        init();
1919    });
1920})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.