PluginProbe ʕ •ᴥ•ʔ
Event Tickets with Ticket Scanner / 3.1.2
Event Tickets with Ticket Scanner v3.1.2
3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.4 trunk 2.6.0 2.7.0 2.7.1 2.7.10 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.10 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 2.9.8 2.9.9 3.0.0 3.0.1 3.0.2 3.0.3
event-tickets-with-ticket-scanner / js / seating_admin.js
event-tickets-with-ticket-scanner / js Last commit date
congress-admin.js 1 week ago congress-frontend.js 1 week ago seating_admin.js 1 week ago seating_designer.js 1 week ago seating_frontend.js 1 week ago
seating_admin.js
1471 lines
1 /**
2 * Seating Admin JavaScript
3 *
4 * Handles admin UI for seating plan management.
5 *
6 * @package Event_Tickets_With_Ticket_Scanner
7 * @since 2.8.0
8 */
9 function sasoEventtickets_js_seating_admin(_myAjaxVar, _basicObj) {
10 const $ = jQuery;
11 const { __, _x, _n, sprintf } = wp.i18n;
12 let myAjax = _myAjaxVar;
13 let BASIC = _basicObj;
14 let adminDiv = null;
15
16 // Config
17 const config = {
18 i18n: {
19 confirmDelete: __('Are you sure you want to delete this?', 'event-tickets-with-ticket-scanner'),
20 confirmDeleteWithSeats: __('This plan has seats. Delete plan and all seats?', 'event-tickets-with-ticket-scanner'),
21 planCreated: __('Seating plan created successfully', 'event-tickets-with-ticket-scanner'),
22 planUpdated: __('Seating plan updated successfully', 'event-tickets-with-ticket-scanner'),
23 planDeleted: __('Seating plan deleted successfully', 'event-tickets-with-ticket-scanner'),
24 planCloned: __('Seating plan cloned successfully', 'event-tickets-with-ticket-scanner'),
25 seatCreated: __('Seat created successfully', 'event-tickets-with-ticket-scanner'),
26 seatsCreated: __('Seats created successfully', 'event-tickets-with-ticket-scanner'),
27 seatUpdated: __('Seat updated successfully', 'event-tickets-with-ticket-scanner'),
28 seatDeleted: __('Seat deleted successfully', 'event-tickets-with-ticket-scanner'),
29 limitReached: __('Limit reached. Upgrade to Premium for unlimited access.', 'event-tickets-with-ticket-scanner'),
30 error: __('An error occurred. Please try again.', 'event-tickets-with-ticket-scanner'),
31 loading: __('Loading...', 'event-tickets-with-ticket-scanner'),
32 noPlans: __('No seating plans found. Create your first plan!', 'event-tickets-with-ticket-scanner'),
33 noSeats: __('No seats in this plan. Add seats below.', 'event-tickets-with-ticket-scanner'),
34 layoutSimpleTitle: __('Simple Layout (Dropdown)', 'event-tickets-with-ticket-scanner'),
35 layoutSimpleDesc: __('Customers will see a dropdown menu to select their seat. Available seats are shown in a list sorted by identifier. This is ideal for smaller venues or when visual seat selection is not needed.', 'event-tickets-with-ticket-scanner'),
36 layoutVisualTitle: __('Visual Layout (Seat Map)', 'event-tickets-with-ticket-scanner'),
37 layoutVisualDesc: __('Customers will see an interactive seat map where they can click on available seats. Occupied seats are shown in a different color. This provides a visual overview of the venue. (Premium feature)', 'event-tickets-with-ticket-scanner'),
38 showLayout: __('Show Layout', 'event-tickets-with-ticket-scanner'),
39 hideLayout: __('Hide Layout', 'event-tickets-with-ticket-scanner'),
40 // Batch operations
41 batchConfirmDelete: __('Delete %d seats?', 'event-tickets-with-ticket-scanner'),
42 batchSeatsUpdated: __('%d seats updated', 'event-tickets-with-ticket-scanner'),
43 batchSeatsDeleted: __('%d seats deleted', 'event-tickets-with-ticket-scanner'),
44 batchWithErrors: __('%d seats processed, %d errors', 'event-tickets-with-ticket-scanner'),
45 selected: __('selected', 'event-tickets-with-ticket-scanner'),
46 batchActionSelect: __('-- Action --', 'event-tickets-with-ticket-scanner'),
47 batchActivate: __('Activate', 'event-tickets-with-ticket-scanner'),
48 batchDeactivate: __('Deactivate', 'event-tickets-with-ticket-scanner'),
49 batchDelete: __('Delete', 'event-tickets-with-ticket-scanner')
50 }
51 };
52
53 // State
54 let currentPlanId = null;
55 let currentPlanName = '';
56 let currentPlanLayoutType = 'simple';
57 let plansData = [];
58 let seatsData = [];
59 let plansDataTable = null;
60 let seatsDataTable = null;
61 let batchInProgress = false;
62
63 /**
64 * Make AJAX request via BASIC._makePost
65 */
66 function makeRequest(action, data, successCb, errorCb) {
67 data = data || {};
68 data.c = action;
69 BASIC._makePost('seating', data, successCb, errorCb);
70 }
71
72 function showNotice(message, type) {
73 type = type || 'success';
74 const $notice = $('<div class="saso-notice saso-notice-' + type + '">' + message + '</div>');
75 // Use .first() to prevent duplicate notices if multiple wrappers exist
76 $('.saso-seating-admin-wrap').first().prepend($notice);
77 setTimeout(function() {
78 $notice.fadeOut(300, function() { $(this).remove(); });
79 }, 3000);
80 }
81
82 function showLoading($container) {
83 $container.html('<div class="saso-loading">' + config.i18n.loading + '</div>');
84 }
85
86 // =========================================================================
87 // Plans Management
88 // =========================================================================
89
90 function loadPlans() {
91 const $list = $('.saso-seating-plans-list');
92 showLoading($list);
93
94 makeRequest('getPlans', {}, function(data) {
95 plansData = data.plans || [];
96 renderPlans();
97 updateAddPlanButton();
98 });
99 }
100
101 function renderPlans() {
102 const $list = $('.saso-seating-plans-list');
103 const tableId = 'saso-plans-datatable';
104
105 // Destroy existing DataTable
106 if (plansDataTable) {
107 plansDataTable.destroy();
108 plansDataTable = null;
109 }
110
111 // Create table structure
112 $list.html('<table id="' + tableId + '" class="display" style="width:100%">' +
113 '<thead><tr>' +
114 '<th>ID</th>' +
115 '<th>' + __('Name', 'event-tickets-with-ticket-scanner') + '</th>' +
116 '<th>' + __('Layout', 'event-tickets-with-ticket-scanner') + '</th>' +
117 '<th>' + __('Seats', 'event-tickets-with-ticket-scanner') + '</th>' +
118 '<th>' + __('Status', 'event-tickets-with-ticket-scanner') + '</th>' +
119 '<th>' + __('Actions', 'event-tickets-with-ticket-scanner') + '</th>' +
120 '</tr></thead></table>');
121
122 plansDataTable = $('#' + tableId).DataTable({
123 language: {
124 emptyTable: config.i18n.noPlans
125 },
126 responsive: true,
127 searching: true,
128 ordering: true,
129 processing: false,
130 serverSide: false,
131 stateSave: true,
132 data: plansData,
133 order: [[1, 'asc']],
134 columns: [
135 {
136 data: 'id',
137 orderable: true,
138 className: 'dt-center',
139 width: 50
140 },
141 {
142 data: 'name',
143 orderable: true,
144 render: (data) => '<strong>' + escapeHtml(data) + '</strong>'
145 },
146 {
147 data: 'layout_type',
148 orderable: true,
149 render: (data) => data === 'visual'
150 ? __('Visual (Seat Map)', 'event-tickets-with-ticket-scanner')
151 : __('Simple (Dropdown)', 'event-tickets-with-ticket-scanner')
152 },
153 {
154 data: 'seat_count',
155 orderable: true,
156 className: 'dt-center',
157 width: 60,
158 render: (data) => data || 0
159 },
160 {
161 data: 'aktiv',
162 orderable: true,
163 width: 80,
164 render: (data) => data == 1
165 ? '<span class="saso-status-active">' + __('Active', 'event-tickets-with-ticket-scanner') + '</span>'
166 : '<span class="saso-status-inactive">' + __('Inactive', 'event-tickets-with-ticket-scanner') + '</span>'
167 },
168 {
169 data: null,
170 orderable: false,
171 className: 'dt-right',
172 width: 250,
173 render: (data, type, row) => {
174 let buttons = '';
175 // View button only if image exists (check > 0)
176 if (row.meta?.image_id && parseInt(row.meta.image_id) > 0) {
177 buttons += '<button type="button" class="button button-small saso-view-plan-image" title="' + __('View Venue Photo', 'event-tickets-with-ticket-scanner') + '"><span class="dashicons dashicons-format-image"></span></button> ';
178 }
179 // Open Designer button for visual layout plans
180 if (row.layout_type === 'visual') {
181 buttons += '<button type="button" class="button button-small button-primary saso-open-designer" title="' + __('Open Designer', 'event-tickets-with-ticket-scanner') + '"><span class="dashicons dashicons-layout"></span></button> ';
182 }
183 buttons += '<button type="button" class="button button-small saso-edit-plan" title="' + __('Edit', 'event-tickets-with-ticket-scanner') + '"><span class="dashicons dashicons-edit"></span></button> ';
184 buttons += '<button type="button" class="button button-small saso-manage-seats" title="' + __('Manage Seats', 'event-tickets-with-ticket-scanner') + '"><span class="dashicons dashicons-grid-view"></span></button> ';
185 buttons += '<button type="button" class="button button-small saso-clone-plan" title="' + __('Clone', 'event-tickets-with-ticket-scanner') + '"><span class="dashicons dashicons-admin-page"></span></button> ';
186 buttons += '<button type="button" class="button button-small saso-delete-plan" title="' + __('Delete', 'event-tickets-with-ticket-scanner') + '"><span class="dashicons dashicons-trash"></span></button>';
187 return buttons;
188 }
189 }
190 ]
191 });
192
193 // Set table width to 100%
194 $('#' + tableId).css('width', '100%');
195
196 // Event handlers for DataTable buttons - use .off() first to prevent duplicates
197 const $tbody = $('#' + tableId + ' tbody');
198 $tbody.off('click.sasoPlanActions');
199
200 $tbody.on('click.sasoPlanActions', '.saso-view-plan-image', function() {
201 const row = plansDataTable.row($(this).closest('tr'));
202 const plan = row.data();
203 if (plan?.meta?.image_id) showPlanImageModal(plan.meta.image_id, plan.name);
204 });
205
206 $tbody.on('click.sasoPlanActions', '.saso-edit-plan', function() {
207 const row = plansDataTable.row($(this).closest('tr'));
208 const plan = row.data();
209 if (plan) openPlanModal(plan);
210 });
211
212 $tbody.on('click.sasoPlanActions', '.saso-manage-seats', function() {
213 const row = plansDataTable.row($(this).closest('tr'));
214 const plan = row.data();
215 if (plan) showSeatsView(plan);
216 });
217
218 $tbody.on('click.sasoPlanActions', '.saso-delete-plan', function() {
219 const row = plansDataTable.row($(this).closest('tr'));
220 const plan = row.data();
221 if (plan) deletePlan(plan.id, plan.seat_count || 0);
222 });
223
224 $tbody.on('click.sasoPlanActions', '.saso-clone-plan', function() {
225 const row = plansDataTable.row($(this).closest('tr'));
226 const plan = row.data();
227 if (plan) clonePlan(plan.id);
228 });
229
230 $tbody.on('click.sasoPlanActions', '.saso-open-designer', function() {
231 const row = plansDataTable.row($(this).closest('tr'));
232 const plan = row.data();
233 if (plan) openDesigner(plan);
234 });
235 }
236
237 function updateAddPlanButton() {
238 const $btn = $('.saso-add-plan');
239 const isPremium = myAjax._isPremium || false;
240 const max = myAjax._max?.seatingplans || 1;
241 const allowed = isPremium || plansData.length < max;
242
243 if (!allowed) {
244 $btn.prop('disabled', true).attr('title', config.i18n.limitReached);
245 } else {
246 $btn.prop('disabled', false).removeAttr('title');
247 }
248 }
249
250 function openPlanModal(plan) {
251 const $modal = $('#saso-plan-editor-modal');
252 const $form = $('#saso-plan-form');
253 const isEdit = plan !== null;
254
255 $form[0].reset();
256 clearImagePreview();
257
258 if (isEdit) {
259 $form.find('[name="plan_id"]').val(plan.id);
260 $form.find('[name="name"]').val(plan.name);
261 $form.find('[name="description"]').val(plan.meta?.description || '');
262 $form.find('[name="layout_type"]').val(plan.layout_type || 'simple');
263 $form.find('[name="aktiv"]').prop('checked', plan.aktiv == 1);
264 $modal.find('.saso-modal-title').text(__('Edit Seating Plan', 'event-tickets-with-ticket-scanner'));
265
266 // Load image if exists (check > 0 to handle "0" string)
267 if (plan.meta?.image_id && parseInt(plan.meta.image_id) > 0) {
268 $form.find('[name="image_id"]').val(plan.meta.image_id);
269 loadImagePreview(plan.meta.image_id);
270 }
271 } else {
272 $form.find('[name="plan_id"]').val('');
273 $form.find('[name="aktiv"]').prop('checked', true);
274 $modal.find('.saso-modal-title').text(__('New Seating Plan', 'event-tickets-with-ticket-scanner'));
275 }
276
277 $modal.show();
278 }
279
280 let isSavingPlan = false;
281 function savePlan() {
282 // Prevent duplicate saves
283 if (isSavingPlan) {
284 console.warn('savePlan: Already saving, ignoring duplicate call');
285 return;
286 }
287 isSavingPlan = true;
288
289 const $form = $('#saso-plan-form');
290 const planId = $form.find('[name="plan_id"]').val();
291 const isEdit = planId !== '';
292
293 const data = {
294 name: $form.find('[name="name"]').val(),
295 description: $form.find('[name="description"]').val(),
296 layout_type: $form.find('[name="layout_type"]').val(),
297 image_id: $form.find('[name="image_id"]').val() || '',
298 aktiv: $form.find('[name="aktiv"]').is(':checked') ? 1 : 0
299 };
300
301 if (isEdit) {
302 data.plan_id = planId;
303 }
304
305 const action = isEdit ? 'updatePlan' : 'createPlan';
306
307 makeRequest(action, data, function(response) {
308 isSavingPlan = false;
309 $('#saso-plan-editor-modal').hide();
310 showNotice(isEdit ? config.i18n.planUpdated : config.i18n.planCreated);
311 loadPlans();
312 }, function() {
313 isSavingPlan = false;
314 });
315 }
316
317 // Image handling - use 'close' event like backend.js _openMediaChooser
318 function openMediaChooser() {
319 if (typeof wp === 'undefined' || typeof wp.media === 'undefined') {
320 alert('Media library not available');
321 return;
322 }
323
324 const frame = wp.media({
325 title: config.i18n.selectImage || 'Select Image',
326 multiple: false,
327 library: { type: 'image' }
328 });
329
330 // Use 'close' event (not 'select') - this fires when modal closes
331 frame.on('close', function() {
332 const selection = frame.state().get('selection');
333 if (selection.length === 0) return; // User cancelled
334
335 const attachment = selection.first().toJSON();
336 const $modal = $('#saso-plan-editor-modal');
337 $modal.find('[name="image_id"]').val(attachment.id);
338 $modal.find('.saso-image-preview').html('<img src="' + attachment.url + '" alt="">');
339 $modal.find('.saso-remove-image').show();
340 });
341
342 frame.open();
343 }
344
345 function loadImagePreview(imageId) {
346 if (!imageId) return;
347
348 BASIC._getMediaData(imageId, function(data) {
349 if (data?.url) {
350 const $modal = $('#saso-plan-editor-modal');
351 $modal.find('.saso-image-preview').html('<img src="' + data.url + '" alt="">');
352 $modal.find('.saso-remove-image').show();
353 }
354 });
355 }
356
357 function clearImagePreview() {
358 const $modal = $('#saso-plan-editor-modal');
359 $modal.find('[name="image_id"]').val('');
360 $modal.find('.saso-image-preview').empty();
361 $modal.find('.saso-remove-image').hide();
362 }
363
364 function deletePlan(planId, seatCount) {
365 const confirmMsg = seatCount > 0 ? config.i18n.confirmDeleteWithSeats : config.i18n.confirmDelete;
366 if (!confirm(confirmMsg)) return;
367
368 makeRequest('deletePlan', { plan_id: planId, force: 'true' }, function(response) {
369 showNotice(config.i18n.planDeleted);
370 loadPlans();
371 });
372 }
373
374 function clonePlan(planId) {
375 makeRequest('clonePlan', { plan_id: planId }, function(response) {
376 showNotice(config.i18n.planCloned);
377 loadPlans();
378 });
379 }
380
381 // =========================================================================
382 // Visual Designer
383 // =========================================================================
384
385 let designerInstance = null;
386 let designerScriptLoaded = false;
387
388 /**
389 * Load designer script dynamically if not already loaded
390 */
391 function loadDesignerScript(callback) {
392 // Already loaded?
393 if (typeof window.initSeatingDesigner === 'function') {
394 callback();
395 return;
396 }
397
398 let version = myAjax._plugin_version || new Date().getTime();
399 const baseUrl = myAjax._plugin_home_url;
400
401 if (BASIC._getVarSYSTEM().is_debug) {
402 version = new Date().getTime();
403 }
404
405 // Load CSS first
406 BASIC._addStyleTag(baseUrl + '/css/seating_designer.css?v=' + version, 'saso_seating_designer_css');
407
408 // Load JS
409 const jsUrl = baseUrl + '/js/seating_designer.js?v=' + version;
410 $.getScript(jsUrl)
411 .done(function() {
412 designerScriptLoaded = true;
413 callback();
414 })
415 .fail(function(jqxhr, settings, exception) {
416 console.error('Failed to load seating_designer.js:', exception);
417 showNotice(__('Failed to load designer script', 'event-tickets-with-ticket-scanner'), 'error');
418 });
419 }
420
421 function openDesigner(plan) {
422 const $wrap = $('.saso-seating-admin-wrap');
423
424 // Destroy any existing designer BEFORE rendering new HTML
425 // (otherwise destroy() would empty the new container)
426 if (designerInstance) {
427 designerInstance.destroy();
428 designerInstance = null;
429 }
430 if (window.SasoSeatingDesigner) {
431 window.SasoSeatingDesigner = null;
432 }
433
434 $wrap.html('<div class="saso-loading">' + config.i18n.loading + '</div>');
435
436 // First load the designer script, then fetch data
437 loadDesignerScript(function() {
438 makeRequest('getDesignerPage', { plan_id: plan.id }, function(response) {
439 // Generate designer HTML in JS (no HTML from server)
440 $wrap.addClass('designer-mode').html(renderDesignerHTML(response));
441
442 // Initialize designer with config and data
443 if (typeof window.initSeatingDesigner === 'function') {
444 designerInstance = window.initSeatingDesigner(response.config);
445 // Apply loaded data
446 if (designerInstance && designerInstance.applyLoadedData) {
447 designerInstance.applyLoadedData(response);
448 }
449 } else {
450 console.error('SeatingDesigner not loaded');
451 showNotice(__('Designer failed to load', 'event-tickets-with-ticket-scanner'), 'error');
452 }
453 }, function(error) {
454 showNotice(error || config.i18n.error, 'error');
455 // Restore admin view
456 $wrap.html(renderAdminHTML());
457 initEventHandlers();
458 loadPlans();
459 });
460 });
461 }
462
463 function renderDesignerHTML(response) {
464 const plan = response.plan;
465 // These are now inside plan object (from getFullPlan)
466 const publishInfo = plan.publish_info;
467 const auditInfo = plan.audit_info || {};
468 const hasUnpublishedChanges = plan.has_unpublished_changes;
469
470 const statusClass = plan.aktiv ? 'active' : 'inactive';
471 const statusText = plan.aktiv ? __('Active', 'event-tickets-with-ticket-scanner') : __('Inactive', 'event-tickets-with-ticket-scanner');
472
473 // Build status badges
474 let badges = '<span class="saso-plan-status ' + statusClass + '">' + statusText + '</span>';
475
476 // Published badge with date (clickable to view published version)
477 if (publishInfo && publishInfo.published_at) {
478 badges += '<span class="saso-plan-status published clickable" title="' + __('Click to view published version', 'event-tickets-with-ticket-scanner') + '">' +
479 '<span class="dashicons dashicons-yes-alt"></span> ' +
480 __('Published', 'event-tickets-with-ticket-scanner') +
481 '<span class="saso-status-date">' + formatDate(publishInfo.published_at) + '</span>' +
482 '</span>';
483 }
484
485 // Draft badge with date (if has unpublished changes, clickable to view/edit draft)
486 if (hasUnpublishedChanges) {
487 const draftDate = auditInfo.updated_at || '';
488 badges += '<span class="saso-plan-status draft clickable viewing" title="' + __('Click to view/edit draft', 'event-tickets-with-ticket-scanner') + '">' +
489 '<span class="dashicons dashicons-edit"></span> ' +
490 __('Draft', 'event-tickets-with-ticket-scanner') +
491 (draftDate ? '<span class="saso-status-date">' + formatDate(draftDate) + '</span>' : '') +
492 '</span>';
493 }
494
495 return '<div class="saso-designer-container" id="saso-designer-container" data-plan-id="' + plan.id + '">' +
496 '<div class="saso-designer-header">' +
497 '<div class="header-left">' +
498 '<button type="button" class="button saso-back-to-plans">' +
499 '<span class="dashicons dashicons-arrow-left-alt"></span> ' +
500 __('Back to Plans', 'event-tickets-with-ticket-scanner') +
501 '</button>' +
502 '<h2>' + plan.name + ' - ' + __('Visual Designer', 'event-tickets-with-ticket-scanner') + '</h2>' +
503 '</div>' +
504 '<div class="header-right">' +
505 badges +
506 '</div>' +
507 '</div>' +
508 '<div class="saso-designer-notices"></div>' +
509 '<div class="saso-designer-wrap">' +
510 '<div class="saso-designer-main">' +
511 '<div class="saso-designer-toolbar-area"></div>' +
512 '<div class="saso-designer-canvas-area">' +
513 '<div class="saso-loading">' + __('Loading designer...', 'event-tickets-with-ticket-scanner') + '</div>' +
514 '</div>' +
515 '</div>' +
516 '<div class="saso-designer-sidebar">' +
517 '<div class="saso-designer-properties-area"></div>' +
518 '<div class="saso-designer-actions-area"></div>' +
519 '</div>' +
520 '</div>' +
521 '</div>';
522 }
523
524 /**
525 * Format date string for display
526 */
527 function formatDate(dateStr) {
528 if (!dateStr) return '';
529 try {
530 const date = new Date(dateStr);
531 // Format: DD.MM.YY HH:MM
532 const day = String(date.getDate()).padStart(2, '0');
533 const month = String(date.getMonth() + 1).padStart(2, '0');
534 const year = String(date.getFullYear()).slice(-2);
535 const hours = String(date.getHours()).padStart(2, '0');
536 const mins = String(date.getMinutes()).padStart(2, '0');
537 return day + '.' + month + '.' + year + ' ' + hours + ':' + mins;
538 } catch (e) {
539 return dateStr;
540 }
541 }
542
543 function closeDesigner() {
544 if (designerInstance) {
545 designerInstance.destroy();
546 designerInstance = null;
547 }
548 // Restore admin view
549 const $wrap = $('.saso-seating-admin-wrap');
550 $wrap.removeClass('designer-mode').html(renderAdminHTML());
551 initEventHandlers();
552 loadPlans();
553 }
554
555 // Make closeDesigner available globally for designer to call
556 window.sasoSeatingCloseDesigner = closeDesigner;
557
558 // =========================================================================
559 // Seats Management
560 // =========================================================================
561
562 function showSeatsView(plan) {
563 currentPlanId = plan.id;
564 currentPlanName = plan.name;
565 currentPlanLayoutType = plan.layout_type || 'simple';
566
567 $('.saso-seating-plans-section').hide();
568 $('.saso-seating-seats-section').show();
569 $('.saso-current-plan-name').text(plan.name);
570
571 // Show layout explanation
572 showLayoutExplanation(currentPlanLayoutType);
573
574 // Show plan image if exists
575 showPlanImage(plan.meta?.image_id);
576
577 // Visual mode: disable Add Seat button, show note
578 updateVisualModeUI();
579
580 loadSeats();
581 }
582
583 /**
584 * Update UI for Visual layout mode
585 * In visual mode, seats should be added/deleted via the Visual Designer only
586 */
587 function updateVisualModeUI() {
588 const $addBtn = $('.saso-add-seat');
589 const $note = $('.saso-visual-mode-note');
590
591 if (currentPlanLayoutType === 'visual') {
592 // Disable Add button in Visual mode
593 $addBtn.prop('disabled', true)
594 .attr('title', __('Use Visual Designer to add seats', 'event-tickets-with-ticket-scanner'));
595
596 // Show note if not already shown
597 if ($note.length === 0) {
598 $('.saso-seating-seats-toolbar').after(
599 '<div class="saso-visual-mode-note saso-notice saso-notice-info">' +
600 '<span class="dashicons dashicons-info"></span> ' +
601 __('This plan uses Visual Layout. Add and delete seats using the Visual Designer. You can edit seat details (label, category) here.', 'event-tickets-with-ticket-scanner') +
602 '</div>'
603 );
604 }
605 } else {
606 // Simple mode: ensure Add button is enabled
607 $addBtn.prop('disabled', false).removeAttr('title');
608 $note.remove();
609 }
610 }
611
612 function showPlanImage(imageId) {
613 const $section = $('.saso-plan-image-section');
614 const $container = $('.saso-plan-image-container');
615 const $toggleBtn = $('.saso-toggle-plan-image');
616
617 if (!imageId || imageId == 0) {
618 $section.hide();
619 $container.empty();
620 $toggleBtn.hide();
621 return;
622 }
623
624 BASIC._getMediaData(imageId, function(data) {
625 if (data?.url) {
626 $container.html('<img src="' + data.url + '" alt="">');
627 $toggleBtn.show();
628 $section.hide();
629 updateToggleButtonText(false);
630 } else {
631 $section.hide();
632 $toggleBtn.hide();
633 }
634 });
635 }
636
637 function showPlanImageModal(imageId, planName) {
638 if (!imageId) return;
639
640 BASIC._getMediaData(imageId, function(data) {
641 if (data?.url) {
642 // Create and show modal
643 const $modal = $(`
644 <div class="saso-modal saso-image-modal" style="display:flex;">
645 <div class="saso-modal-content" style="max-width:90vw;max-height:90vh;">
646 <div class="saso-modal-header">
647 <h3 class="saso-modal-title">${escapeHtml(planName)}</h3>
648 <button type="button" class="saso-modal-close">&times;</button>
649 </div>
650 <div class="saso-modal-body" style="padding:0;overflow:auto;">
651 <img src="${data.url}" alt="${escapeHtml(planName)}" style="max-width:100%;height:auto;display:block;">
652 </div>
653 </div>
654 </div>
655 `);
656
657 $modal.on('click', '.saso-modal-close', function() {
658 $modal.remove();
659 });
660 $modal.on('click', function(e) {
661 if (e.target === this) $modal.remove();
662 });
663
664 $('body').append($modal);
665 }
666 });
667 }
668
669 function togglePlanImage() {
670 const $section = $('.saso-plan-image-section');
671 const isVisible = $section.is(':visible');
672
673 if (isVisible) {
674 $section.slideUp(200);
675 } else {
676 $section.slideDown(200);
677 }
678 updateToggleButtonText(!isVisible);
679 }
680
681 function updateToggleButtonText(isVisible) {
682 const $btn = $('.saso-toggle-plan-image');
683 const showText = config.i18n.showLayout || 'Show Layout';
684 const hideText = config.i18n.hideLayout || 'Hide Layout';
685
686 $btn.find('.dashicons').removeClass('dashicons-hidden dashicons-format-image')
687 .addClass(isVisible ? 'dashicons-hidden' : 'dashicons-format-image');
688 $btn.contents().filter(function() { return this.nodeType === 3; }).remove();
689 $btn.append(' ' + (isVisible ? hideText : showText));
690 }
691
692 function showLayoutExplanation(layoutType) {
693 const $box = $('.saso-layout-explanation');
694 let title, desc, icon;
695
696 if (layoutType === 'visual') {
697 title = config.i18n.layoutVisualTitle;
698 desc = config.i18n.layoutVisualDesc;
699 icon = 'dashicons-layout';
700 } else {
701 title = config.i18n.layoutSimpleTitle;
702 desc = config.i18n.layoutSimpleDesc;
703 icon = 'dashicons-list-view';
704 }
705
706 $box.html(
707 '<div class="saso-layout-box">' +
708 '<span class="dashicons ' + icon + '"></span>' +
709 '<div class="saso-layout-text">' +
710 '<strong>' + title + '</strong>' +
711 '<p>' + desc + '</p>' +
712 '</div></div>'
713 ).show();
714 }
715
716 function showPlansView() {
717 currentPlanId = null;
718 currentPlanName = '';
719 currentPlanLayoutType = 'simple';
720
721 // Remove visual mode note if exists
722 $('.saso-visual-mode-note').remove();
723
724 $('.saso-seating-seats-section').hide();
725 $('.saso-seating-plans-section').show();
726 }
727
728 function loadSeats() {
729 if (!currentPlanId) return;
730
731 const $list = $('.saso-seating-seats-list');
732 $list.html('<div class="saso-loading">' + config.i18n.loading + '</div>');
733
734 makeRequest('getSeats', { plan_id: currentPlanId }, function(data) {
735 seatsData = data.seats || [];
736 renderSeats();
737 updateSeatsCount();
738 updateAddSeatButton();
739 });
740 }
741
742 function renderSeats() {
743 const $list = $('.saso-seating-seats-list');
744 const tableId = 'saso-seats-datatable';
745
746 // Destroy existing DataTable
747 if (seatsDataTable) {
748 seatsDataTable.destroy();
749 seatsDataTable = null;
750 }
751
752 // Create table structure with checkbox column
753 $list.html('<table id="' + tableId + '" class="display" style="width:100%">' +
754 '<thead><tr>' +
755 '<th class="saso-batch-checkbox-col"><input type="checkbox" class="saso-seat-select-all" title="' + __('Select All', 'event-tickets-with-ticket-scanner') + '"></th>' +
756 '<th>' + __('Identifier', 'event-tickets-with-ticket-scanner') + '</th>' +
757 '<th>' + __('Label', 'event-tickets-with-ticket-scanner') + '</th>' +
758 '<th>' + __('Category', 'event-tickets-with-ticket-scanner') + '</th>' +
759 '<th>' + __('Status', 'event-tickets-with-ticket-scanner') + '</th>' +
760 '<th>' + __('Actions', 'event-tickets-with-ticket-scanner') + '</th>' +
761 '</tr></thead></table>');
762
763 seatsDataTable = $('#' + tableId).DataTable({
764 language: {
765 emptyTable: config.i18n.noSeats
766 },
767 responsive: true,
768 searching: true,
769 ordering: true,
770 processing: false,
771 serverSide: false,
772 stateSave: false,
773 pageLength: 25,
774 data: seatsData,
775 order: [[1, 'asc']], // Sort by Identifier (column 1 now, checkbox is 0)
776 columns: [
777 {
778 // Checkbox column
779 data: null,
780 orderable: false,
781 className: 'dt-center saso-batch-checkbox-col',
782 width: 30,
783 render: (data, type, row) => '<input type="checkbox" class="saso-seat-checkbox" data-seat-id="' + row.id + '">'
784 },
785 {
786 data: 'seat_identifier',
787 orderable: true,
788 render: (data) => '<code>' + escapeHtml(data) + '</code>'
789 },
790 {
791 data: 'meta',
792 orderable: true,
793 render: (data) => escapeHtml(data?.seat_label || '-')
794 },
795 {
796 data: 'meta',
797 orderable: true,
798 render: (data) => escapeHtml(data?.seat_category || '-')
799 },
800 {
801 data: 'aktiv',
802 orderable: true,
803 width: 80,
804 render: (data) => data == 1
805 ? '<span class="saso-status-active">' + __('Active', 'event-tickets-with-ticket-scanner') + '</span>'
806 : '<span class="saso-status-inactive">' + __('Inactive', 'event-tickets-with-ticket-scanner') + '</span>'
807 },
808 {
809 data: null,
810 orderable: false,
811 className: 'dt-right',
812 width: 100,
813 render: () => {
814 let buttons = '<button type="button" class="button button-small saso-edit-seat" title="' + __('Edit', 'event-tickets-with-ticket-scanner') + '"><span class="dashicons dashicons-edit"></span></button> ';
815 // In Visual mode, hide delete button (must use Designer)
816 if (currentPlanLayoutType !== 'visual') {
817 buttons += '<button type="button" class="button button-small saso-delete-seat" title="' + __('Delete', 'event-tickets-with-ticket-scanner') + '"><span class="dashicons dashicons-trash"></span></button>';
818 }
819 return buttons;
820 }
821 }
822 ]
823 });
824
825 // Set table width to 100%
826 $('#' + tableId).css('width', '100%');
827
828 // Event handlers for DataTable buttons - use .off() first to prevent duplicates
829 const $seatsTbody = $('#' + tableId + ' tbody');
830 $seatsTbody.off('click.sasoSeatActions');
831
832 $seatsTbody.on('click.sasoSeatActions', '.saso-edit-seat', function() {
833 const row = seatsDataTable.row($(this).closest('tr'));
834 const seat = row.data();
835 if (seat) openSeatModal(seat);
836 });
837
838 $seatsTbody.on('click.sasoSeatActions', '.saso-delete-seat', function() {
839 const row = seatsDataTable.row($(this).closest('tr'));
840 const seat = row.data();
841 if (seat) deleteSeat(seat.id);
842 });
843 }
844
845 function updateSeatsCount() {
846 $('.saso-seats-count').text(sprintf(__('%d seats', 'event-tickets-with-ticket-scanner'), seatsData.length));
847 }
848
849 function updateAddSeatButton() {
850 const $btn = $('.saso-add-seat');
851
852 // In Visual mode, Add button is always disabled (use Designer)
853 if (currentPlanLayoutType === 'visual') {
854 $btn.prop('disabled', true)
855 .attr('title', __('Use Visual Designer to add seats', 'event-tickets-with-ticket-scanner'));
856 return;
857 }
858
859 // Simple mode: check limits
860 const isPremium = myAjax._isPremium || false;
861 const max = myAjax._max?.seats_per_plan || 20;
862 const allowed = isPremium || seatsData.length < max;
863
864 if (!allowed) {
865 $btn.prop('disabled', true).attr('title', config.i18n.limitReached);
866 } else {
867 $btn.prop('disabled', false).removeAttr('title');
868 }
869 }
870
871 function openSeatModal(seat) {
872 const $modal = $('#saso-seat-editor-modal');
873 const $form = $('#saso-seat-form');
874 const isEdit = seat !== null;
875
876 $form[0].reset();
877 $form.find('[name="plan_id"]').val(currentPlanId);
878
879 if (isEdit) {
880 $form.find('[name="seat_id"]').val(seat.id);
881 $form.find('[name="seat_identifier"]').val(seat.seat_identifier);
882 $form.find('[name="seat_label"]').val(seat.meta?.seat_label || '');
883 $form.find('[name="seat_category"]').val(seat.meta?.seat_category || '');
884 $form.find('[name="aktiv"]').prop('checked', seat.aktiv == 1);
885 $modal.find('.saso-modal-title').text(__('Edit Seat', 'event-tickets-with-ticket-scanner'));
886 } else {
887 $form.find('[name="seat_id"]').val('');
888 $form.find('[name="aktiv"]').prop('checked', true);
889 $modal.find('.saso-modal-title').text(__('New Seat', 'event-tickets-with-ticket-scanner'));
890 }
891
892 $modal.show();
893 }
894
895 function saveSeat(keepOpen) {
896 const $form = $('#saso-seat-form');
897 const $modal = $('#saso-seat-editor-modal');
898 const seatId = $form.find('[name="seat_id"]').val();
899 const isEdit = seatId !== '';
900
901 const identifier = $form.find('[name="seat_identifier"]').val().trim();
902 if (!identifier) {
903 showNotice(__('Identifier is required', 'event-tickets-with-ticket-scanner'), 'error');
904 $form.find('[name="seat_identifier"]').focus();
905 return;
906 }
907
908 const data = {
909 plan_id: $form.find('[name="plan_id"]').val(),
910 seat_identifier: identifier,
911 seat_label: $form.find('[name="seat_label"]').val(),
912 seat_category: $form.find('[name="seat_category"]').val(),
913 aktiv: $form.find('[name="aktiv"]').is(':checked') ? 1 : 0
914 };
915
916 if (isEdit) {
917 data.seat_id = seatId;
918 }
919
920 const action = isEdit ? 'updateSeat' : 'createSeat';
921
922 makeRequest(action, data, function() {
923 showNotice(isEdit ? config.i18n.seatUpdated : config.i18n.seatCreated);
924 loadSeats();
925
926 if (keepOpen && !isEdit) {
927 // Clear form for next entry, keep modal open
928 $form.find('[name="seat_identifier"]').val('').focus();
929 $form.find('[name="seat_label"]').val('');
930 $form.find('[name="seat_category"]').val('');
931 // Keep aktiv checked and plan_id
932 } else {
933 $modal.hide();
934 }
935 });
936 }
937
938 function deleteSeat(seatId) {
939 if (!confirm(config.i18n.confirmDelete)) return;
940
941 makeRequest('deleteSeat', { seat_id: seatId, force: 'true' }, function() {
942 showNotice(config.i18n.seatDeleted);
943 loadSeats();
944 });
945 }
946
947 function handleImportCSV(file) {
948 var reader = new FileReader();
949 reader.onload = function(e) {
950 var rows = parseCSV(e.target.result);
951 if (!rows || rows.length === 0) {
952 showNotice(__('No valid rows found in CSV file.', 'event-tickets-with-ticket-scanner'), 'error');
953 return;
954 }
955 var msg = __('Import %d rows? Existing seats with matching identifiers will be updated.', 'event-tickets-with-ticket-scanner').replace('%d', rows.length);
956 if (!confirm(msg)) return;
957
958 makeRequest('importSeatsCSV', {
959 plan_id: currentPlanId,
960 rows: JSON.stringify(rows)
961 }, function(data) {
962 var parts = [];
963 if (data.created > 0) parts.push(data.created + ' ' + __('created', 'event-tickets-with-ticket-scanner'));
964 if (data.updated > 0) parts.push(data.updated + ' ' + __('updated', 'event-tickets-with-ticket-scanner'));
965 if (data.skipped > 0) parts.push(data.skipped + ' ' + __('skipped', 'event-tickets-with-ticket-scanner'));
966 var msg = __('Import complete:', 'event-tickets-with-ticket-scanner') + ' ' + parts.join(', ');
967 if (data.ignored_columns && data.ignored_columns.length > 0) {
968 msg += '\n' + __('Ignored unknown columns:', 'event-tickets-with-ticket-scanner') + ' ' + data.ignored_columns.join(', ');
969 }
970 showNotice(msg);
971 loadSeats();
972 }, function(error) {
973 showNotice(error || __('Import failed.', 'event-tickets-with-ticket-scanner'), 'error');
974 });
975 };
976 reader.readAsText(file);
977 }
978
979 function parseCSV(text) {
980 var lines = text.split(/\r?\n/);
981 if (lines.length < 2) return [];
982
983 // Detect delimiter: semicolon (from our export) or comma
984 var firstLine = lines[0];
985 var delimiter = firstLine.indexOf(';') !== -1 ? ';' : ',';
986
987 var headers = lines[0].split(delimiter).map(function(h) {
988 return h.trim().replace(/^["']|["']$/g, '');
989 });
990
991 // Require identifier column
992 if (headers.indexOf('identifier') === -1) return [];
993
994 var rows = [];
995 for (var i = 1; i < lines.length; i++) {
996 var line = lines[i].trim();
997 if (!line) continue;
998
999 var values = splitCSVLine(line, delimiter);
1000 var row = {};
1001 for (var j = 0; j < headers.length; j++) {
1002 row[headers[j]] = values[j] || '';
1003 }
1004 rows.push(row);
1005 }
1006 return rows;
1007 }
1008
1009 function splitCSVLine(line, delimiter) {
1010 var result = [];
1011 var current = '';
1012 var inQuotes = false;
1013 for (var i = 0; i < line.length; i++) {
1014 var ch = line[i];
1015 if (inQuotes) {
1016 if (ch === '"' && line[i + 1] === '"') {
1017 current += '"';
1018 i++;
1019 } else if (ch === '"') {
1020 inQuotes = false;
1021 } else {
1022 current += ch;
1023 }
1024 } else {
1025 if (ch === '"') {
1026 inQuotes = true;
1027 } else if (ch === delimiter) {
1028 result.push(current.trim());
1029 current = '';
1030 } else {
1031 current += ch;
1032 }
1033 }
1034 }
1035 result.push(current.trim());
1036 return result;
1037 }
1038
1039 // =========================================================================
1040 // Batch Operations
1041 // =========================================================================
1042
1043 function getSelectedSeatIds() {
1044 return $('.saso-seat-checkbox:checked').map(function() {
1045 return $(this).data('seat-id');
1046 }).get();
1047 }
1048
1049 function updateBatchToolbar() {
1050 if (batchInProgress) return; // Don't update during batch
1051
1052 const count = getSelectedSeatIds().length;
1053 $('.saso-selected-count').text(count);
1054 $('.saso-batch-toolbar').toggle(count > 0);
1055 $('.saso-batch-execute').prop('disabled', count === 0 || !$('.saso-batch-action').val());
1056
1057 // In Visual mode: hide delete option
1058 if (currentPlanLayoutType === 'visual') {
1059 $('.saso-batch-delete-option').hide();
1060 } else {
1061 $('.saso-batch-delete-option').show();
1062 }
1063
1064 // Update select-all checkbox state
1065 const totalCheckboxes = $('.saso-seat-checkbox').length;
1066 const checkedCheckboxes = $('.saso-seat-checkbox:checked').length;
1067 $('.saso-seat-select-all').prop('checked', totalCheckboxes > 0 && checkedCheckboxes === totalCheckboxes);
1068 }
1069
1070 function setBatchUIState(inProgress, current, total) {
1071 batchInProgress = inProgress;
1072
1073 // Disable/Enable UI elements
1074 $('.saso-seat-checkbox, .saso-seat-select-all, .saso-batch-action, .saso-batch-execute, .saso-add-seat, .saso-back-to-plans')
1075 .prop('disabled', inProgress);
1076
1077 // Show/hide progress
1078 $('.saso-batch-progress').toggle(inProgress);
1079 if (inProgress && total > 0) {
1080 $('.saso-batch-progress-text').text(current + '/' + total);
1081 }
1082 }
1083
1084 function executeBatchAction() {
1085 if (batchInProgress) return;
1086
1087 // IMPORTANT: Capture all values BEFORE the loop!
1088 const ids = getSelectedSeatIds().slice(); // Copy array
1089 const action = $('.saso-batch-action').val();
1090
1091 if (!ids.length || !action) return;
1092
1093 if (action === 'delete') {
1094 if (!confirm(sprintf(config.i18n.batchConfirmDelete, ids.length))) return;
1095 runBatchOperation(ids, 'delete', {});
1096 } else if (action === 'activate') {
1097 runBatchOperation(ids, 'update', { aktiv: 1 });
1098 } else if (action === 'deactivate') {
1099 runBatchOperation(ids, 'update', { aktiv: 0 });
1100 }
1101 }
1102
1103 function runBatchOperation(ids, operation, updateData) {
1104 const total = ids.length;
1105 const useDelay = total > 10; // Anti-DDOS: 500ms delay for >10 items
1106 let completed = 0;
1107 let errors = 0;
1108 let index = 0;
1109
1110 setBatchUIState(true, 0, total);
1111
1112 function processNext() {
1113 if (index >= ids.length) {
1114 // Done
1115 finishBatch(completed, errors, total, operation);
1116 return;
1117 }
1118
1119 const id = ids[index];
1120 index++;
1121
1122 const onSuccess = function() {
1123 completed++;
1124 setBatchUIState(true, completed, total);
1125 scheduleNext();
1126 };
1127
1128 const onError = function() {
1129 errors++;
1130 completed++;
1131 setBatchUIState(true, completed, total);
1132 scheduleNext();
1133 };
1134
1135 if (operation === 'delete') {
1136 makeRequest('deleteSeat', { seat_id: id, force: 'true' }, onSuccess, onError);
1137 } else {
1138 const requestData = Object.assign({ seat_id: id }, updateData);
1139 makeRequest('updateSeat', requestData, onSuccess, onError);
1140 }
1141 }
1142
1143 function scheduleNext() {
1144 if (useDelay) {
1145 setTimeout(processNext, 500);
1146 } else {
1147 processNext();
1148 }
1149 }
1150
1151 // Start
1152 processNext();
1153 }
1154
1155 function finishBatch(completed, errors, total, operation) {
1156 setBatchUIState(false, 0, 0);
1157
1158 const success = total - errors;
1159
1160 if (errors > 0) {
1161 showNotice(sprintf(config.i18n.batchWithErrors, success, errors), 'warning');
1162 } else {
1163 if (operation === 'delete') {
1164 showNotice(sprintf(config.i18n.batchSeatsDeleted, success));
1165 } else {
1166 showNotice(sprintf(config.i18n.batchSeatsUpdated, success));
1167 }
1168 }
1169
1170 // Reset selection and reload
1171 $('.saso-batch-action').val('');
1172 $('.saso-seat-select-all').prop('checked', false);
1173 loadSeats();
1174 }
1175
1176 // =========================================================================
1177 // Utility Functions
1178 // =========================================================================
1179
1180 function escapeHtml(text) {
1181 if (!text) return '';
1182 const div = document.createElement('div');
1183 div.textContent = text;
1184 return div.innerHTML;
1185 }
1186
1187 // =========================================================================
1188 // Event Handlers
1189 // =========================================================================
1190
1191 function initEventHandlers() {
1192 const $wrap = $('.saso-seating-admin-wrap');
1193
1194 // IMPORTANT: Remove all existing handlers first to prevent duplicates
1195 $wrap.off('click.sasoSeating');
1196 $(document).off('keydown.sasoSeatingAdmin');
1197
1198 // Toolbar buttons (not in DataTable) - use namespaced events
1199 $wrap.on('click.sasoSeating', '.saso-add-plan', function() { openPlanModal(null); });
1200 $wrap.on('click.sasoSeating', '.saso-back-to-plans', function() { showPlansView(); });
1201 $wrap.on('click.sasoSeating', '.saso-add-seat', function() { openSeatModal(null); });
1202 $wrap.on('click.sasoSeating', '.saso-export-seats-csv', function() {
1203 if (!currentPlanId) return;
1204 let url = BASIC._requestURL('seating', {c: 'exportSeatsCSV', plan_id: currentPlanId});
1205 window.open(url, '_blank');
1206 });
1207 $wrap.on('click.sasoSeating', '.saso-import-seats-csv', function() {
1208 if (!currentPlanId) return;
1209 $wrap.find('.saso-import-csv-file').val('').trigger('click');
1210 });
1211 $wrap.on('change.sasoSeating', '.saso-import-csv-file', function() {
1212 var file = this.files[0];
1213 if (!file) return;
1214 handleImportCSV(file);
1215 });
1216
1217 // Modal buttons
1218 $wrap.on('click.sasoSeating', '.saso-save-plan', function() { savePlan(); });
1219 $wrap.on('click.sasoSeating', '.saso-save-seat', function() { saveSeat(false); });
1220 $wrap.on('click.sasoSeating', '.saso-save-seat-next', function() { saveSeat(true); });
1221
1222 // Image handlers
1223 $wrap.on('click.sasoSeating', '.saso-select-image', function() { openMediaChooser(); });
1224 $wrap.on('click.sasoSeating', '.saso-remove-image', function() { clearImagePreview(); });
1225 $wrap.on('click.sasoSeating', '.saso-toggle-plan-image', function() { togglePlanImage(); });
1226
1227 // Modal close
1228 $wrap.on('click.sasoSeating', '.saso-modal-close, .saso-modal-cancel', function() {
1229 $(this).closest('.saso-modal').hide();
1230 });
1231 $wrap.on('click.sasoSeating', '.saso-modal', function(e) {
1232 if (e.target === this) $(this).hide();
1233 });
1234 // Use namespaced event so we can remove it later
1235 $(document).on('keydown.sasoSeatingAdmin', function(e) {
1236 if (e.key === 'Escape') $('.saso-modal:visible').hide();
1237 });
1238
1239 // Batch operations
1240 $wrap.on('change.sasoSeating', '.saso-seat-checkbox', function() {
1241 updateBatchToolbar();
1242 });
1243 $wrap.on('change.sasoSeating', '.saso-seat-select-all', function() {
1244 const checked = $(this).prop('checked');
1245 $('.saso-seat-checkbox').prop('checked', checked);
1246 updateBatchToolbar();
1247 });
1248 $wrap.on('change.sasoSeating', '.saso-batch-action', function() {
1249 updateBatchToolbar();
1250 });
1251 $wrap.on('click.sasoSeating', '.saso-batch-execute', function() {
1252 executeBatchAction();
1253 });
1254 }
1255
1256 // =========================================================================
1257 // HTML Templates
1258 // =========================================================================
1259
1260 function renderAdminHTML() {
1261 const isPremium = myAjax._isPremium || false;
1262 const max = myAjax._max?.seatingplans || 1;
1263 const limitsText = !isPremium ?
1264 sprintf(__('Free Version: %d/%d plans', 'event-tickets-with-ticket-scanner'), plansData.length, max) : '';
1265
1266 return `
1267 <div class="saso-seating-admin-wrap">
1268 <div class="saso-seating-header">
1269 <h2>${__('Seating Plans', 'event-tickets-with-ticket-scanner')}</h2>
1270 ${!isPremium ? `
1271 <div class="saso-seating-limits">
1272 ${limitsText}
1273 <a href="https://vollstart.com/event-tickets-with-ticket-scanner/" target="_blank" class="button">
1274 ${__('Upgrade to Premium', 'event-tickets-with-ticket-scanner')}
1275 </a>
1276 </div>` : ''}
1277 </div>
1278
1279 <!-- Plans List -->
1280 <div class="saso-seating-plans-section">
1281 <div class="saso-seating-toolbar">
1282 <button type="button" class="button button-primary saso-add-plan">
1283 ${__('+ Add Seating Plan', 'event-tickets-with-ticket-scanner')}
1284 </button>
1285 </div>
1286 <div class="saso-seating-plans-list">
1287 <div class="saso-loading">${config.i18n.loading}</div>
1288 </div>
1289 </div>
1290
1291 <!-- Plan Editor Modal -->
1292 ${renderPlanModal()}
1293
1294 <!-- Seats Section -->
1295 <div class="saso-seating-seats-section" style="display:none;">
1296 <div class="saso-seating-seats-header">
1297 <h3>${__('Seats', 'event-tickets-with-ticket-scanner')} - <span class="saso-current-plan-name"></span></h3>
1298 <button type="button" class="button saso-back-to-plans">&larr; ${__('Back to Plans', 'event-tickets-with-ticket-scanner')}</button>
1299 </div>
1300 <div class="saso-layout-explanation" style="display:none;"></div>
1301 <div class="saso-seating-seats-toolbar">
1302 <button type="button" class="button button-primary saso-add-seat">${__('+ Add Seat', 'event-tickets-with-ticket-scanner')}</button>
1303 <button type="button" class="button saso-toggle-plan-image" style="display:none;">
1304 <span class="dashicons dashicons-format-image"></span>
1305 ${__('Show Layout', 'event-tickets-with-ticket-scanner')}
1306 </button>
1307 ${myAjax._isPremium ? '<button type="button" class="button saso-export-seats-csv"><span class="dashicons dashicons-download" style="vertical-align:middle;margin-right:2px;"></span>' + __('Export CSV', 'event-tickets-with-ticket-scanner') + '</button>' : ''}
1308 ${myAjax._isPremium ? '<button type="button" class="button saso-import-seats-csv"><span class="dashicons dashicons-upload" style="vertical-align:middle;margin-right:2px;"></span>' + __('Import CSV', 'event-tickets-with-ticket-scanner') + '</button><input type="file" class="saso-import-csv-file" accept=".csv" style="display:none;">' : ''}
1309 <span class="saso-seats-count"></span>
1310 </div>
1311 <div class="saso-batch-toolbar" style="display:none;">
1312 <span class="saso-batch-selection">
1313 <span class="saso-selected-count">0</span> ${config.i18n.selected}
1314 </span>
1315 <select class="saso-batch-action">
1316 <option value="">${config.i18n.batchActionSelect}</option>
1317 <option value="activate">${config.i18n.batchActivate}</option>
1318 <option value="deactivate">${config.i18n.batchDeactivate}</option>
1319 <option value="delete" class="saso-batch-delete-option">${config.i18n.batchDelete}</option>
1320 </select>
1321 <button type="button" class="button saso-batch-execute" disabled>${__('Apply', 'event-tickets-with-ticket-scanner')}</button>
1322 <span class="saso-batch-progress" style="display:none;">
1323 <span class="spinner is-active" style="float:none;margin:0 5px;"></span>
1324 <span class="saso-batch-progress-text">0/0</span>
1325 </span>
1326 </div>
1327 <div class="saso-seating-seats-list"></div>
1328 <div class="saso-plan-image-section" style="display:none;">
1329 <h4>${__('Seating Plan Layout', 'event-tickets-with-ticket-scanner')}</h4>
1330 <div class="saso-plan-image-container"></div>
1331 </div>
1332 </div>
1333
1334 <!-- Seat Editor Modal -->
1335 ${renderSeatModal()}
1336 </div>`;
1337 }
1338
1339 function renderPlanModal() {
1340 return `
1341 <div id="saso-plan-editor-modal" class="saso-modal" style="display:none;">
1342 <div class="saso-modal-content">
1343 <div class="saso-modal-header">
1344 <h3 class="saso-modal-title">${__('Seating Plan', 'event-tickets-with-ticket-scanner')}</h3>
1345 <button type="button" class="saso-modal-close">&times;</button>
1346 </div>
1347 <div class="saso-modal-body">
1348 <form id="saso-plan-form">
1349 <input type="hidden" name="plan_id" value="">
1350 <div class="saso-form-group">
1351 <label for="plan_name">${__('Plan Name', 'event-tickets-with-ticket-scanner')} <span class="required">*</span></label>
1352 <input type="text" id="plan_name" name="name" required class="regular-text">
1353 </div>
1354 <div class="saso-form-group">
1355 <label for="plan_description">${__('Description', 'event-tickets-with-ticket-scanner')}</label>
1356 <textarea id="plan_description" name="description" rows="3" class="large-text"></textarea>
1357 </div>
1358 <div class="saso-form-row">
1359 <div class="saso-form-group">
1360 <label for="plan_layout_type">${__('Layout Type', 'event-tickets-with-ticket-scanner')}</label>
1361 <select id="plan_layout_type" name="layout_type">
1362 <option value="simple">${__('Simple (Dropdown)', 'event-tickets-with-ticket-scanner')}</option>
1363 <option value="visual">${__('Visual (Seat Map)', 'event-tickets-with-ticket-scanner')}</option>
1364 </select>
1365 </div>
1366 <div class="saso-form-group">
1367 <label><input type="checkbox" name="aktiv" value="1" checked> ${__('Active', 'event-tickets-with-ticket-scanner')}</label>
1368 </div>
1369 </div>
1370 <div class="saso-form-group">
1371 <label>${__('Venue Photo', 'event-tickets-with-ticket-scanner')} <span class="optional">(${__('optional', 'event-tickets-with-ticket-scanner')})</span></label>
1372 <p class="description">${__('Optional: Upload a photo of the entire venue/hall. Customers can view this via "View Seating Plan" button. Useful when the seat map only shows a specific section.', 'event-tickets-with-ticket-scanner')}</p>
1373 <input type="hidden" name="image_id" id="plan_image_id" value="">
1374 <div class="saso-image-preview" id="plan_image_preview"></div>
1375 <div class="saso-image-buttons">
1376 <button type="button" class="button saso-select-image">${__('Select Image', 'event-tickets-with-ticket-scanner')}</button>
1377 <button type="button" class="button saso-remove-image" style="display:none;">${__('Remove', 'event-tickets-with-ticket-scanner')}</button>
1378 </div>
1379 </div>
1380 </form>
1381 </div>
1382 <div class="saso-modal-footer">
1383 <button type="button" class="button saso-modal-cancel">${__('Cancel', 'event-tickets-with-ticket-scanner')}</button>
1384 <button type="button" class="button button-primary saso-save-plan">${__('Save Plan', 'event-tickets-with-ticket-scanner')}</button>
1385 </div>
1386 </div>
1387 </div>`;
1388 }
1389
1390 function renderSeatModal() {
1391 return `
1392 <div id="saso-seat-editor-modal" class="saso-modal" style="display:none;">
1393 <div class="saso-modal-content">
1394 <div class="saso-modal-header">
1395 <h3 class="saso-modal-title">${__('Seat', 'event-tickets-with-ticket-scanner')}</h3>
1396 <button type="button" class="saso-modal-close">&times;</button>
1397 </div>
1398 <div class="saso-modal-body">
1399 <form id="saso-seat-form">
1400 <input type="hidden" name="seat_id" value="">
1401 <input type="hidden" name="plan_id" value="">
1402 <div class="saso-form-group">
1403 <label for="seat_identifier">${__('Seat Identifier', 'event-tickets-with-ticket-scanner')} <span class="required">*</span></label>
1404 <input type="text" id="seat_identifier" name="seat_identifier" required class="regular-text" placeholder="A-1">
1405 <p class="description">${__('Unique ID like A-1, B-2, VIP-01', 'event-tickets-with-ticket-scanner')}</p>
1406 </div>
1407 <div class="saso-form-group">
1408 <label for="seat_label">${__('Display Label', 'event-tickets-with-ticket-scanner')}</label>
1409 <input type="text" id="seat_label" name="seat_label" class="regular-text" placeholder="Row A, Seat 1">
1410 <p class="description">${__('Shown on ticket PDF', 'event-tickets-with-ticket-scanner')}</p>
1411 </div>
1412 <div class="saso-form-group">
1413 <label for="seat_category">${__('Category', 'event-tickets-with-ticket-scanner')}</label>
1414 <input type="text" id="seat_category" name="seat_category" class="regular-text" placeholder="VIP, Standard, Balcony">
1415 </div>
1416 <div class="saso-form-group">
1417 <label><input type="checkbox" name="aktiv" value="1" checked> ${__('Active', 'event-tickets-with-ticket-scanner')}</label>
1418 </div>
1419 </form>
1420 </div>
1421 <div class="saso-modal-footer">
1422 <button type="button" class="button saso-modal-cancel">${__('Cancel', 'event-tickets-with-ticket-scanner')}</button>
1423 <button type="button" class="button saso-save-seat-next">${__('Save & Add Next', 'event-tickets-with-ticket-scanner')}</button>
1424 <button type="button" class="button button-primary saso-save-seat">${__('Save & Close', 'event-tickets-with-ticket-scanner')}</button>
1425 </div>
1426 </div>
1427 </div>`;
1428 }
1429
1430 // =========================================================================
1431 // Init
1432 // =========================================================================
1433
1434 function cleanup() {
1435 // Destroy designer if open
1436 if (designerInstance) {
1437 designerInstance.destroy();
1438 designerInstance = null;
1439 }
1440
1441 // Remove all namespaced event handlers to prevent duplicates
1442 $('.saso-seating-admin-wrap').off('click.sasoSeating');
1443 $(document).off('keydown.sasoSeatingAdmin');
1444
1445 // Destroy DataTables
1446 if (plansDataTable) {
1447 plansDataTable.destroy();
1448 plansDataTable = null;
1449 }
1450 if (seatsDataTable) {
1451 seatsDataTable.destroy();
1452 seatsDataTable = null;
1453 }
1454 }
1455
1456 function initAdmin(div) {
1457 // Clean up any previous state first
1458 cleanup();
1459
1460 adminDiv = div;
1461 adminDiv.html(renderAdminHTML());
1462 initEventHandlers();
1463 loadPlans();
1464 }
1465
1466 return {
1467 initAdmin: initAdmin,
1468 cleanup: cleanup
1469 };
1470 }
1471