{% 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-behaelter-outline"></i>
<div class="t-label">Erforderliche Eingaben</div>
</div>
<div class="">{{ checkin.expected }}</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">Einheit</div>
</div>
{% set uTitle = checkin.unit_title|default('Behälter') %}
{% set uAmount = checkin.unit_amount_display|default(checkin.behaelter) %}
<div class="">{{ uAmount }} × {{ uTitle }}</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">
{% set expected = checkin.expected|default(checkin.behaelter) %}
{% for i in 1..expected %}
<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 %}