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">×</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">← ${__('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">×</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">×</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 |