if (!crypto.randomUUID) { crypto.randomUUID = function() { return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ); }; } function addHTMXListeners() { document.body.addEventListener("htmx:sendError", function (e) { let errorMessage = e.detail.error; errorMessage = "Failed to send request to server - " + errorMessage; let htmlFragment = '

' + errorMessage.replace(/'; let bodyElement = document.querySelector('body'); bodyElement.insertAdjacentHTML('afterbegin', htmlFragment); }); document.body.addEventListener("htmx:responseError", function (e) { let errorMessage = e.detail.xhr.response; alert(errorMessage); let htmlFragment = '

' + errorMessage.replace(/'; let bodyElement = document.querySelector('body'); bodyElement.insertAdjacentHTML('afterbegin', htmlFragment); }); } async function getFile(fileEntry) { return new Promise((resolve, reject) => { fileEntry.file(resolve, reject); }); } async function processDirectory(entry, path = '', files = []) { path = path ? path + "/" + entry.name : entry.name; let dirReader = entry.createReader(); const readEntries = async () => { return new Promise((resolve, reject) => { dirReader.readEntries(resolve, reject); }); }; let entries; try { entries = await readEntries(); } catch (error) { console.error("Error reading directory entries", error); return; } for (let entry of entries) { if (entry.isDirectory) { await processDirectory(entry, path, files); } else { let fileEntry = await getFile(entry); fileEntry._relativefilePath = path + "/" + entry.name; files.push(fileEntry); } } } class ValidationController { constructor(formId, translations) { this.translations = translations; this.formId = formId; this.form = document.getElementById(formId); if (!this.form) { console.warn('ValidationController: form not found:', formId); return; } this.fields = this.form.querySelectorAll('input, select, textarea'); this.submitButtons = this.form.querySelectorAll( 'button[type="submit"][data-validation-submit]' ); this.setupEventListeners(); this.validateForm(); } translate(s) { return this.translations[s] || s; } setupEventListeners() { this.form.addEventListener('submit', (e) => this.handleSubmit(e)); this.fields.forEach(field => { if (this.shouldValidateField(field)) { field.addEventListener('blur', () => this.validateForm()); field.addEventListener('input', () => this.validateForm()); field.addEventListener('change', () => this.validateForm()); // For select and date inputs } }); } handleSubmit(e) { e.preventDefault(); if (this.validateForm()) { console.log('Form is valid, submit it!'); // You can submit the form here if needed // this.form.submit(); } } shouldValidateField(field) { return field.hasAttribute('required') || field.hasAttribute('data-validation-type'); } async validateField(field) { if (!this.shouldValidateField(field)) { return true; // Field doesn't need validation } const isRequired = field.hasAttribute('required'); const minLength = parseInt(field.getAttribute('data-min-length')) || 0; const maxLength = parseInt(field.getAttribute('data-max-length')) || Infinity; let isValid = true; let errorMessage = ''; if (isRequired) { if (field.tagName.toLowerCase() === 'select') { if (!field.value || field.value === "") { isValid = false; errorMessage = 'Please select an option.'; } } else if (field.type === 'date') { if (!field.value) { isValid = false; errorMessage = 'Please enter a valid date.'; } } else if (!field.value.trim()) { isValid = false; errorMessage = 'Dies ist ein Pflichtfeld'; } } if (isValid && field.value.trim()) { if (field.type !== 'date' && field.tagName.toLowerCase() !== 'select') { if (field.value.trim().length < minLength) { isValid = false; errorMessage = `Minimum length is ${minLength} characters.`; } else if (field.value.trim().length > maxLength) { isValid = false; errorMessage = `Maximum length is ${maxLength} characters.`; } } } // Additional date validation if (isValid && field.type === 'date') { const minDate = field.getAttribute('min'); const maxDate = field.getAttribute('max'); const selectedDate = new Date(field.value); if (minDate && selectedDate < new Date(minDate)) { isValid = false; errorMessage = `Date must be on or after ${minDate}.`; } else if (maxDate && selectedDate > new Date(maxDate)) { isValid = false; errorMessage = `Date must be on or before ${maxDate}.`; } } // Money validation if (field.getAttribute('data-validation-type') === 'money') { //console.log(field); const fieldValue = field.value.trim(); const isValidNumber = /^-?\d+(\.\d{1,2})?$/.test(fieldValue); if (!isValidNumber) { isValid = false; errorMessage = 'Please enter a valid amount (e.g., 123.45).'; } else { const fieldLength = fieldValue.length; if (typeof minLength === 'number' && fieldLength < minLength) { isValid = false; errorMessage = `The value must be at least ${minLength} characters long.`; } else if (typeof maxLength === 'number' && fieldLength > maxLength) { isValid = false; errorMessage = `The value must be no more than ${maxLength} characters long.`; } } } // Year validation if (field.getAttribute('data-validation-type') === 'year') { const fieldValue = field.value.trim(); const isValidYear = /^\d{4}$/.test(fieldValue); if (!isValidYear) { isValid = false; errorMessage = 'Please enter a valid amount (e.g., 123.45).'; } else { const year = parseInt(fieldValue, 10); const currentYear = new Date().getFullYear(); const minYear = 1900; const maxYear = currentYear + 10; if (year < minYear || year > maxYear) { isValid = false; errorMessage = `The year must be between ${minYear} and ${maxYear}.`; } } } // Email validation if (field.getAttribute('data-validation-type') === 'email') { const fieldValue = field.value.trim(); const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(fieldValue); if (!isValidEmail) { isValid = false; errorMessage = 'Bitte geben Sie eine gültige E-Mail-Adresse ein (z. B. example@example.com).'; } } // Iban validation if (field.getAttribute('data-validation-type') === 'iban') { const fieldValue = field.value.trim(); const iban = fieldValue.replace(/\s+/g, '').toUpperCase(); // Basic IBAN structure check if (iban && !/^[A-Z0-9]{15,34}$/.test(iban)) { isValid = false; errorMessage = 'IBAN must be between 15 and 34 characters long'; this.updateFieldStatus(field, isValid, errorMessage); return isValid; } // Move the first four characters to the end of the string const rearrangedIban = iban.slice(4) + iban.slice(0, 4); // Replace each letter in the string with two digits, expanding the string as needed const expandedIban = rearrangedIban.replace(/[A-Z]/g, function(match) { return match.charCodeAt(0) - 55; }); // Perform Modulo 97 operation on the expanded IBAN const ibanAsNumber = BigInt(expandedIban); isValid = !ibanAsNumber || ibanAsNumber % 97n === 1n; //const isValidIban = /^[A-Z0-9]{15,34}$/.test(iban); if (!isValid) { errorMessage = 'Please enter a valid IBAN (e.g., CH6330000004400005700).'; } } this.updateFieldStatus(field, isValid, errorMessage); return isValid; } async validateForm() { let isValid = true; for (const field of this.fields) { if (this.shouldValidateField(field) && !(await this.validateField(field))) { console.log('invalid field', field); isValid = false; } }; this.formValidated(); console.log('form isValid', isValid); this.updateSubmitButtons(isValid); return isValid; } formValidated() {} updateFieldStatus(field, isValid, errorMessage) { if (!field) { console.warn('updateFieldStatus: Kein Feld übergeben.'); return; } const feedbackElement = field.nextElementSibling; if (isValid) { field.classList.remove('is-invalid'); field.classList.add('is-valid'); if (feedbackElement && feedbackElement.classList.contains('invalid-feedback')) { feedbackElement.textContent = ''; } else if (!feedbackElement) { console.warn(`Kein Feedback-Element für Feld "${field.id}" gefunden.`); } } else { field.classList.remove('is-valid'); field.classList.add('is-invalid'); const translatedMessage = this.translate(errorMessage || 'Fehlerhafte Eingabe'); if (feedbackElement && feedbackElement.classList.contains('invalid-feedback')) { feedbackElement.textContent = translatedMessage; } else if (!feedbackElement) { console.warn(`Kein Feedback-Element für Feld "${field.id}" gefunden.`); } } } updateSubmitButtons(isValid) { this.submitButtons.forEach(button => { button.disabled = !isValid; if (isValid) { button.classList.remove('disabled'); } else { button.classList.add('disabled'); } }); } } class DirectUploadHandler { constructor(config) { this.filesToUpload = []; this.ticket = config.ticket; this.elem = config.elem; this.parent = config.parent; this.file_properties = config.file_properties; this.uploadURL = config.uploadURL; this.callbackActions = config.callbackActions; this.dragOverTimeout = null; this.chunkSize = config.chunkSize ? config.chunkSize : 3 * 1024 * 1024; this.progress = config.progress; this.elem.removeEventListener('dragover', this.dragOverHandler.bind(this)); this.elem.addEventListener('dragover', this.dragOverHandler.bind(this)); this.elem.removeEventListener('drop', this.dropHandler.bind(this)); this.elem.addEventListener('drop', this.dropHandler.bind(this)); this.originalBackgroundColor = this.elem.style.backgroundColor; } async dropHandler(ev) { ev.preventDefault(); console.log('drop event!!!'); if (ev.dataTransfer.items) { this.filesToUpload = []; let items = ev.dataTransfer.items; for (let i = 0; i < items.length; i++) { let item = items[i]; if (item.webkitGetAsEntry) { let entry = item.webkitGetAsEntry(); if (entry.isDirectory) { await processDirectory(entry, item.name, this.filesToUpload); } else if (entry.isFile) { this.filesToUpload.push(item.getAsFile()); } } else if (item.kind === 'file') { this.filesToUpload.push(item.getAsFile()); } } } else { this.filesToUpload = ev.dataTransfer.files; } await this.upload(); } dragOverHandler(ev) { ev.preventDefault(); this.elem.style.backgroundColor = 'lightblue'; clearTimeout(this.dragOverTimeout); this.dragOverTimeout = setTimeout(() => { this.elem.style.backgroundColor = this.originalBackgroundColor; }, 300); } onprogress(event) { let percent = (event.loaded / event.total) * 100; //console.log('upload progress:', percent); let elem = this.progress; if (elem) { elem.innerText = 'uploading... ' + Math.round(percent).toString() + " %"; } } async callback(response) { console.log('uploadResponse:', response, response['status']); response = JSON.parse(response); let elem = document.getElementById("progress"); if (elem) { elem.innerText = 'uploaded'; } if (this.callbackActions) { this.callbackActions['values'] = { action: 'uploaded', obj_id: response['obj_id'], obj_content_hash: response['obj_content_hash'] }; } console.log('incallback', this, this.callbackActions); htmx.ajax('GET', this.callbackActions.url, this.callbackActions); } async doUploadFile(file, obj_text, add_to_clipboard) { console.log('doUploadFile:', file); let dateObj = new Date(file.lastModified); // Create a Date object let isoDate = dateObj.toISOString(); // Convert to ISO date string const projnr = document.getElementById('cq_projnr'); const projname = document.getElementById('cq_projname'); let properties = { "_obj_profile": "cq_file", "_obj_name": file.name, "_obj_title": file.name, "_obj_date": isoDate, "_obj_text": obj_text, "_obj_source": 'webui', "_cq_doctype": "document", "_obj_type": "document", "_obj_lifecycle": "document", "_obj_lifecycle_state": "00 uploaded", "_add_to_clipboard": add_to_clipboard }; if (projnr) { properties['cq_projnr'] = projnr.value()}; if (projname) { properties['cq_projname'] = projname.value()}; //Object.assign(properties, this.file_properties); let incomingProps = this.file_properties; if (typeof incomingProps === 'string') { try { incomingProps = JSON.parse(incomingProps); } catch (e) { incomingProps = {}; } } Object.assign(properties, incomingProps); properties['_relative_path'] = file._relativefilePath ? file._relativefilePath : (file.webkitRelativePath ? file.webkitRelativePath : ""); console.log(properties); console.log('uploadURL:', this.uploadURL); let uploadUrl = this.uploadURL ? this.uploadURL : "/dms/upload?type=file&profile=cq_file"; if (this.parent) { uploadUrl = uploadUrl + "&parent=" + this.parent; } uploadUrl = uploadUrl + "&ticket=" + this.ticket; try { const response = await uploadFile(uploadUrl, file, this.onprogress.bind(this), properties, {}, this.chunkSize); await this.callback(response); } catch (error) { console.error('Upload failed:', error); } } async upload() { console.log('do upload:', this.filesToUpload); if (this.filesToUpload) { for (let file of this.filesToUpload) { await this.doUploadFile(file, ''); } } } } function dropHandler(ev, folderId) { ev.preventDefault(); if (ev.dataTransfer.items) { let files = []; let items = ev.dataTransfer.items; for (let i = 0; i < items.length; i++) { let item = items[i]; if (item.webkitGetAsEntry) { let entry = item.webkitGetAsEntry(); if (entry.isDirectory) { processDirectory(entry, item.name, files); } else if (entry.isFile) { files.push(item.getAsFile()); } } else if (item.kind === 'file') { files.push(item.getAsFile()); } } document._filesToUpload = files; htmx.ajax('GET', `upload.html?id=${folderId}`, {target: '#body'}); } else { document._filesToUpload = ev.dataTransfer.files; htmx.ajax('GET', `upload.html?id=${folderId}`, {target: '#body'}); } } function dragOverHandler(ev) { ev.preventDefault(); } function submitOnEnter(event, id) { if (event.shiftKey && event.key == 'Enter') { event.preventDefault(); event.stopPropagation(); htmx.trigger(id, 'submit', {}); } } function submitOnEnter2(event, id) { if (event.shiftKey && event.key == 'Enter') { event.preventDefault(); event.stopPropagation(); document.getElementById(id).click(); } } function showDialog(url, cssSelector) { htmx.ajax('GET', url, {target: '#dialog', swap: 'innerHTML'}).then(() => { $(cssSelector).modal('show'); }); } function getVAFilterFields(fields) { const data = {}; fields.forEach(id => { const element = document.getElementById(id); if (element) { const name = element.name; const value = element.value; if (name) { data[name] = value; } } }); const jsonString = JSON.stringify(data); return encodeURIComponent(jsonString); } function showDrawer(url, cssSelector) { htmx.ajax('GET', url, {target: '#dialog', swap: 'innerHTML'}).then(() => { $(cssSelector).modal('show'); }); } function closeDialog(cssSelector) { $(cssSelector).modal('hide'); } function closeDialog2(cssSelector) { let dialog = document.querySelector(cssSelector); dialog.style.display = 'none'; let body = document.querySelector('body'); body.style = ''; body.className = ''; let backdrop = document.querySelector('.modal-backdrop'); if (backdrop) { backdrop.remove(); } } function _showPopup(pageX, pageY) { const popup = document.querySelector('#popup'); window.addEventListener('click', function (event) { if (!popup.contains(event.target) && event.target !== popup) { closePopup(); } }, {once: true}); const menuWidth = popup.offsetWidth; const menuHeight = popup.offsetHeight; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let top = pageY; let left = pageX; if (left + menuWidth + 5 > viewportWidth) { left -= menuWidth; } if (top + menuHeight + 5 > viewportHeight) { top -= menuHeight; } if (left < 5) left = 5; if (top < 5) top = 5; popup.style.left = left + 'px'; popup.style.top = top + 'px'; popup.style.display = 'block'; } function showPopup(e, url) { htmx.ajax('GET', url, {target: '#popup', swap: 'innerHTML'}).then(() => { _showPopup(e.pageX, e.pageY); }); } function closePopup() { const popup = document.querySelector('#popup'); popup.style.display = 'none'; } function splitPanel(container) { let children = container.children; let leftPanel = children[0]; let divider = children[1]; let isResizing = false; const stopResize = (e) => { isResizing = false; document.removeEventListener('pointerup', stopResize, false); document.removeEventListener('pointermove', doResize, false); } const doResize = (e) => { let containerRect = container.getBoundingClientRect(); let leftWidth = e.clientX - containerRect.left; leftPanel.style.flexBasis = `${leftWidth}px`; } divider.addEventListener('pointerdown', (e) => { isResizing = true; document.addEventListener('pointerup', stopResize); document.addEventListener('pointermove', doResize); }); } function toggle_full_screen() { if (!document.fullscreenElement) { if (document.documentElement.requestFullscreen) { document.documentElement.requestFullscreen(); } else if (document.documentElement.mozRequestFullScreen) { document.documentElement.mozRequestFullScreen(); } else if (document.documentElement.webkitRequestFullscreen) { document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); } else if (document.documentElement.msRequestFullscreen) { document.documentElement.msRequestFullscreen(); } } else { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.webkitCancelFullScreen) { document.webkitCancelFullScreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } } } function copyToClipboard(copyText) { navigator.clipboard.writeText(copyText); } /* Updated initTagify to prevent double initialization and avoid undefined whitelist errors */ function initTagify(querySelector, whiteList = [], enforceWhiteList = false) { // Find the input element const inputElement = document.querySelector(querySelector); if (!inputElement) return; // Prevent double initialization: reuse existing instance if present if (inputElement._tagify) { // Optionally update the whitelist settings inputElement._tagify.settings.whitelist = Array.isArray(whiteList) ? whiteList : []; inputElement._tagify.settings.enforceWhitelist = enforceWhiteList; return inputElement._tagify; } // Remove any leftover container if (inputElement.previousSibling && inputElement.previousSibling.tagName === 'TAGS') { inputElement.previousSibling.remove(); } // Initialize Tagify with a safe default whitelist const tagify = new Tagify(inputElement, { whitelist: Array.isArray(whiteList) ? whiteList : [], enforceWhitelist: enforceWhiteList }); // Store the instance to avoid re-init inputElement._tagify = tagify; // Setup drag-and-drop reordering function onDragEnd() { tagify.updateValueByDOMTags(); } new DragSort(tagify.DOM.scope, { selector: '.' + tagify.settings.classNames.tag, callbacks: { dragEnd: onDragEnd } }); return tagify; } function initTypeahead(selector, dataUrl) { // Initialize Bloodhound suggestion engine var bloodhound = new Bloodhound({ /* datumTokenizer: function(datum) { return Bloodhound.tokenizers.whitespace(datum.value + ' ' + datum.display); }, queryTokenizer: Bloodhound.tokenizers.whitespace, */ datumTokenizer: function(datum) { return [datum.value]; // Return full value as single token }, queryTokenizer: function(query) { return [query]; // Return full query as single token }, remote: { url: dataUrl, wildcard: '%QUERY', rateLimitBy: 'throttle', rateLimitWait: 300 } }); // Initialize typeahead $(selector).typeahead({ minLength: 2, highlight: true, hint: true }, { name: 'suggestions', display: 'value', source: bloodhound, limit: 10, templates: { empty: [ '
', 'No matches found', '
' ].join('\n'), suggestion: function(data) { return '
' + data.display + '
'; } } }); // Event handlers $(selector) .on('typeahead:select', function(ev, suggestion) { $(this).typeahead('val', suggestion.value); console.log('Selected value:', suggestion.value); console.log('Selected display:', suggestion.display); ev.target.dispatchEvent(new Event('input')); }) .on('typeahead:asyncrequest', function() { console.log('Loading suggestions...'); }) .on('typeahead:asyncreceive', function() { console.log('Finished loading suggestions'); }) .on('typeahead:asynccancel', function() { console.log('Loading cancelled'); }); } var tableSelection = {}; function toggleAllCheckboxes(source) { const checkboxes = document.querySelectorAll('.row-checkbox'); checkboxes.forEach(checkbox => { checkbox.checked = source.checked; updateTableSelection(checkbox.id, source.checked); }); } function checkboxChanged(objId, evt) { const isChecked = evt.target.checked; updateTableSelection(objId, isChecked); } function showHideDropdownMenu(hasSelections) { const targetDiv = document.querySelector('#header_dropdown_menu > span > div'); if (targetDiv) { if (hasSelections) { targetDiv.classList.add('inline-block'); targetDiv.classList.remove('d-none'); } else { targetDiv.classList.remove('inline-block'); targetDiv.classList.add('d-none'); } } } function updateTableSelection(objId, isChecked) { if (isChecked) { tableSelection[objId] = true; } else { delete tableSelection[objId]; } console.log('Current tableSelection:', tableSelection); const hasSelections = Object.keys(tableSelection).length > 0; showHideDropdownMenu(hasSelections); } async function addToFavorites(overview_url) { let selection = Object.keys(tableSelection) .map(key => key.replace(/^chk_/, '')); // Remove 'chk_' prefix for (let id of selection) { try { await fetch(`{overview_url}?action=add_favorite&obj_link={details_url}?id=${id}&obj_id=${id}&obj_name=${id}`); } catch (error) { console.error(`Error adding id ${id} to favorites:`, error); } } htmx.ajax('GET', overview_url, {target: "#main", swap: "outerHTML", select: "#main"}); } async function deleteEntries(overview_url) { let selection = Object.keys(tableSelection) .map(key => key.replace(/^chk_/, '')); // Remove 'chk_' prefix let amount = selection.length; // Get the number of selected entries let confirmDeletion = window.confirm(`Are you sure that you want to delete the selected ${amount} entries?`); if (!confirmDeletion) { return; } for (let id of selection) { try { await fetch(`${overview_url}?action=set_delete&oid=${id}`); } catch (error) { console.error(`Error deleting entry with id ${id}:`, error); } } // After all deletions, refresh the page htmx.ajax('GET', overview_url, {target: "#main", swap: "outerHTML", select: "#main"}); } async function undeleteEntries(overview_url) { let selection = Object.keys(tableSelection) .map(key => key.replace(/^chk_/, '')); // Remove 'chk_' prefix let amount = selection.length; // Get the number of selected entries let confirmDeletion = window.confirm(`Are you sure that you want to undelete the selected ${amount} entries?`); if (!confirmDeletion) { return; } for (let id of selection) { try { await fetch(`${overview_url}?action=set_undelete&oid=${id}`); } catch (error) { console.error(`Error undeleting entry with id ${id}:`, error); } } // After all deletions, refresh the page htmx.ajax('GET', overview_url, {target: "#main", swap: "outerHTML", select: "#main"}); } async function mergeEntries(overview_url) { // Gather selected IDs (removing any 'chk_' prefix) const selection = Object.keys(tableSelection) .map(key => key.replace(/^chk_/, '')); const amount = selection.length; // Confirm the merge action const confirmMerge = window.confirm(`Are you sure that you want to merge the selected ${amount} entries?`); if (!confirmMerge) { // If the user cancels, exit early return; } try { await fetch(`${overview_url}?action=merge_pdf&selection=` + selection); } catch (error) { console.error(`Error merging entry with selection ${selection}:`, error); } } function sendResetPassword(id) { try { let url = `/webui/user?id=${id}&action=send_reset_password`; fetch(url); alert('E-Mail versendet'); } catch (error) { console.error(`Error deleting entry with id ${id}:`, error); } } async function changeProfile() { try { let profile = document.getElementById('obj_profile').value; const params = new URLSearchParams(window.location.search); const id = params.get("id"); let url = `/webui/archive?id=${id}&action=change_profile&profile=${profile}`; const response = await fetch(url); if (response.ok) { window.location.reload(); } else { console.error("Server returned error:", response.status); } } catch (error) { console.error(`Error changing profile for entry with id ${id}:`, error); } } async function deleteTableEntries(overview_url, aspect = null) { // Auswahl extrahieren let selection = Object.keys(tableSelection) .filter(key => key.startsWith('chk_')) .map(key => key.replace(/^chk_/, '')); let amount = selection.length; // Wenn keine Checkbox ausgewählt wurde if (amount === 0) { alert("Bitte wähle mindestens einen Eintrag aus, den du löschen möchtest."); return; } // Bestätigungsdialog let confirmDeletion = window.confirm( `Are you sure that you want to delete the selected ${amount} entries?` ); if (!confirmDeletion) return; // Löschen for (let id of selection) { try { let url = `${overview_url}?id=${id}&action=delete`; if (aspect) { url += `&aspect=${encodeURIComponent(aspect)}`; } await fetch(url); } catch (error) { console.error(`Error deleting entry with id ${id}:`, error); } } // Ansicht aktualisieren htmx.ajax('GET', overview_url, { target: "#main", swap: "outerHTML", select: "#main" }); } /* function safeName(s){ return (s||'').replace(/[\\/:*?"<>|]+/g,'_').trim(); } await fetch(`${overview_url}?action=ai&id=${id}&selection=${selection}`); htmx.ajax('GET', overview_url, {target: "#main", select: "#main", swap: "outerHTML"}); } */ async function downloadAsZip() { const zip = new JSZip(); const ids = Object.keys(tableSelection).map(k => k.replace(/^chk_/, '')); if (!ids.length) { alert('No items selected!'); return; } // Optional: Titel aus der Tabelle holen (data-title), sonst ID const titleById = new Map( Array.from(document.querySelectorAll('a[data-id][data-title]')) .map(a => [a.dataset.id, a.dataset.title]) ); // data-id/data-title sind in den Zellen vorhanden :contentReference[oaicite:5]{index=5} for (const id of ids) { try { const url = `/dms/content?id=${encodeURIComponent(id)}`; // echter Download :contentReference[oaicite:6]{index=6} const res = await fetch(url, { credentials: 'same-origin' }); if (!res.ok) { console.error('Download failed', id, res.status); continue; } const blob = await res.blob(); const titleRaw = titleById.get(id) || id; const title = String(titleRaw).replace(/[\\/:*?"<>|]+/g, '_').trim(); const ct = (res.headers.get('content-type') || '').toLowerCase(); const ext = ct.includes('pdf') ? 'pdf' : ct.includes('jpeg') ? 'jpg' : ct.includes('png') ? 'png' : 'bin'; zip.file(`${title}.${ext}`, blob); } catch (e) { console.error('Error', id, e); } } const stamp = new Date().toISOString().replace(/[-:T]/g, '_').split('.')[0]; const content = await zip.generateAsync({ type: 'blob' }); const a = document.createElement('a'); a.href = URL.createObjectURL(content); a.download = `selected_${stamp}.zip`; a.click(); URL.revokeObjectURL(a.href); } async function createProjectEntries(overview_url) { let selection = Object.keys(tableSelection) .map(key => key.replace(/^chk_/, '')); // Remove 'chk_' prefix let amount = selection.length; // Get the number of selected entries if (selection.length === 0) { alert('Bitte wählen Sie zuerst mindestens einen Eintrag aus'); return; } let confirmCreation = window.confirm('Möchten Sie die Einträge wirklich erstellen??'); if (!confirmCreation) { return; } for (let id of selection) { try { await fetch(overview_url +`?action=create_new_project_entries&id=${id}&references=projects`); } catch (error) { console.error(`was not able to create project with id ${id}:`, error); } } // After all deletions, refresh the page htmx.ajax('GET', overview_url + '?id=projects', {target: "#main", swap: "outerHTML", select: "#main"}); } async function changeDefaultTenant(overview_url) { let tenant = document.getElementById('tenant'); if (!tenant) { return; } try { const tenantValue = tenant.value; const url = overview_url + (overview_url.includes("?") ? `&action=change_default_tenant&tenant=${tenantValue}` : `?action=change_default_tenant&tenant=${tenantValue}` ); const url1 = url //.replace('http:','https:'); const response = await fetch(url1); if (response.ok) { // Seite neu laden nach Erfolg window.location.reload(); } else { console.error("Server returned error:", response.status); } } catch (error) { console.error("Error changing client accounting:", error); } } class WebSocketClient { constructor(config = {}) { // Default configuration this.config = { host: window.location.host, protocol: window.location.protocol === 'https:' ? 'wss:' : 'ws:', path: '/ws/jsonrpc', reconnectMaxAttempts: 5, reconnectBaseDelay: 1000, maxDelay: 30000, debug: false, ...config }; this.socket = null; this.reconnectAttempt = 0; this.reconnectTimeout = null; this.messageHandler = null; this.subscriptions = []; } connect() { const wsUrl = `${this.config.protocol}//${this.config.host}${this.config.path}`; this.socket = new WebSocket(wsUrl); this.attachEventListeners(); } setMessageHandler(handler) { this.messageHandler = handler; } addSubscription(topic) { this.subscriptions.push(topic); if (this.isConnected()) { this.sendSubscription(topic); } } sendSubscription(topic) { const data = { 'id': crypto.randomUUID(), 'method': 'server/subscribe', 'params': { 'topic': topic } }; this.send(data); this.log(`Sent subscription for topic: ${topic}`); } // Send message send(data) { if (this.isConnected()) { this.socket.send(JSON.stringify(data)); } else { this.log('Cannot send message - socket is not connected', 'error'); } } // Check if socket is connected isConnected() { return this.socket && this.socket.readyState === WebSocket.OPEN; } // Attach WebSocket event listeners attachEventListeners() { this.socket.addEventListener('open', () => { this.log('Connected to WebSocket server'); this.reconnectAttempt = 0; // Send all subscriptions this.subscriptions.forEach(topic => { this.sendSubscription(topic); }); }); this.socket.addEventListener('message', (event) => { this.log('Message received:', 'info', event.data); try { const data = JSON.parse(event.data); if (this.messageHandler) { this.messageHandler(data); } } catch (error) { console.log(error); this.log('Error processing message:', 'error', error); } }); this.socket.addEventListener('close', () => { this.log('Disconnected from WebSocket server'); this.handleReconnection(); }); this.socket.addEventListener('error', (error) => { this.log('WebSocket error:', 'error', error); }); } // Handle reconnection logic handleReconnection() { if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); } if (this.reconnectAttempt < this.config.reconnectMaxAttempts) { const delay = Math.min( this.config.reconnectBaseDelay * Math.pow(2, this.reconnectAttempt), this.config.maxDelay ); this.log( `Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempt + 1}/${this.config.reconnectMaxAttempts})` ); this.reconnectTimeout = setTimeout(() => { this.reconnectAttempt++; this.connect(); }, delay); } else { this.log('Maximum reconnection attempts reached', 'error'); if (this.config.onMaxReconnectAttemptsReached) { this.config.onMaxReconnectAttemptsReached(); } } } // Logging utility log(message, level = 'info', ...args) { if (this.config.debug) { const timestamp = new Date().toISOString(); const prefix = `[WebSocketClient ${timestamp}]`; console.log('logg', prefix, message, args); switch (level) { case 'error': console.error(prefix, message, ...args); break; case 'warn': console.warn(prefix, message, ...args); break; default: console.log(prefix, message, ...args); } } } // Cleanup and close connection disconnect() { if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); } if (this.socket) { this.socket.close(); } } } async function addFavorite(linkEl) { const id = linkEl.dataset.id; const parent = linkEl.dataset.parent; const oid = linkEl.dataset.oid; const objLink = linkEl.dataset.objLink; const objName = linkEl.dataset.objName || ""; const objDescription = linkEl.dataset.objDescription || ""; const objCategory = linkEl.dataset.objCategory || ""; const params = new URLSearchParams({ id: id, parent: parent, action: "add_favorite", oid: oid, obj_link: objLink, obj_name: objName, obj_description: objDescription, obj_category: objCategory, }); const url = "/webui/archive?" + params.toString(); try { const response = await fetch(url, { method: "POST", // Wenn du CSRF brauchst, hier ergänzen: // headers: { "X-CSRFToken": "..."}, }); if (!response.ok) { throw new Error("Server-Fehler: " + response.status); } // Verhalten wie hx-on="htmx:afterRequest: window.location.reload()" window.location.reload(); } catch (err) { console.error("Fehler beim Hinzufügen zu den Favoriten:", err); alert("Der Favorit konnte nicht gespeichert werden."); } }