{% block content %} {% if modal %}<div class="modal-content">{% endif %} <div id="wa-checkin-{{ id }}" hx-target="this" hx-swap="outerHTML" class="frame"> <div class="frame__header"> <h3>Check-in</h3> <div class="grid-md u-gap-2"> <div class="grid-c-6 mb-2 mb-0-md"> <div class="u-flex u-items-center u-gap-1"> <i class="icon-uhr-outline"></i> <div class="t-label">Tag/Urzeit</div> </div> <div class="">{{ slot.time|date('d.m.Y H:i') }}</div> </div> <div class="grid-c-6 mb-2 mb-0-md"> <div class="u-flex u-items-center u-gap-1"> <i class="icon-standort-outline"></i> <div class="t-label">Standort</div> </div> <div class="">{{ standort.title }}</div> </div> <div class="grid-c-6 mb-2 mb-0-md"> <div class="u-flex u-items-center u-gap-1"> <i class="icon-behaelter-outline"></i> <div class="t-label">Gebuchte Behälterkapazität</div> </div> <div class="">{{ buchung.behaelter }}</div> </div> <div class="grid-c-6 mb-2 mb-0-md"> <div class="u-flex u-items-center u-gap-1"> <i class="icon-reben-outline"></i> <div class="t-label">Anliefernde Sorten</div> </div> <div class="">{{ buchung.sorten|join(', ') }}</div> </div> {% if slot.anmerkungen %} <div class="grid-c-12 mb-2 mb-0-md"> <div class="u-flex u-items-center u-gap-1"> <i class="icon-info-outline"></i> <div class="t-label">Anmerkungen</div> </div> {{ slot.anmerkungen|raw }} </div> {% endif %} </div> </div> <div class="divider m-0 mb-2"></div> <div class="frame__body"> <h3>Behälternummern zuweisen</h3> {% if toast is defined %} {{ toast|raw }} {% endif %} <form hx-post="/_ajax/vr_wa/v1/slot?do=updateCheckin" enctype="multipart/form-data"> <input type="hidden" name="id" value="{{ id }}"> <!-- Datalist for all behaelter number inputs --> <datalist id="behaelter-numbers-list"> <!-- Options will be loaded via Ajax --> </datalist> <div class="grid-md u-gap-2"> {% for i in 1..checkin.behaelter %} <div class="grid-c-12"> <div class="grid-md u-gap-2"> <div class="grid-c-6"> <fieldset> <label for="behaelter-number-{{ i }}"><strong>Behälter {{ i }}</strong><sup class="text-danger">*</sup></label> <input type="text" id="behaelter-number-{{ i }}" name="behaelter_numbers[]" required data-loaded="false" list="behaelter-numbers-list" autocomplete="off" {% if form_data is defined and form_data.behaelter_numbers[i-1] is defined %} value="{{ form_data.behaelter_numbers[i-1] }}" {% if form_data.invalid_fields is defined and (i-1) in form_data.invalid_fields %} class="is-invalid" {% endif %} {% endif %} > {% if form_data is defined and form_data.invalid_fields is defined and (i-1) in form_data.invalid_fields %} <div id="behaelter-number-{{ i }}-validation" class="validation-message error"> Bitte korrigieren Sie dieses Feld </div> {% endif %} </fieldset> </div> <div class="grid-c-6"> <fieldset> <label for="member-number-{{ i }}"><strong>Mitgliedsnummer</strong></label> <input type="text" id="member-number-{{ i }}" name="member_numbers[]" value="{% if form_data is defined and form_data.member_numbers[i-1] is defined %}{{ form_data.member_numbers[i-1] }}{% else %}{{ current_member.memberno|default('') }}{% endif %}" placeholder="Mitgliedsnummer"> <p class="text-sm text-muted">Leer lassen für aktuelle Mitgliedsnummer: {{ current_member.memberno|default('Keine') }}</p> </fieldset> </div> </div> </div> {% endfor %} </div> <fieldset> <button id="checkin-submit" type="submit">Check-in durchführen</button> </fieldset> </form> </div> </div> {% if modal %}</div>{% endif %} {% endblock %} {% block modal %} {% if modal %} <style> .loading-spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(0, 0, 0, 0.1); border-left-color: #3498db; border-radius: 50%; animation: spin 1s linear infinite; vertical-align: middle; margin-right: 5px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } #numbers-loading { display: flex; align-items: center; margin: 10px 0; } /* Style for the special option "Nummer nicht bekannt" */ .special-option { font-weight: bold; color: #3498db; border-bottom: 1px dashed #ccc; margin-bottom: 5px; } /* Style for datalist options */ option { padding: 5px; font-size: 14px; } /* Style for input fields with datalist */ input[list] { background-color: #fff; border: 1px solid #ccc; border-radius: 4px; padding: 8px 12px; width: 100%; box-sizing: border-box; } input[list]:focus { border-color: #3498db; outline: none; box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); } /* Validation styles */ input[list].is-invalid { border-color: #dc3545; box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.2); } input[list].is-valid { border-color: #28a745; box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.2); } .validation-message { font-size: 12px; margin-top: 4px; display: none; } .validation-message.error { color: #dc3545; display: block; } </style> <script> (function ($) { // Store the initDynamicSelects function in a global scope for reuse window.initDynamicSelectsForCheckin = null; window.htmxSwapOccurred = false; // Add event listeners for HTMX events document.body.addEventListener('htmx:afterSwap', function(event) { // Check if the swapped element is our check-in form if (event.detail.target && event.detail.target.id && event.detail.target.id.startsWith('wa-checkin-')) { console.log('HTMX content swapped, reinitializing JavaScript'); // Set flag to indicate a swap occurred window.htmxSwapOccurred = true; // Use the stored function if available if (window.initDynamicSelectsForCheckin) { window.initDynamicSelectsForCheckin(); } else { console.error('initDynamicSelectsForCheckin function not available'); // Try to reinitialize the modal var id = event.detail.target.id.replace('wa-checkin-', ''); if (window.modals && window.modals.wa_checkins && window.modals.wa_checkins['details' + id]) { setTimeout(function() { window.modals.wa_checkins['details' + id].options.onOpen(); }, 100); } } } }); // Log HTMX request errors document.body.addEventListener('htmx:responseError', function(event) { console.error('HTMX response error:', event.detail.error, event.detail.xhr); }); // Log when HTMX sends a request document.body.addEventListener('htmx:beforeRequest', function(event) { console.log('HTMX sending request to:', event.detail.path); }); window.modals = window.modals || [] window.modals.wa_checkins = window.modals.wa_checkins || [] if (window.modals.wa_checkins.details{{ id }} === undefined) { window.modals.wa_checkins.details{{ id }} = new jBox('Modal', { closeButton: 'box', content: $('#wa-checkin-{{ id }}'), maxWidth: 650, minWidth: 100, minHeight: 100, width: 650, overlay: true, closeOnClick: false, zIndex: 'auto', addClass: '', onOpen: function() { // Initialize the dynamic select behavior initDynamicSelects(); } }).open(); } else { window.modals.wa_checkins.details{{ id }}.content.empty(); window.modals.wa_checkins.details{{ id }}.setContent($('#wa-checkin-{{ id }}')).open(); // Initialize the dynamic select behavior setTimeout(initDynamicSelects, 100); // Small delay to ensure DOM is ready } // Function to initialize the dynamic input behavior function initDynamicSelects() { console.log('Initializing dynamic selects for check-in form'); // Store this function in the global scope for reuse after HTMX swaps window.initDynamicSelectsForCheckin = initDynamicSelects; // Reset the numbersLoaded flag if this is being called after an HTMX swap if (window.htmxSwapOccurred) { console.log('HTMX swap detected, resetting numbersLoaded flag'); window.htmxSwapOccurred = false; numbersLoaded = false; // Force immediate loading of available numbers after HTMX swap setTimeout(function() { console.log('Forcing load of available numbers after HTMX swap'); loadAvailableNumbers(); }, 200); } // Cache DOM elements and data var $inputs = $('#wa-checkin-{{ id }} input[name="behaelter_numbers[]"]'); var $form = $('#wa-checkin-{{ id }} form'); var $datalist = $('#behaelter-numbers-list'); var allOptionsArray = []; // Store all available options as a flat array var inputsCount = $inputs.length; var previousValues = {}; // Track previous values to detect changes var isLoadingNumbers = false; // Flag to prevent multiple simultaneous requests var numbersLoaded = false; // Flag to track if numbers have been loaded var validationInProgress = false; // Flag to track if validation is in progress var validationResults = {}; // Store validation results for each input // Check if there are server-side validation errors var hasServerErrors = $inputs.filter('.is-invalid').length > 0; // If there are server-side errors, scroll to the first invalid field if (hasServerErrors) { var $firstInvalid = $inputs.filter('.is-invalid').first(); if ($firstInvalid.length) { setTimeout(function() { $firstInvalid[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); $firstInvalid.focus(); }, 500); } } // Load available numbers immediately when the modal is opened (generate once) if (!numbersLoaded) { loadAvailableNumbers(); } // Function to load available numbers via Ajax function loadAvailableNumbers() { console.log('Loading available numbers for check-in form'); if (isLoadingNumbers || numbersLoaded) { console.log('Numbers already loading or loaded, skipping'); return; } isLoadingNumbers = true; // Show loading animation $inputs.prop('disabled', true); $inputs.first().after('<div id="numbers-loading" class="text-sm text-muted"><div class="loading-spinner"></div> Lade verfügbare Nummern...</div>'); // Make Ajax request to get available numbers $.ajax({ url: '/_ajax/vr_wa/v1/slot', method: 'GET', data: { do: 'getAvailableNumbers', id: '{{ id }}', limit: 500 // Adjust this based on your needs }, dataType: 'json', success: function(response) { if (response.numbers && response.numbers.length > 0) { // Store the numbers in the allOptionsArray allOptionsArray = response.numbers.map(function(number) { // Special handling for the value 9999 if (number === '9999') { return { value: number, text: 'Nummer nicht bekannt', isSpecial: true }; } return { value: number, text: number }; }); // Clear the datalist $datalist.empty(); // Add options to the datalist allOptionsArray.forEach(function(option) { var $option = $('<option>', { value: option.value, class: option.isSpecial ? 'special-option' : '' }); // For the special option, add a label if (option.isSpecial) { $option.text('Nummer nicht bekannt'); } $datalist.append($option); }); // Mark all inputs as loaded $inputs.attr('data-loaded', 'true'); numbersLoaded = true; } else { // Handle case where no numbers are available $inputs.first().after('<div class="text-sm text-danger">Keine Nummern verfügbar</div>'); } }, error: function(xhr) { var errorMessage = 'Fehler beim Laden der Nummern'; try { var response = JSON.parse(xhr.responseText); if (response.error) { errorMessage = response.error; } } catch (e) {} $inputs.first().after('<div class="text-sm text-danger">' + errorMessage + '</div>'); }, complete: function() { // Remove loading indicator and re-enable inputs $('#numbers-loading').remove(); $inputs.prop('disabled', false); isLoadingNumbers = false; } }); } // Function to update the datalist when input values change function updateDatalist() { // If numbers haven't been loaded yet, don't update if (allOptionsArray.length === 0) { return; } // Show a brief loading animation var $loadingIndicator = $('<div class="loading-spinner" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9999;"></div>'); $('body').append($loadingIndicator); // Remove the loading indicator after a short delay setTimeout(function() { $loadingIndicator.remove(); }, 300); // Get all currently used values var usedValues = {}; $inputs.each(function() { var val = $(this).val(); if (val && val !== '9999') { // Allow multiple use of special value 9999 usedValues[val] = true; } }); // Clear the datalist $datalist.empty(); // First add the special option (9999) if it's available var specialOption = allOptionsArray.find(function(option) { return option.isSpecial; }); if (specialOption) { var $specialOption = $('<option>', { value: specialOption.value, text: specialOption.text, class: 'special-option' }); $datalist.append($specialOption); } // Then add all other available options allOptionsArray.forEach(function(option) { // Skip the special option (already added) and used values if (!option.isSpecial && !usedValues[option.value]) { var $option = $('<option>', { value: option.value }); $datalist.append($option); } }); } // Function to validate a single input field function validateInput($input) { var inputId = $input.attr('id'); var value = $input.val().trim(); // Remove any existing validation message $('#' + inputId + '-validation').remove(); // Reset validation classes $input.removeClass('is-valid is-invalid'); // If empty, mark as invalid but don't show message (HTML5 required will handle this) if (!value) { return Promise.resolve(false); } // Check for duplicate values (except for special value 9999) if (value !== '9999') { var duplicateFound = false; var duplicateIndex = -1; $inputs.each(function(index) { var $otherInput = $(this); // Skip the current input if ($otherInput.attr('id') === inputId) { return; } var otherValue = $otherInput.val().trim(); if (otherValue === value) { duplicateFound = true; duplicateIndex = index + 1; // +1 because index is zero-based return false; // Break the loop } }); if (duplicateFound) { $input.addClass('is-invalid'); var $fieldset = $input.closest('fieldset'); $fieldset.append('<div id="' + inputId + '-validation" class="validation-message error">Diese Nummer wird bereits für Behälter ' + duplicateIndex + ' verwendet.</div>'); // Store validation result validationResults[inputId] = false; return Promise.resolve(false); } } // Show loading indicator var $fieldset = $input.closest('fieldset'); var $loadingIndicator = $('<div class="loading-spinner" style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%);"></div>'); $fieldset.css('position', 'relative').append($loadingIndicator); // Make Ajax request to validate the number return $.ajax({ url: '/_ajax/vr_wa/v1/slot', method: 'GET', data: { do: 'validateNumber', id: '{{ id }}', number: value }, dataType: 'json' }).then(function(response) { // Remove loading indicator $loadingIndicator.remove(); // Store validation result validationResults[inputId] = response.valid; if (response.valid) { // Valid number $input.addClass('is-valid'); return true; } else { // Invalid number $input.addClass('is-invalid'); // Add validation message var message = response.message || 'Ungültige Nummer'; $fieldset.append('<div id="' + inputId + '-validation" class="validation-message error">' + message + '</div>'); return false; } }).catch(function() { // Remove loading indicator $loadingIndicator.remove(); // Mark as invalid on error $input.addClass('is-invalid'); $fieldset.append('<div id="' + inputId + '-validation" class="validation-message error">Fehler bei der Validierung</div>'); // Store validation result validationResults[inputId] = false; return false; }); } // Function to validate all inputs function validateAllInputs() { if (validationInProgress) { return Promise.reject('Validation already in progress'); } validationInProgress = true; // Disable submit button during validation $('#checkin-submit').prop('disabled', true).html('<div class="loading-spinner" style="display: inline-block;"></div> Validiere...'); // Create an array of promises for each input validation var validationPromises = []; $inputs.each(function() { var $input = $(this); validationPromises.push(validateInput($input)); }); // Wait for all validations to complete return Promise.all(validationPromises).then(function(results) { // Check if all inputs are valid var allValid = results.every(function(result) { return result === true; }); // Re-enable submit button $('#checkin-submit').prop('disabled', false).text('Check-in durchführen'); validationInProgress = false; return allValid; }).catch(function(error) { console.error('Validation error:', error); // Re-enable submit button $('#checkin-submit').prop('disabled', false).text('Check-in durchführen'); validationInProgress = false; return false; }); } // Remove dynamic datalist updates on change per requirements (datalist generated once at start) $inputs.off('change'); // Add focus event to all inputs to load numbers when first focused $inputs.on('focus', function() { if (!numbersLoaded) { loadAvailableNumbers(); } }); // Input event: no realtime validation and no dynamic datalist updates; only clear any previous validation state when user types $inputs.on('input', function() { var $input = $(this); var inputId = $input.attr('id'); var initialValue = $input.data('initial-value'); var currentValue = $input.val(); if (initialValue === undefined) { $input.data('initial-value', currentValue); initialValue = currentValue; } if (currentValue !== initialValue) { $input.removeClass('is-valid is-invalid'); $('#' + inputId + '-validation').remove(); } }); // Remove realtime validation: no validation on blur anymore per requirements $inputs.off('blur'); // Add form submit handler to validate all fields before submission $form.on('submit', function(e) { // Prevent the default form submission e.preventDefault(); // Remove any existing toast messages $form.find('.toast').remove(); // Validate all inputs validateAllInputs().then(function(isValid) { if (isValid) { // If all inputs are valid, submit the form using HTMX // Show loading state on submit button $('#checkin-submit').prop('disabled', true).html('<div class="loading-spinner" style="display: inline-block;"></div> Wird gespeichert...'); // Use HTMX to submit the form var formData = new FormData($form[0]); var url = $form.attr('hx-post') || $form.attr('action'); var target = $form.closest('[hx-target]').attr('hx-target'); var swap = $form.closest('[hx-swap]').attr('hx-swap'); console.log('Submitting form via HTMX to:', url); console.log('Target:', target); console.log('Swap:', swap); try { // Check if HTMX is available if (typeof htmx !== 'undefined') { htmx.ajax('POST', url, { target: target, swap: swap, values: formData, headers: { 'HX-Request': 'true' }, error: function(xhr, status) { console.error('HTMX form submission error:', status); $('#checkin-submit').prop('disabled', false).text('Check-in durchführen'); var $toast = $('<div class="toast toast--danger mx-0 mb-3"><p>Es ist ein Fehler bei der Übermittlung aufgetreten. Bitte versuchen Sie es erneut.</p></div>'); $form.prepend($toast); } }); } else { // Fallback to standard form submission if HTMX is not available console.warn('HTMX not available, falling back to standard form submission'); $form[0].submit(); } } catch (e) { console.error('HTMX form submission exception:', e); $('#checkin-submit').prop('disabled', false).text('Check-in durchführen'); var $toast = $('<div class="toast toast--danger mx-0 mb-3"><p>Es ist ein Fehler bei der Übermittlung aufgetreten. Bitte versuchen Sie es erneut.</p></div>'); $form.prepend($toast); } } else { // If any input is invalid, show a message var $toast = $('<div class="toast toast--danger mx-0 mb-3"><p>Bitte korrigieren Sie die markierten Felder.</p></div>'); $form.prepend($toast); // Scroll to the first invalid input var $firstInvalid = $inputs.filter('.is-invalid').first(); if ($firstInvalid.length) { $firstInvalid[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); $firstInvalid.focus(); } // Remove the toast after 5 seconds setTimeout(function() { $toast.fadeOut(function() { $(this).remove(); }); }, 5000); } }).catch(function(error) { console.error('Form validation error:', error); var $toast = $('<div class="toast toast--danger mx-0 mb-3"><p>Es ist ein Fehler bei der Validierung aufgetreten. Bitte versuchen Sie es erneut.</p></div>'); $form.prepend($toast); }); }); } })(jQuery); </script> {% endif %} {% endblock %}