Quelltext des aktuellen Standes
/* 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">📦 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 '
+ calSelFrom.split('-').reverse().join('.') + ' bis '
+ 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">✓</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, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"');
}
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);
})();