OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

loan-devices.js

29 KB
/* OpenNIT – Leihgeräte JS (extern eingebunden, nicht inline) */
(function () {
    'use strict';

    function initLoanDevices() {
        var wrap = document.getElementById('opennit-loan-wrap');
        if (!wrap || wrap._opennitInit) return;
        wrap._opennitInit = true;

        var ajaxUrl        = wrap.dataset.ajaxUrl        || '';
        var nonce          = wrap.dataset.nonce          || '';
        var txtAvailable   = wrap.dataset.textAvailable  || 'Verfügbar';
        var txtRequest     = wrap.dataset.textRequest    || 'Anfragen';
        var txtContactIt   = wrap.dataset.textContactIt  || 'Bitte IT-Team direkt kontaktieren';
        var maxAdvanceDays = parseInt(wrap.dataset.maxAdvanceDays  || '90', 10);
        var maxDurationDays= parseInt(wrap.dataset.maxDurationDays || '14', 10);
        var errTooFarAhead = wrap.dataset.errorTooFarAhead || 'Das gewählte Datum liegt zu weit in der Zukunft.';
        var errTooLong     = wrap.dataset.errorTooLong     || 'Der gewählte Zeitraum ist zu lang.';

        // ----------------------------------------------------------------
        // Filter
        // ----------------------------------------------------------------

        window.opennitFilterDevices = function () {
            var type     = document.getElementById('opennit-f-type').value;
            var dateFrom = document.getElementById('opennit-f-from').value;
            var dateTo   = document.getElementById('opennit-f-to').value;

            if ((dateFrom && !dateTo) || (!dateFrom && dateTo)) {
                opennitFilterStatus('Bitte beide Datumsfelder (Von und Bis) ausfüllen.', 'error');
                return;
            }
            if (dateFrom && dateTo && dateFrom > dateTo) {
                opennitFilterStatus('Das Bis-Datum muss nach dem Von-Datum liegen.', 'error');
                return;
            }

            var statusEl = document.getElementById('opennit-filter-status');
            statusEl.hidden = false;
            statusEl.className = 'opennit-filter-status opennit-filter-loading';
            statusEl.textContent = 'Suche läuft…';

            var formData = new FormData();
            formData.append('action',    'opennit_filter_devices');
            formData.append('nonce',     nonce);
            formData.append('type',      type);
            formData.append('date_from', dateFrom);
            formData.append('date_to',   dateTo);

            fetch(ajaxUrl, { method: 'POST', body: formData })
                .then(function (r) { return r.json(); })
                .then(function (data) {
                    if (!data.success) {
                        opennitFilterStatus(data.data || 'Fehler beim Laden.', 'error');
                        return;
                    }
                    var devices = data.data.devices || [];
                    var msg = '';
                    if (dateFrom && dateTo) {
                        var df = dateFrom.split('-').reverse().join('.');
                        var dt = dateTo.split('-').reverse().join('.');
                        msg = devices.length
                            ? devices.length + ' Gerät' + (devices.length !== 1 ? 'e' : '') + ' verfügbar vom ' + df + ' bis ' + dt
                            : 'Kein Gerät verfügbar vom ' + df + ' bis ' + dt;
                        if (type) msg += ' (Typ: ' + type + ')';
                    } else if (type) {
                        msg = devices.length + ' Gerät' + (devices.length !== 1 ? 'e' : '') + ' gefunden (Typ: ' + type + ')';
                    }
                    opennitFilterStatus(msg + (msg ? ' – <a href="#" onclick="opennitResetFilter(event)">Alle anzeigen</a>' : ''), 'info');
                    opennitRenderDevices(devices, dateFrom, dateTo);
                })
                .catch(function () {
                    opennitFilterStatus('Netzwerkfehler. Bitte erneut versuchen.', 'error');
                });
        };

        window.opennitResetFilter = function (e) {
            if (e) e.preventDefault();
            document.getElementById('opennit-f-type').value = '';
            document.getElementById('opennit-f-from').value = '';
            document.getElementById('opennit-f-to').value   = '';
            document.getElementById('opennit-filter-status').hidden = true;
            window.opennitFilterDevices();
        };

        function opennitFilterStatus(html, type) {
            var el = document.getElementById('opennit-filter-status');
            el.hidden    = !html;
            el.className = 'opennit-filter-status opennit-filter-' + type;
            el.innerHTML = html;
        }

        // ----------------------------------------------------------------
        // Gerätekarten dynamisch rendern (nach Filter-AJAX)
        // ----------------------------------------------------------------

        function opennitRenderDevices(devices, dateFrom, dateTo) {
            var container = document.getElementById('opennit-devices-container');
            if (!container) return;

            if (!devices.length) {
                container.innerHTML = '<div class="opennit-notice opennit-notice-info">Kein passendes Gerät gefunden.</div>';
                return;
            }

            var html = '<div class="opennit-devices-list" id="opennit-devices-list">';
            devices.forEach(function (d) {
                var id          = parseInt(d.id, 10);
                var isAvail     = d.status === 'available';
                var requestable = d.wp_requestable == 1 || d.wp_requestable === true || d.wp_requestable === '1';

                html += '<div class="opennit-device-item" id="opennit-device-' + id + '">';
                if (d.image_url) {
                    html += '<div class="opennit-device-image"><img src="' + opennitEsc(d.image_url)
                         + '" alt="' + opennitEsc(d.title) + '" loading="lazy"></div>';
                }
                html += '<div class="opennit-device-info">';
                html += '<div class="opennit-device-title-row">';
                html += '<span class="opennit-device-name">' + opennitEsc(d.title) + '</span>';
                if (d.type) html += '<span class="opennit-device-type">' + opennitEsc(d.type) + '</span>';
                html += '</div>';
                if (d.inventory_number) html += '<span class="opennit-device-inv">' + opennitEsc(d.inventory_number) + '</span>';
                if (d.description)      html += '<p class="opennit-device-desc">'   + opennitEsc(d.description) + '</p>';
                html += '</div>';

                html += '<div class="opennit-device-status-col">';
                if (isAvail) {
                    html += '<span class="opennit-badge opennit-badge-available">' + opennitEsc(txtAvailable) + '</span>';
                } else if (d.occupied_until) {
                    html += '<span class="opennit-badge opennit-badge-occupied">Belegt bis ' + opennitFmtDate(d.occupied_until) + '</span>';
                } else {
                    var lbl = { lent: 'Ausgeliehen', reserved: 'Reserviert', maintenance: 'In Wartung', retired: 'Ausgemustert' };
                    html += '<span class="opennit-badge opennit-badge-occupied">' + opennitEsc(lbl[d.status] || d.status) + '</span>';
                }
                if (isAvail && requestable) {
                    html += '<button type="button" class="opennit-btn opennit-btn-request"'
                         + ' data-device-id="' + id + '" data-device-name="' + opennitEsc(d.title)
                         + '" data-date-from="' + opennitEsc(dateFrom) + '" data-date-to="' + opennitEsc(dateTo)
                         + '" aria-expanded="false" onclick="opennitOpenForm(this)">'
                         + (dateFrom && dateTo ? 'Reservieren' : opennitEsc(txtRequest)) + '</button>';
                } else if (isAvail && !requestable) {
                    html += '<span class="opennit-hint">' + opennitEsc(txtContactIt) + '</span>';
                }
                html += '</div></div>';

                if (isAvail && requestable) {
                    var maxDateStr = opennitAddDays(opennitToday(), maxAdvanceDays);
                    html += '<div class="opennit-request-form" id="opennit-form-' + id + '" hidden>';
                    html += '<form class="opennit-form" data-device-id="' + id + '" onsubmit="opennitSubmitForm(event, this)">';
                    html += '<h3 class="opennit-form-title">Leihgerät anfragen: <em>' + opennitEsc(d.title) + '</em></h3>';
                    html += '<div class="opennit-accessory-note">&#128230; Die Reservierung umfasst das Gerät inklusive des gesamten Zubehörs (Netzteil, Kabel, Tasche u. ä.).</div>';
                    html += '<div class="opennit-calendar-wrap" id="opennit-cal-' + id + '"><div class="opennit-cal-loading">Verfügbarkeit wird geladen…</div></div>';
                    html += '<div class="opennit-form-row">';
                    html += '<div class="opennit-form-group"><label>Von</label>'
                         + '<input type="date" id="opennit-start-' + id + '" name="planned_start" min="' + opennitToday() + '" max="' + maxDateStr + '"'
                         + (dateFrom ? ' value="' + opennitEsc(dateFrom) + '"' : '') + ' required></div>';
                    html += '<div class="opennit-form-group"><label>Bis</label>'
                         + '<input type="date" id="opennit-end-' + id + '" name="planned_end" min="' + opennitToday() + '" max="' + maxDateStr + '"'
                         + (dateTo ? ' value="' + opennitEsc(dateTo) + '"' : '') + ' required></div>';
                    html += '</div>';
                    html += '<div class="opennit-form-group"><label>Verwendungszweck</label>'
                         + '<textarea name="purpose" rows="2" placeholder="Wofür benötigen Sie das Gerät?"></textarea></div>';
                    html += '<div class="opennit-form-actions">';
                    html += '<button type="submit" class="opennit-btn opennit-btn-primary">Anfrage senden</button>';
                    html += '<button type="button" class="opennit-btn opennit-btn-cancel" onclick="opennitCloseForm(' + id + ')">Abbrechen</button>';
                    html += '</div>';
                    html += '<div class="opennit-form-feedback" id="opennit-feedback-' + id + '" hidden></div>';
                    html += '</form></div>';
                }
            });
            html += '</div>';
            container.innerHTML = html;
            initDateSync();
        }

        // ----------------------------------------------------------------
        // Kalender-Widget
        // ----------------------------------------------------------------

        var deviceSlotsCache = {};

        function opennitLoadCalendar(deviceId) {
            var calWrap = document.getElementById('opennit-cal-' + deviceId);
            if (!calWrap) return;

            if (deviceSlotsCache[deviceId] !== undefined) {
                opennitRenderCalendar(deviceId, deviceSlotsCache[deviceId]);
                return;
            }

            calWrap.innerHTML = '<div class="opennit-cal-loading">Verfügbarkeit wird geladen…</div>';

            var fd = new FormData();
            fd.append('action',    'opennit_get_device_slots');
            fd.append('nonce',     nonce);
            fd.append('device_id', deviceId);

            fetch(ajaxUrl, { method: 'POST', body: fd })
                .then(function (r) { return r.json(); })
                .then(function (res) {
                    var slots = (res.success && res.data && res.data.slots) ? res.data.slots : [];
                    deviceSlotsCache[deviceId] = slots;
                    opennitRenderCalendar(deviceId, slots);
                })
                .catch(function () { calWrap.innerHTML = ''; });
        }

        function opennitRenderCalendar(deviceId, slots) {
            var calWrap = document.getElementById('opennit-cal-' + deviceId);
            if (!calWrap) return;

            var occupiedSet = {};
            slots.forEach(function (slot) {
                if (!slot.start || !slot.end) return;
                var cur = new Date(slot.start);
                var end = new Date(slot.end);
                while (cur <= end) {
                    occupiedSet[cur.toISOString().substring(0, 10)] = true;
                    cur.setDate(cur.getDate() + 1);
                }
            });

            var today    = new Date(); today.setHours(0, 0, 0, 0);
            var todayStr = today.toISOString().substring(0, 10);
            var form       = calWrap.closest('.opennit-form');
            var startInput = form ? form.querySelector('[name="planned_start"]') : null;
            var endInput   = form ? form.querySelector('[name="planned_end"]')   : null;
            var calSelFrom = null;
            var calSelTo   = null;

            var MONTHS_DE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
            var DAYS_DE   = ['Mo','Di','Mi','Do','Fr','Sa','So'];

            function buildMonthGrid(year, month) {
                var html = '<div class="opennit-cal-month">';
                html += '<div class="opennit-cal-month-title">' + MONTHS_DE[month] + ' ' + year + '</div>';
                html += '<div class="opennit-cal-grid">';
                DAYS_DE.forEach(function (d) { html += '<div class="opennit-cal-dow">' + d + '</div>'; });
                var first    = new Date(year, month, 1);
                var startDow = (first.getDay() + 6) % 7;
                for (var e = 0; e < startDow; e++) html += '<div class="opennit-cal-day opennit-cal-day-empty"></div>';
                var maxAllowedDate = opennitAddDays(todayStr, maxAdvanceDays);
                var maxEndAllowed  = calSelFrom ? opennitAddDays(calSelFrom, maxDurationDays) : null;
                var daysInMonth    = new Date(year, month + 1, 0).getDate();
                for (var d = 1; d <= daysInMonth; d++) {
                    var isoDate = year + '-' + String(month + 1).padStart(2, '0') + '-' + String(d).padStart(2, '0');
                    var dayDate = new Date(year, month, d);
                    var isPast        = dayDate < today;
                    var isToday       = isoDate === todayStr;
                    var isOccupied    = !!occupiedSet[isoDate];
                    var isTooFarAhead = isoDate > maxAllowedDate;
                    var isTooLong     = !!(maxEndAllowed && isoDate > maxEndAllowed);
                    var isSelFrom     = calSelFrom === isoDate;
                    var isSelTo       = calSelTo   === isoDate;
                    var inRange       = calSelFrom && calSelTo && isoDate > calSelFrom && isoDate < calSelTo;
                    var isDisabled    = isPast || isOccupied || isTooFarAhead || isTooLong;
                    var cls = 'opennit-cal-day';
                    if (isDisabled)               cls += ' opennit-cal-day-disabled';
                    if (isToday)                  cls += ' opennit-cal-day-today';
                    if (isOccupied)               cls += ' opennit-cal-day-occupied';
                    if (isTooFarAhead || isTooLong) cls += ' opennit-cal-day-outofrange';
                    if (isSelFrom || isSelTo)     cls += ' opennit-cal-day-selected';
                    if (inRange)                  cls += ' opennit-cal-day-inrange';
                    var attrs = ' data-date="' + isoDate + '"';
                    if (!isDisabled) attrs += ' role="button" tabindex="0"';
                    html += '<div class="' + cls + '"' + attrs + '>' + d + '</div>';
                }
                html += '</div></div>';
                return html;
            }

            var calRangeError = null;

            function renderGrid() {
                var html = '<div class="opennit-cal-months">';
                for (var m = 0; m < 3; m++) {
                    var d = new Date(today.getFullYear(), today.getMonth() + m, 1);
                    html += buildMonthGrid(d.getFullYear(), d.getMonth());
                }
                html += '</div>';
                if (calRangeError) {
                    html += '<p class="opennit-cal-hint opennit-cal-hint-error">' + opennitEsc(calRangeError) + '</p>';
                } else if (calSelFrom && !calSelTo) {
                    html += '<p class="opennit-cal-hint">Bitte Enddatum auswählen.</p>';
                } else if (calSelFrom && calSelTo) {
                    html += '<p class="opennit-cal-hint opennit-cal-hint-ok">Von&nbsp;'
                          + calSelFrom.split('-').reverse().join('.') + '&nbsp;bis&nbsp;'
                          + calSelTo.split('-').reverse().join('.') + ' ausgewählt.</p>';
                } else {
                    html += '<p class="opennit-cal-hint">Klicken Sie auf ein freies Datum, um den Zeitraum zu wählen.</p>';
                }
                calWrap.innerHTML = html;
                calWrap.querySelectorAll('.opennit-cal-day[data-date]').forEach(function (el) {
                    if (el.classList.contains('opennit-cal-day-disabled')) return;
                    el.addEventListener('click', function () {
                        var d = this.getAttribute('data-date');
                        calRangeError = null;
                        if (!calSelFrom || (calSelFrom && calSelTo)) {
                            calSelFrom = d; calSelTo = null;
                        } else if (d === calSelFrom) {
                            calSelFrom = null;
                        } else {
                            var from = d < calSelFrom ? d : calSelFrom;
                            var to   = d < calSelFrom ? calSelFrom : d;
                            var blocked = false;
                            var cur = new Date(from);
                            var end = new Date(to);
                            while (cur <= end) {
                                if (occupiedSet[cur.toISOString().substring(0, 10)]) { blocked = true; break; }
                                cur.setDate(cur.getDate() + 1);
                            }
                            if (blocked) {
                                calSelFrom = d; calSelTo = null;
                                calRangeError = 'Der Zeitraum enthält belegte Tage. Bitte wählen Sie einen anderen Bereich.';
                            } else {
                                calSelFrom = from; calSelTo = to;
                            }
                        }
                        if (startInput) startInput.value = calSelFrom || '';
                        if (endInput)   endInput.value   = calSelTo   || '';
                        if (endInput && calSelFrom) endInput.min = calSelFrom;
                        renderGrid();
                    });
                });
            }

            calSelFrom = (startInput && startInput.value) ? startInput.value : null;
            calSelTo   = (endInput   && endInput.value)   ? endInput.value   : null;
            renderGrid();
        }

        // ----------------------------------------------------------------
        // Formular-Aktionen
        // ----------------------------------------------------------------

        window.opennitOpenForm = function (btn) {
            var deviceId = btn.dataset.deviceId;
            var form     = document.getElementById('opennit-form-' + deviceId);
            if (!form) return;
            var isOpen = !form.hidden;
            form.hidden = isOpen;
            btn.setAttribute('aria-expanded', String(!isOpen));
            if (isOpen) {
                btn.textContent = (btn.dataset.dateFrom && btn.dataset.dateTo) ? 'Reservieren' : 'Anfragen';
            } else {
                btn.textContent = 'Schließen';
                opennitLoadCalendar(deviceId);
            }
        };

        window.opennitCloseForm = function (deviceId) {
            var form   = document.getElementById('opennit-form-' + deviceId);
            var reqBtn = document.querySelector('[data-device-id="' + deviceId + '"].opennit-btn-request');
            if (form)   form.hidden = true;
            if (reqBtn) {
                reqBtn.setAttribute('aria-expanded', 'false');
                reqBtn.textContent = (reqBtn.dataset.dateFrom && reqBtn.dataset.dateTo) ? 'Reservieren' : 'Anfragen';
            }
        };

        wrap.querySelectorAll('.opennit-btn-request').forEach(function (btn) {
            btn.addEventListener('click', function () { window.opennitOpenForm(this); });
        });

        document.addEventListener('submit', function (e) {
            if (e.target.matches('.opennit-form')) {
                e.preventDefault();
                window.opennitSubmitForm(e, e.target);
            }
        });

        window.opennitSubmitForm = function (e, form) {
            if (e) e.preventDefault();
            var deviceId     = form.dataset.deviceId;
            var feedback     = document.getElementById('opennit-feedback-' + deviceId);
            var submitBtn    = form.querySelector('[type="submit"]');
            var startInput   = form.querySelector('[name="planned_start"]');
            var endInput     = form.querySelector('[name="planned_end"]');
            var purposeInput = form.querySelector('[name="purpose"]');

            if (!startInput.value || !endInput.value) {
                showFeedback(feedback, 'Bitte Datum ausfüllen.', 'error');
                return;
            }
            var today     = new Date(); today.setHours(0, 0, 0, 0);
            var startDate = new Date(startInput.value);
            var endDate   = new Date(endInput.value);
            var maxStart  = new Date(today); maxStart.setDate(today.getDate() + maxAdvanceDays);
            if (startDate > maxStart) { showFeedback(feedback, errTooFarAhead, 'error'); return; }
            var durationDays = Math.round((endDate - startDate) / 86400000);
            if (durationDays > maxDurationDays) { showFeedback(feedback, errTooLong, 'error'); return; }

            submitBtn.disabled    = true;
            submitBtn.textContent = 'Wird gesendet…';

            var formData = new FormData();
            formData.append('action',        'opennit_loan_request');
            formData.append('nonce',         nonce);
            formData.append('device_id',     deviceId);
            formData.append('planned_start', startInput.value);
            formData.append('planned_end',   endInput.value);
            formData.append('purpose',       purposeInput ? purposeInput.value : '');

            fetch(ajaxUrl, { method: 'POST', body: formData })
                .then(function (r) { return r.json(); })
                .then(function (data) {
                    if (data.success) {
                        // Server liefert message + status ('pending' | 'reserved' | 'active')
                        var resp      = data.data || {};
                        var isPending = (resp.status === 'pending');
                        // Fallback je nach Status: Anfrage-Text bei pending, Reservierungs-Text sonst
                        var fallbackText = isPending
                            ? (wrap.dataset.requestText || 'Ihre Anfrage wurde übermittelt. Die Freigabe erfolgt durch das IT-Team.')
                            : (wrap.dataset.successText || 'Erfolgreich reserviert.');
                        var successText = resp.message || fallbackText;
                        var panelTitle = isPending
                            ? 'Anfrage erfolgreich gesendet!'
                            : 'Gerät erfolgreich reserviert!';
                        var formWrap = document.getElementById('opennit-form-' + deviceId);
                        if (formWrap) {
                            formWrap.hidden = false;
                            formWrap.innerHTML = '<div class="opennit-success-panel">'
                                + '<div class="opennit-success-panel__icon">&#10003;</div>'
                                + '<div class="opennit-success-panel__body">'
                                + '<span class="opennit-success-panel__title">' + opennitEsc(panelTitle) + '</span>'
                                + '<span class="opennit-success-panel__text">' + opennitEsc(successText) + '</span>'
                                + '</div></div>';
                        }
                        var deviceItem = document.getElementById('opennit-device-' + deviceId);
                        if (deviceItem) {
                            var badge = deviceItem.querySelector('.opennit-badge');
                            if (badge) {
                                badge.className = 'opennit-badge ' + (isPending ? 'opennit-badge-pending' : 'opennit-badge-occupied');
                                badge.textContent = isPending ? 'Anfrage offen' : 'Reserviert';
                            }
                            var reqBtn = deviceItem.querySelector('.opennit-btn-request');
                            if (reqBtn) reqBtn.remove();
                        }
                    } else {
                        var errData = data.data || {};
                        var msg = (typeof errData === 'string') ? errData : (errData.message || 'Fehler beim Senden.');
                        if (errData.conflict_from && errData.conflict_to) {
                            msg = 'Das Gerät ist im gewählten Zeitraum nicht verfügbar – '
                                + 'es ist vom ' + errData.conflict_from + ' bis ' + errData.conflict_to + ' bereits vergeben.';
                        }
                        showFeedback(feedback, msg, 'error');
                        submitBtn.disabled    = false;
                        submitBtn.textContent = 'Anfrage senden';
                    }
                })
                .catch(function () {
                    showFeedback(feedback, 'Netzwerkfehler. Bitte erneut versuchen.', 'error');
                    submitBtn.disabled    = false;
                    submitBtn.textContent = 'Anfrage senden';
                });
        };

        window.opennitCancelLoan = function (btn) {
            if (!confirm('Reservierung wirklich stornieren?')) return;
            var lendingId = btn.dataset.lendingId;
            btn.disabled    = true;
            btn.textContent = 'Wird storniert…';
            var formData = new FormData();
            formData.append('action',     'opennit_cancel_loan');
            formData.append('nonce',      nonce);
            formData.append('lending_id', lendingId);
            fetch(ajaxUrl, { method: 'POST', body: formData })
                .then(function (r) { return r.json(); })
                .then(function (data) {
                    if (data.success) {
                        var card = btn.closest('.opennit-loan-card');
                        if (card) { card.style.opacity = '0.4'; card.innerHTML = '<div class="opennit-loan-card-body"><p>Storniert.</p></div>'; }
                    } else {
                        alert(data.data || 'Fehler beim Stornieren.');
                        btn.disabled    = false;
                        btn.textContent = 'Stornieren';
                    }
                })
                .catch(function () {
                    alert('Netzwerkfehler. Bitte erneut versuchen.');
                    btn.disabled    = false;
                    btn.textContent = 'Stornieren';
                });
        };

        // ----------------------------------------------------------------
        // Datums-Synchronisation
        // ----------------------------------------------------------------

        function initDateSync() {
            document.querySelectorAll('.opennit-form').forEach(function (form) {
                var s = form.querySelector('[name="planned_start"]');
                var t = form.querySelector('[name="planned_end"]');
                if (!s || !t) return;
                s.addEventListener('change', function () {
                    if (t.value && t.value < this.value) t.value = this.value;
                    t.min = this.value;
                });
            });
        }
        initDateSync();

        var fFrom = document.getElementById('opennit-f-from');
        var fTo   = document.getElementById('opennit-f-to');
        if (fFrom && fTo) {
            fFrom.addEventListener('change', function () {
                if (fTo.value && fTo.value < this.value) fTo.value = this.value;
                fTo.min = this.value;
            });
        }

        // ----------------------------------------------------------------
        // Hilfsfunktionen
        // ----------------------------------------------------------------

        function showFeedback(el, message, type) {
            if (!el) return;
            el.hidden      = false;
            el.textContent = message;
            el.className   = 'opennit-form-feedback opennit-feedback-' + type;
        }

        function opennitEsc(str) {
            return String(str || '')
                .replace(/&/g, '&amp;').replace(/</g, '&lt;')
                .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
        }

        function opennitFmtDate(iso) {
            if (!iso) return '';
            var p = iso.split('-');
            return p.length === 3 ? p[2] + '.' + p[1] + '.' + p[0] : iso;
        }

        function opennitToday() {
            return new Date().toISOString().substring(0, 10);
        }

        function opennitAddDays(isoDate, days) {
            var d = new Date(isoDate);
            d.setDate(d.getDate() + days);
            return d.toISOString().substring(0, 10);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initLoanDevices);
    } else {
        initLoanDevices();
    }
    window.addEventListener('load', initLoanDevices);
    setTimeout(initLoanDevices, 800);

})();