Browse code

Use input fields with datalist instead of select fields to improve usability and performance

Benjamin Roth authored on28/08/2025 15:00:24
Showing2 changed files
... ...
@@ -60,6 +60,11 @@
60 60
 
61 61
                 <button type="button" id="load-numbers-btn" class="btn btn--sm btn-info mb-2">Nummern laden</button>
62 62
 
63
+                <!-- Datalist for all behaelter number inputs -->
64
+                <datalist id="behaelter-numbers-list">
65
+                    <!-- Options will be loaded via Ajax -->
66
+                </datalist>
67
+
63 68
                 <div class="grid-md u-gap-2">
64 69
                     {% for i in 1..checkin.behaelter %}
65 70
                         <div class="grid-c-12">
... ...
@@ -67,16 +72,27 @@
67 72
                                 <div class="grid-c-6">
68 73
                                     <fieldset>
69 74
                                         <label for="behaelter-number-{{ i }}"><strong>Behälter {{ i }}</strong><sup class="text-danger">*</sup></label>
70
-                                        <select id="behaelter-number-{{ i }}" name="behaelter_numbers[]" required data-loaded="false">
71
-                                            <option value="">-</option>
72
-                                            <!-- Options will be loaded via Ajax when the select is focused -->
73
-                                        </select>
75
+                                        <input type="text" id="behaelter-number-{{ i }}" name="behaelter_numbers[]" required data-loaded="false" list="behaelter-numbers-list" autocomplete="off"
76
+                                            {% if form_data is defined and form_data.behaelter_numbers[i-1] is defined %}
77
+                                                value="{{ form_data.behaelter_numbers[i-1] }}"
78
+                                                {% if form_data.invalid_fields is defined and (i-1) in form_data.invalid_fields %}
79
+                                                    class="is-invalid"
80
+                                                {% endif %}
81
+                                            {% endif %}
82
+                                        >
83
+                                        {% if form_data is defined and form_data.invalid_fields is defined and (i-1) in form_data.invalid_fields %}
84
+                                            <div id="behaelter-number-{{ i }}-validation" class="validation-message error">
85
+                                                Bitte korrigieren Sie dieses Feld
86
+                                            </div>
87
+                                        {% endif %}
74 88
                                     </fieldset>
75 89
                                 </div>
76 90
                                 <div class="grid-c-6">
77 91
                                     <fieldset>
78 92
                                         <label for="member-number-{{ i }}"><strong>Mitgliedsnummer</strong></label>
79
-                                        <input type="text" id="member-number-{{ i }}" name="member_numbers[]" value="{{ current_member.memberno|default('') }}" placeholder="Mitgliedsnummer">
93
+                                        <input type="text" id="member-number-{{ i }}" name="member_numbers[]"
94
+                                            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 %}"
95
+                                            placeholder="Mitgliedsnummer">
80 96
                                         <p class="text-sm text-muted">Leer lassen für aktuelle Mitgliedsnummer: {{ current_member.memberno|default('Keine') }}</p>
81 97
                                     </fieldset>
82 98
                                 </div>
... ...
@@ -127,9 +143,89 @@
127 143
                 border-bottom: 1px dashed #ccc;
128 144
                 margin-bottom: 5px;
129 145
             }
146
+
147
+            /* Style for datalist options */
148
+            option {
149
+                padding: 5px;
150
+                font-size: 14px;
151
+            }
152
+
153
+            /* Style for input fields with datalist */
154
+            input[list] {
155
+                background-color: #fff;
156
+                border: 1px solid #ccc;
157
+                border-radius: 4px;
158
+                padding: 8px 12px;
159
+                width: 100%;
160
+                box-sizing: border-box;
161
+            }
162
+
163
+            input[list]:focus {
164
+                border-color: #3498db;
165
+                outline: none;
166
+                box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
167
+            }
168
+
169
+            /* Validation styles */
170
+            input[list].is-invalid {
171
+                border-color: #dc3545;
172
+                box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.2);
173
+            }
174
+
175
+            input[list].is-valid {
176
+                border-color: #28a745;
177
+                box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.2);
178
+            }
179
+
180
+            .validation-message {
181
+                font-size: 12px;
182
+                margin-top: 4px;
183
+                display: none;
184
+            }
185
+
186
+            .validation-message.error {
187
+                color: #dc3545;
188
+                display: block;
189
+            }
130 190
         </style>
131 191
         <script>
132 192
             (function ($) {
193
+                // Store the initDynamicSelects function in a global scope for reuse
194
+                window.initDynamicSelectsForCheckin = null;
195
+                window.htmxSwapOccurred = false;
196
+
197
+                // Add event listeners for HTMX events
198
+                document.body.addEventListener('htmx:afterSwap', function(event) {
199
+                    // Check if the swapped element is our check-in form
200
+                    if (event.detail.target && event.detail.target.id && event.detail.target.id.startsWith('wa-checkin-')) {
201
+                        console.log('HTMX content swapped, reinitializing JavaScript');
202
+                        // Set flag to indicate a swap occurred
203
+                        window.htmxSwapOccurred = true;
204
+                        // Use the stored function if available
205
+                        if (window.initDynamicSelectsForCheckin) {
206
+                            window.initDynamicSelectsForCheckin();
207
+                        } else {
208
+                            console.error('initDynamicSelectsForCheckin function not available');
209
+                            // Try to reinitialize the modal
210
+                            var id = event.detail.target.id.replace('wa-checkin-', '');
211
+                            if (window.modals && window.modals.wa_checkins && window.modals.wa_checkins['details' + id]) {
212
+                                setTimeout(function() {
213
+                                    window.modals.wa_checkins['details' + id].options.onOpen();
214
+                                }, 100);
215
+                            }
216
+                        }
217
+                    }
218
+                });
219
+
220
+                // Log HTMX request errors
221
+                document.body.addEventListener('htmx:responseError', function(event) {
222
+                    console.error('HTMX response error:', event.detail.error, event.detail.xhr);
223
+                });
224
+
225
+                // Log when HTMX sends a request
226
+                document.body.addEventListener('htmx:beforeRequest', function(event) {
227
+                    console.log('HTMX sending request to:', event.detail.path);
228
+                });
133 229
 
134 230
                 window.modals = window.modals || []
135 231
                 window.modals.wa_checkins = window.modals.wa_checkins || []
... ...
@@ -159,28 +255,66 @@
159 255
                     setTimeout(initDynamicSelects, 100); // Small delay to ensure DOM is ready
160 256
                 }
161 257
 
162
-                // Function to initialize the dynamic select behavior
258
+                // Function to initialize the dynamic input behavior
163 259
                 function initDynamicSelects() {
260
+                    console.log('Initializing dynamic selects for check-in form');
261
+                    // Store this function in the global scope for reuse after HTMX swaps
262
+                    window.initDynamicSelectsForCheckin = initDynamicSelects;
263
+
264
+                    // Reset the numbersLoaded flag if this is being called after an HTMX swap
265
+                    if (window.htmxSwapOccurred) {
266
+                        console.log('HTMX swap detected, resetting numbersLoaded flag');
267
+                        window.htmxSwapOccurred = false;
268
+                        numbersLoaded = false;
269
+
270
+                        // Force immediate loading of available numbers after HTMX swap
271
+                        setTimeout(function() {
272
+                            console.log('Forcing load of available numbers after HTMX swap');
273
+                            loadAvailableNumbers();
274
+                        }, 200);
275
+                    }
164 276
                     // Cache DOM elements and data
165
-                    var $selects = $('#wa-checkin-{{ id }} select[name="behaelter_numbers[]"]');
277
+                    var $inputs = $('#wa-checkin-{{ id }} input[name="behaelter_numbers[]"]');
278
+                    var $form = $('#wa-checkin-{{ id }} form');
279
+                    var $datalist = $('#behaelter-numbers-list');
166 280
                     var allOptionsArray = []; // Store all available options as a flat array
167
-                    var selectsCount = $selects.length;
281
+                    var inputsCount = $inputs.length;
168 282
                     var previousValues = {}; // Track previous values to detect changes
169
-                    var optionElements = {}; // Pre-created option elements for better performance
170 283
                     var isLoadingNumbers = false; // Flag to prevent multiple simultaneous requests
171 284
                     var numbersLoaded = false; // Flag to track if numbers have been loaded
285
+                    var validationInProgress = false; // Flag to track if validation is in progress
286
+                    var validationResults = {}; // Store validation results for each input
287
+
288
+                    // Check if there are server-side validation errors
289
+                    var hasServerErrors = $inputs.filter('.is-invalid').length > 0;
290
+
291
+                    // If there are server-side errors, scroll to the first invalid field
292
+                    if (hasServerErrors) {
293
+                        var $firstInvalid = $inputs.filter('.is-invalid').first();
294
+                        if ($firstInvalid.length) {
295
+                            setTimeout(function() {
296
+                                $firstInvalid[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
297
+                                $firstInvalid.focus();
298
+                            }, 500);
299
+                        }
300
+                    }
301
+
302
+                    // Load available numbers immediately when the modal is opened
303
+                    loadAvailableNumbers();
172 304
 
173 305
                     // Function to load available numbers via Ajax
174 306
                     function loadAvailableNumbers() {
307
+                        console.log('Loading available numbers for check-in form');
175 308
                         if (isLoadingNumbers || numbersLoaded) {
309
+                            console.log('Numbers already loading or loaded, skipping');
176 310
                             return;
177 311
                         }
178 312
 
179 313
                         isLoadingNumbers = true;
180 314
 
181 315
                         // Show loading animation
182
-                        $selects.prop('disabled', true);
183
-                        $selects.first().after('<div id="numbers-loading" class="text-sm text-muted"><div class="loading-spinner"></div> Lade verfügbare Nummern...</div>');
316
+                        $inputs.prop('disabled', true);
317
+                        $inputs.first().after('<div id="numbers-loading" class="text-sm text-muted"><div class="loading-spinner"></div> Lade verfügbare Nummern...</div>');
184 318
 
185 319
                         // Make Ajax request to get available numbers
186 320
                         $.ajax({
... ...
@@ -210,24 +344,30 @@
210 344
                                         };
211 345
                                     });
212 346
 
213
-                                    // Pre-create option elements for better performance
347
+                                    // Clear the datalist
348
+                                    $datalist.empty();
349
+
350
+                                    // Add options to the datalist
214 351
                                     allOptionsArray.forEach(function(option) {
215
-                                        optionElements[option.value] = $('<option>', {
352
+                                        var $option = $('<option>', {
216 353
                                             value: option.value,
217
-                                            text: option.text,
218 354
                                             class: option.isSpecial ? 'special-option' : ''
219
-                                        })[0]; // Get the raw DOM element for better performance
220
-                                    });
355
+                                        });
221 356
 
222
-                                    // Update all selects with the available numbers
223
-                                    updateSelectOptions();
357
+                                        // For the special option, add a label
358
+                                        if (option.isSpecial) {
359
+                                            $option.text('Nummer nicht bekannt');
360
+                                        }
224 361
 
225
-                                    // Mark all selects as loaded
226
-                                    $selects.attr('data-loaded', 'true');
362
+                                        $datalist.append($option);
363
+                                    });
364
+
365
+                                    // Mark all inputs as loaded
366
+                                    $inputs.attr('data-loaded', 'true');
227 367
                                     numbersLoaded = true;
228 368
                                 } else {
229 369
                                     // Handle case where no numbers are available
230
-                                    $selects.first().after('<div class="text-sm text-danger">Keine Nummern verfügbar</div>');
370
+                                    $inputs.first().after('<div class="text-sm text-danger">Keine Nummern verfügbar</div>');
231 371
                                 }
232 372
                             },
233 373
                             error: function(xhr) {
... ...
@@ -239,128 +379,341 @@
239 379
                                     }
240 380
                                 } catch (e) {}
241 381
 
242
-                                $selects.first().after('<div class="text-sm text-danger">' + errorMessage + '</div>');
382
+                                $inputs.first().after('<div class="text-sm text-danger">' + errorMessage + '</div>');
243 383
                             },
244 384
                             complete: function() {
245
-                                // Remove loading indicator and re-enable selects
385
+                                // Remove loading indicator and re-enable inputs
246 386
                                 $('#numbers-loading').remove();
247
-                                $selects.prop('disabled', false);
387
+                                $inputs.prop('disabled', false);
248 388
                                 isLoadingNumbers = false;
249 389
                             }
250 390
                         });
251 391
                     }
252 392
 
253
-                    // Function to efficiently update select options
254
-                    function updateSelectOptions(changedSelect) {
393
+                    // Function to update the datalist when input values change
394
+                    function updateDatalist() {
255 395
                         // If numbers haven't been loaded yet, don't update
256 396
                         if (allOptionsArray.length === 0) {
257 397
                             return;
258 398
                         }
259 399
 
260
-                        // Show a brief loading animation when refreshing values
261
-                        if (changedSelect) {
262
-                            var $loadingIndicator = $('<div class="loading-spinner" style="position: absolute; right: 25px; top: 50%; transform: translateY(-50%);"></div>');
263
-                            $(changedSelect).parent().css('position', 'relative').append($loadingIndicator);
400
+                        // Show a brief loading animation
401
+                        var $loadingIndicator = $('<div class="loading-spinner" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9999;"></div>');
402
+                        $('body').append($loadingIndicator);
264 403
 
265
-                            // Remove the loading indicator after a short delay
266
-                            setTimeout(function() {
267
-                                $loadingIndicator.remove();
268
-                            }, 500);
269
-                        }
404
+                        // Remove the loading indicator after a short delay
405
+                        setTimeout(function() {
406
+                            $loadingIndicator.remove();
407
+                        }, 300);
270 408
 
271
-                        // Get all currently selected values
272
-                        var selectedValues = {};
273
-                        var hasChanges = false;
409
+                        // Get all currently used values
410
+                        var usedValues = {};
274 411
 
275
-                        $selects.each(function() {
412
+                        $inputs.each(function() {
276 413
                             var val = $(this).val();
277
-                            if (val) {
278
-                                selectedValues[val] = true;
414
+                            if (val && val !== '9999') { // Allow multiple use of special value 9999
415
+                                usedValues[val] = true;
279 416
                             }
280 417
                         });
281 418
 
282
-                        // Only update other selects if there's a change
283
-                        $selects.each(function() {
284
-                            var $select = $(this);
285
-                            var selectId = $select.attr('id');
286
-                            var currentValue = $select.val();
419
+                        // Clear the datalist
420
+                        $datalist.empty();
287 421
 
288
-                            // Skip the select that triggered the change and selects that don't need updating
289
-                            if (changedSelect && this === changedSelect && currentValue) {
290
-                                previousValues[selectId] = currentValue;
291
-                                return;
292
-                            }
422
+                        // First add the special option (9999) if it's available
423
+                        var specialOption = allOptionsArray.find(function(option) {
424
+                            return option.isSpecial;
425
+                        });
426
+
427
+                        if (specialOption) {
428
+                            var $specialOption = $('<option>', {
429
+                                value: specialOption.value,
430
+                                text: specialOption.text,
431
+                                class: 'special-option'
432
+                            });
433
+                            $datalist.append($specialOption);
434
+                        }
293 435
 
294
-                            // Check if we need to update this select
295
-                            var needsUpdate = false;
296
-                            if (changedSelect) {
297
-                                needsUpdate = true; // Always update on explicit change
298
-                            } else if (!previousValues[selectId] || previousValues[selectId] !== currentValue) {
299
-                                needsUpdate = true;
436
+                        // Then add all other available options
437
+                        allOptionsArray.forEach(function(option) {
438
+                            // Skip the special option (already added) and used values
439
+                            if (!option.isSpecial && !usedValues[option.value]) {
440
+                                var $option = $('<option>', {
441
+                                    value: option.value
442
+                                });
443
+                                $datalist.append($option);
300 444
                             }
445
+                        });
446
+                    }
301 447
 
302
-                            if (needsUpdate) {
303
-                                // Store current selection
304
-                                var currentSelection = currentValue;
448
+                    // Function to validate a single input field
449
+                    function validateInput($input) {
450
+                        var inputId = $input.attr('id');
451
+                        var value = $input.val().trim();
305 452
 
306
-                                // Efficiently update options
307
-                                var optionsFragment = document.createDocumentFragment();
308
-                                var emptyOption = $select.find('option:first').clone()[0];
309
-                                optionsFragment.appendChild(emptyOption);
453
+                        // Remove any existing validation message
454
+                        $('#' + inputId + '-validation').remove();
310 455
 
311
-                                // First add the special option (9999) if it's available
312
-                                var specialOption = null;
313
-                                var regularOptions = [];
456
+                        // Reset validation classes
457
+                        $input.removeClass('is-valid is-invalid');
314 458
 
315
-                                // Separate special option from regular options
316
-                                allOptionsArray.forEach(function(option) {
317
-                                    // Always include the special option (9999) or options that are either the current selection or not selected elsewhere
318
-                                    if (option.isSpecial || option.value === currentSelection || !selectedValues[option.value]) {
319
-                                        if (option.isSpecial) {
320
-                                            specialOption = option;
321
-                                        } else {
322
-                                            regularOptions.push(option);
323
-                                        }
324
-                                    }
325
-                                });
459
+                        // If empty, mark as invalid but don't show message (HTML5 required will handle this)
460
+                        if (!value) {
461
+                            return Promise.resolve(false);
462
+                        }
463
+
464
+                        // Check for duplicate values (except for special value 9999)
465
+                        if (value !== '9999') {
466
+                            var duplicateFound = false;
467
+                            var duplicateIndex = -1;
326 468
 
327
-                                // Add special option first (after empty option)
328
-                                if (specialOption) {
329
-                                    var specialOptionClone = optionElements[specialOption.value].cloneNode(true);
330
-                                    optionsFragment.appendChild(specialOptionClone);
469
+                            $inputs.each(function(index) {
470
+                                var $otherInput = $(this);
471
+                                // Skip the current input
472
+                                if ($otherInput.attr('id') === inputId) {
473
+                                    return;
331 474
                                 }
332 475
 
333
-                                // Then add all other options
334
-                                regularOptions.forEach(function(option) {
335
-                                    var optionClone = optionElements[option.value].cloneNode(true);
336
-                                    optionsFragment.appendChild(optionClone);
337
-                                });
476
+                                var otherValue = $otherInput.val().trim();
477
+                                if (otherValue === value) {
478
+                                    duplicateFound = true;
479
+                                    duplicateIndex = index + 1; // +1 because index is zero-based
480
+                                    return false; // Break the loop
481
+                                }
482
+                            });
338 483
 
339
-                                // Replace all options at once for better performance
340
-                                $select.empty()[0].appendChild(optionsFragment);
484
+                            if (duplicateFound) {
485
+                                $input.addClass('is-invalid');
486
+                                var $fieldset = $input.closest('fieldset');
487
+                                $fieldset.append('<div id="' + inputId + '-validation" class="validation-message error">Diese Nummer wird bereits für Behälter ' + duplicateIndex + ' verwendet.</div>');
341 488
 
342
-                                // Restore current selection
343
-                                if (currentSelection) {
344
-                                    $select.val(currentSelection);
345
-                                }
489
+                                // Store validation result
490
+                                validationResults[inputId] = false;
491
+                                return Promise.resolve(false);
492
+                            }
493
+                        }
346 494
 
347
-                                previousValues[selectId] = currentSelection;
495
+                        // Show loading indicator
496
+                        var $fieldset = $input.closest('fieldset');
497
+                        var $loadingIndicator = $('<div class="loading-spinner" style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%);"></div>');
498
+                        $fieldset.css('position', 'relative').append($loadingIndicator);
499
+
500
+                        // Make Ajax request to validate the number
501
+                        return $.ajax({
502
+                            url: '/_ajax/vr_wa/v1/slot',
503
+                            method: 'GET',
504
+                            data: {
505
+                                do: 'validateNumber',
506
+                                id: '{{ id }}',
507
+                                number: value
508
+                            },
509
+                            dataType: 'json'
510
+                        }).then(function(response) {
511
+                            // Remove loading indicator
512
+                            $loadingIndicator.remove();
513
+
514
+                            // Store validation result
515
+                            validationResults[inputId] = response.valid;
516
+
517
+                            if (response.valid) {
518
+                                // Valid number
519
+                                $input.addClass('is-valid');
520
+                                return true;
521
+                            } else {
522
+                                // Invalid number
523
+                                $input.addClass('is-invalid');
524
+
525
+                                // Add validation message
526
+                                var message = response.message || 'Ungültige Nummer';
527
+                                $fieldset.append('<div id="' + inputId + '-validation" class="validation-message error">' + message + '</div>');
528
+
529
+                                return false;
348 530
                             }
531
+                        }).catch(function() {
532
+                            // Remove loading indicator
533
+                            $loadingIndicator.remove();
534
+
535
+                            // Mark as invalid on error
536
+                            $input.addClass('is-invalid');
537
+                            $fieldset.append('<div id="' + inputId + '-validation" class="validation-message error">Fehler bei der Validierung</div>');
538
+
539
+                            // Store validation result
540
+                            validationResults[inputId] = false;
541
+
542
+                            return false;
543
+                        });
544
+                    }
545
+
546
+                    // Function to validate all inputs
547
+                    function validateAllInputs() {
548
+                        if (validationInProgress) {
549
+                            return Promise.reject('Validation already in progress');
550
+                        }
551
+
552
+                        validationInProgress = true;
553
+
554
+                        // Disable submit button during validation
555
+                        $('#checkin-submit').prop('disabled', true).html('<div class="loading-spinner" style="display: inline-block;"></div> Validiere...');
556
+
557
+                        // Create an array of promises for each input validation
558
+                        var validationPromises = [];
559
+
560
+                        $inputs.each(function() {
561
+                            var $input = $(this);
562
+                            validationPromises.push(validateInput($input));
563
+                        });
564
+
565
+                        // Wait for all validations to complete
566
+                        return Promise.all(validationPromises).then(function(results) {
567
+                            // Check if all inputs are valid
568
+                            var allValid = results.every(function(result) {
569
+                                return result === true;
570
+                            });
571
+
572
+                            // Re-enable submit button
573
+                            $('#checkin-submit').prop('disabled', false).text('Check-in durchführen');
574
+
575
+                            validationInProgress = false;
576
+                            return allValid;
577
+                        }).catch(function(error) {
578
+                            console.error('Validation error:', error);
579
+
580
+                            // Re-enable submit button
581
+                            $('#checkin-submit').prop('disabled', false).text('Check-in durchführen');
582
+
583
+                            validationInProgress = false;
584
+                            return false;
349 585
                         });
350 586
                     }
351 587
 
352
-                    // Add change event to all selects
353
-                    $selects.on('change', function() {
354
-                        updateSelectOptions(this);
588
+                    // Add change event to all inputs
589
+                    $inputs.on('change', function() {
590
+                        updateDatalist();
355 591
                     });
356 592
 
357
-                    // Add focus event to all selects to load numbers when first focused
358
-                    $selects.on('focus', function() {
593
+                    // Add focus event to all inputs to load numbers when first focused
594
+                    $inputs.on('focus', function() {
359 595
                         if (!numbersLoaded) {
360 596
                             loadAvailableNumbers();
361 597
                         }
362 598
                     });
363 599
 
600
+                    // Add input event to handle typing in the fields
601
+                    $inputs.on('input', function() {
602
+                        // Update the datalist when the user types in the field
603
+                        // Use a small delay to avoid too frequent updates
604
+                        clearTimeout($(this).data('inputTimer'));
605
+                        $(this).data('inputTimer', setTimeout(updateDatalist, 300));
606
+
607
+                        // Only remove validation classes and messages if this is not a server-side error
608
+                        // or if the user has changed the value
609
+                        var $input = $(this);
610
+                        var inputId = $input.attr('id');
611
+                        var initialValue = $input.data('initial-value');
612
+                        var currentValue = $input.val();
613
+
614
+                        // Store initial value on first input
615
+                        if (initialValue === undefined) {
616
+                            $input.data('initial-value', currentValue);
617
+                            initialValue = currentValue;
618
+                        }
619
+
620
+                        // If value has changed from initial value, remove validation classes
621
+                        if (currentValue !== initialValue) {
622
+                            $input.removeClass('is-valid is-invalid');
623
+                            $('#' + inputId + '-validation').remove();
624
+                        }
625
+                    });
626
+
627
+                    // Add blur event to validate when field loses focus
628
+                    $inputs.on('blur', function() {
629
+                        var $input = $(this);
630
+                        var value = $input.val().trim();
631
+
632
+                        // Only validate if the field has a value
633
+                        if (value) {
634
+                            validateInput($input);
635
+                        }
636
+                    });
637
+
638
+                    // Add form submit handler to validate all fields before submission
639
+                    $form.on('submit', function(e) {
640
+                        // Prevent the default form submission
641
+                        e.preventDefault();
642
+
643
+                        // Remove any existing toast messages
644
+                        $form.find('.toast').remove();
645
+
646
+                        // Validate all inputs
647
+                        validateAllInputs().then(function(isValid) {
648
+                            if (isValid) {
649
+                                // If all inputs are valid, submit the form using HTMX
650
+                                // Show loading state on submit button
651
+                                $('#checkin-submit').prop('disabled', true).html('<div class="loading-spinner" style="display: inline-block;"></div> Wird gespeichert...');
652
+
653
+                                // Use HTMX to submit the form
654
+                                var formData = new FormData($form[0]);
655
+                                var url = $form.attr('hx-post') || $form.attr('action');
656
+                                var target = $form.closest('[hx-target]').attr('hx-target');
657
+                                var swap = $form.closest('[hx-swap]').attr('hx-swap');
658
+
659
+                                console.log('Submitting form via HTMX to:', url);
660
+                                console.log('Target:', target);
661
+                                console.log('Swap:', swap);
662
+
663
+                                try {
664
+                                    // Check if HTMX is available
665
+                                    if (typeof htmx !== 'undefined') {
666
+                                        htmx.ajax('POST', url, {
667
+                                            target: target,
668
+                                            swap: swap,
669
+                                            values: formData,
670
+                                            headers: {
671
+                                                'HX-Request': 'true'
672
+                                            },
673
+                                            error: function(xhr, status) {
674
+                                                console.error('HTMX form submission error:', status);
675
+                                                $('#checkin-submit').prop('disabled', false).text('Check-in durchführen');
676
+                                                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>');
677
+                                                $form.prepend($toast);
678
+                                            }
679
+                                        });
680
+                                    } else {
681
+                                        // Fallback to standard form submission if HTMX is not available
682
+                                        console.warn('HTMX not available, falling back to standard form submission');
683
+                                        $form[0].submit();
684
+                                    }
685
+                                } catch (e) {
686
+                                    console.error('HTMX form submission exception:', e);
687
+                                    $('#checkin-submit').prop('disabled', false).text('Check-in durchführen');
688
+                                    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>');
689
+                                    $form.prepend($toast);
690
+                                }
691
+                            } else {
692
+                                // If any input is invalid, show a message
693
+                                var $toast = $('<div class="toast toast--danger mx-0 mb-3"><p>Bitte korrigieren Sie die markierten Felder.</p></div>');
694
+                                $form.prepend($toast);
695
+
696
+                                // Scroll to the first invalid input
697
+                                var $firstInvalid = $inputs.filter('.is-invalid').first();
698
+                                if ($firstInvalid.length) {
699
+                                    $firstInvalid[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
700
+                                    $firstInvalid.focus();
701
+                                }
702
+
703
+                                // Remove the toast after 5 seconds
704
+                                setTimeout(function() {
705
+                                    $toast.fadeOut(function() {
706
+                                        $(this).remove();
707
+                                    });
708
+                                }, 5000);
709
+                            }
710
+                        }).catch(function(error) {
711
+                            console.error('Form validation error:', error);
712
+                            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>');
713
+                            $form.prepend($toast);
714
+                        });
715
+                    });
716
+
364 717
                     // Add click handler to the load numbers button
365 718
                     $('#load-numbers-btn').on('click', function(e) {
366 719
                         e.preventDefault();
... ...
@@ -112,6 +112,9 @@ class SlotAjaxController extends AbstractController
112 112
 
113 113
             case 'getAvailableNumbers':
114 114
                 return $this->getAvailableNumbers();
115
+
116
+            case 'validateNumber':
117
+                return $this->validateNumber();
115 118
         }
116 119
 
117 120
         return new Response('',500);
... ...
@@ -684,7 +687,7 @@ class SlotAjaxController extends AbstractController
684 687
         return $this->render('@Contao/modal_unauthorized.html.twig');
685 688
     }
686 689
 
687
-    protected function renderCheckin(bool $blnModal=true, string $error=null)
690
+    protected function renderCheckin(bool $blnModal=true, string $error=null, array $formData=null)
688 691
     {
689 692
         $insertTagService = Controller::getContainer()->get('contao.insert_tag.parser');
690 693
 
... ...
@@ -770,6 +773,11 @@ class SlotAjaxController extends AbstractController
770 773
             'current_member' => $currentMemberModel ? $currentMemberModel->row() : null
771 774
         ]);
772 775
 
776
+        // Add form data if provided (to preserve values after validation errors)
777
+        if ($formData !== null) {
778
+            $arrData['form_data'] = $formData;
779
+        }
780
+
773 781
         if (!empty($error))
774 782
         {
775 783
             $arrData['toast'] = $error;
... ...
@@ -795,7 +803,12 @@ class SlotAjaxController extends AbstractController
795 803
         $behaelterNumbers = Input::post('behaelter_numbers');
796 804
         if (!is_array($behaelterNumbers) || count($behaelterNumbers) != $Booking->behaelter)
797 805
         {
798
-            return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Bitte wählen Sie für jeden Behälter eine Nummer aus.</div>');
806
+            // Prepare form data to preserve input values
807
+            $formData = [
808
+                'behaelter_numbers' => $behaelterNumbers ?: [],
809
+                'member_numbers' => Input::post('member_numbers') ?: []
810
+            ];
811
+            return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Bitte wählen Sie für jeden Behälter eine Nummer aus.</div>', $formData);
799 812
         }
800 813
 
801 814
         // Get member numbers from the form
... ...
@@ -819,7 +832,98 @@ class SlotAjaxController extends AbstractController
819 832
         // Check for duplicate numbers (excluding the special value 9999)
820 833
         if (count(array_unique($numbersForDuplicateCheck)) != count($numbersForDuplicateCheck))
821 834
         {
822
-            return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Jede Nummer kann nur einmal verwendet werden.</div>');
835
+            // Prepare form data to preserve input values
836
+            $formData = [
837
+                'behaelter_numbers' => $behaelterNumbers,
838
+                'member_numbers' => $memberNumbers
839
+            ];
840
+            return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Jede Nummer kann nur einmal verwendet werden.</div>', $formData);
841
+        }
842
+
843
+        // Validate all numbers on the server side as a final check
844
+        $invalidNumbers = [];
845
+        $currentTime = time();
846
+        $Slot = $Booking->getRelated('pid');
847
+        $Standort = $Slot->getRelated('pid');
848
+
849
+        // Get all used numbers from current bookings (excluding past bookings)
850
+        $usedNumbers = [];
851
+
852
+        // Get the database connection
853
+        $db = Controller::getContainer()->get('database_connection');
854
+
855
+        // Query to get used numbers from current bookings
856
+        $sql = "SELECT r.behaelter_numbers
857
+                FROM tl_vr_wa_reservation r
858
+                JOIN tl_vr_wa_slot s ON r.pid = s.id
859
+                WHERE r.behaelter_numbers != ''
860
+                AND s.time >= ?
861
+                AND r.id != ?
862
+                AND s.pid = ?"; // Only check for the same standort
863
+
864
+        $stmt = $db->prepare($sql);
865
+        $stmt->bindValue(1, $currentTime);
866
+        $stmt->bindValue(2, $Booking->id);
867
+        $stmt->bindValue(3, $Standort->id);
868
+        $result = $stmt->executeQuery();
869
+
870
+        while ($row = $result->fetchAssociative()) {
871
+            $numbers = json_decode($row['behaelter_numbers'], true);
872
+            if (is_array($numbers)) {
873
+                foreach ($numbers as $item) {
874
+                    $usedNumbers[] = isset($item['behaelter']) ? $item['behaelter'] : $item;
875
+                }
876
+            }
877
+        }
878
+
879
+        // Check each number
880
+        foreach ($behaelterNumbers as $index => $number) {
881
+            // Skip the special value 9999
882
+            if ($number === '9999') {
883
+                continue;
884
+            }
885
+
886
+            // Check if the number is numeric
887
+            if (!is_numeric($number)) {
888
+                $invalidNumbers[] = "Behälter " . ($index + 1) . ": Die eingegebene Nummer ist keine gültige Zahl.";
889
+                continue;
890
+            }
891
+
892
+            // Check if the number is already in use
893
+            if (in_array($number, $usedNumbers)) {
894
+                $invalidNumbers[] = "Behälter " . ($index + 1) . ": Diese Nummer wird bereits in einer anderen aktiven Buchung verwendet.";
895
+                continue;
896
+            }
897
+
898
+            // Check if the number is within the valid ranges for this standort
899
+            $validRanges = $Standort->extractNumbersFromRanges([], 10000); // Get all possible numbers
900
+            if (!in_array($number, $validRanges) && !empty($validRanges)) {
901
+                $invalidNumbers[] = "Behälter " . ($index + 1) . ": Die eingegebene Nummer liegt nicht im gültigen Bereich für diesen Standort.";
902
+            }
903
+        }
904
+
905
+        // If there are invalid numbers, return an error
906
+        if (!empty($invalidNumbers)) {
907
+            $errorMessage = '<div class="toast toast--danger mx-0"><p>Folgende Fehler wurden gefunden:</p><ul>';
908
+            foreach ($invalidNumbers as $error) {
909
+                $errorMessage .= '<li>' . $error . '</li>';
910
+            }
911
+            $errorMessage .= '</ul></div>';
912
+
913
+            // Prepare form data to preserve input values
914
+            $formData = [
915
+                'behaelter_numbers' => $behaelterNumbers,
916
+                'member_numbers' => $memberNumbers,
917
+                'invalid_fields' => array_map(function($error) {
918
+                    // Extract the behälter number from the error message
919
+                    if (preg_match('/Behälter (\d+):/', $error, $matches)) {
920
+                        return (int)$matches[1] - 1; // Convert to zero-based index
921
+                    }
922
+                    return null;
923
+                }, $invalidNumbers)
924
+            ];
925
+
926
+            return $this->renderCheckin(false, $errorMessage, $formData);
823 927
         }
824 928
 
825 929
         // Create combined array with behaelter numbers and member numbers
... ...
@@ -926,4 +1030,91 @@ class SlotAjaxController extends AbstractController
926 1030
         // Return the numbers as JSON
927 1031
         return new Response(json_encode(['numbers' => $availableNumbers]), 200, ['Content-Type' => 'application/json']);
928 1032
     }
1033
+
1034
+    protected function validateNumber()
1035
+    {
1036
+        if (empty($_REQUEST['id']) || !isset($_REQUEST['number']))
1037
+        {
1038
+            return new Response(json_encode(['valid' => false, 'message' => 'Required parameters missing']), 412, ['Content-Type' => 'application/json']);
1039
+        }
1040
+
1041
+        $number = $_REQUEST['number'];
1042
+
1043
+        // Special case: "Nummer nicht bekannt" is always valid
1044
+        if ($number === '9999') {
1045
+            return new Response(json_encode(['valid' => true]), 200, ['Content-Type' => 'application/json']);
1046
+        }
1047
+
1048
+        // Check if the number is a valid number format
1049
+        if (!is_numeric($number)) {
1050
+            return new Response(json_encode([
1051
+                'valid' => false,
1052
+                'message' => 'Die eingegebene Nummer ist keine gültige Zahl.'
1053
+            ]), 200, ['Content-Type' => 'application/json']);
1054
+        }
1055
+
1056
+        /** @var WeinanlieferungReservationModel $Booking */
1057
+        if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null || ($Slot = $Booking->getRelated('pid')) === null)
1058
+        {
1059
+            return new Response(json_encode(['valid' => false, 'message' => 'Could not load booking data']), 500, ['Content-Type' => 'application/json']);
1060
+        }
1061
+
1062
+        // Get the standort to access the number_ranges
1063
+        $Standort = $Slot->getRelated('pid');
1064
+        if ($Standort === null)
1065
+        {
1066
+            return new Response(json_encode(['valid' => false, 'message' => 'Could not load standort data']), 500, ['Content-Type' => 'application/json']);
1067
+        }
1068
+
1069
+        // Get all used numbers from current bookings (excluding past bookings)
1070
+        $usedNumbers = [];
1071
+        $currentTime = time();
1072
+
1073
+        // Get the database connection
1074
+        $db = Controller::getContainer()->get('database_connection');
1075
+
1076
+        // Query to get used numbers from current bookings
1077
+        $sql = "SELECT r.behaelter_numbers
1078
+                FROM tl_vr_wa_reservation r
1079
+                JOIN tl_vr_wa_slot s ON r.pid = s.id
1080
+                WHERE r.behaelter_numbers != ''
1081
+                AND s.time >= ?
1082
+                AND r.id != ?
1083
+                AND s.pid = ?"; // Only check for the same standort
1084
+
1085
+        $stmt = $db->prepare($sql);
1086
+        $stmt->bindValue(1, $currentTime);
1087
+        $stmt->bindValue(2, $Booking->id);
1088
+        $stmt->bindValue(3, $Standort->id);
1089
+        $result = $stmt->executeQuery();
1090
+
1091
+        while ($row = $result->fetchAssociative()) {
1092
+            $numbers = json_decode($row['behaelter_numbers'], true);
1093
+            if (is_array($numbers)) {
1094
+                foreach ($numbers as $item) {
1095
+                    $usedNumbers[] = isset($item['behaelter']) ? $item['behaelter'] : $item;
1096
+                }
1097
+            }
1098
+        }
1099
+
1100
+        // Check if the number is already in use
1101
+        if (in_array($number, $usedNumbers)) {
1102
+            return new Response(json_encode([
1103
+                'valid' => false,
1104
+                'message' => 'Diese Nummer wird bereits in einer anderen aktiven Buchung verwendet.'
1105
+            ]), 200, ['Content-Type' => 'application/json']);
1106
+        }
1107
+
1108
+        // Check if the number is within the valid ranges for this standort
1109
+        $validRanges = $Standort->extractNumbersFromRanges([], 10000); // Get all possible numbers
1110
+        if (!in_array($number, $validRanges) && !empty($validRanges)) {
1111
+            return new Response(json_encode([
1112
+                'valid' => false,
1113
+                'message' => 'Die eingegebene Nummer liegt nicht im gültigen Bereich für diesen Standort.'
1114
+            ]), 200, ['Content-Type' => 'application/json']);
1115
+        }
1116
+
1117
+        // If we got here, the number is valid
1118
+        return new Response(json_encode(['valid' => true]), 200, ['Content-Type' => 'application/json']);
1119
+    }
929 1120
 }