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 cqInstallHtmxInlineScriptGuard() {
if (!window.__cqHtmxInlineScriptGuardInstalled) {
window.__cqHtmxInlineScriptGuardInstalled = true;
document.addEventListener("htmx:beforeSwap", function (e) {
let response = e && e.detail ? e.detail.serverResponse : null;
if (typeof response !== "string" || (response.indexOf("cqParentBackUrl") === -1 && response.indexOf("cqParentBackdropUrl") === -1)) {
return;
}
e.detail.serverResponse = response.replace(
/\b(?:const|let)\s+(cqParentBackUrl|cqParentBackdropUrl)\s*=/g,
"window.$1 ="
);
});
}
}
cqInstallHtmxInlineScriptGuard();
function addHTMXListeners() {
cqInstallHtmxInlineScriptGuard();
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);
});
};
while (true) {
let entries;
try {
entries = await readEntries();
} catch (error) {
console.error("Error reading directory entries", error);
return;
}
if (!entries || !entries.length) {
break;
}
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);
}
}
}
}
window.ValidationController = window.ValidationController || class {
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');
const submitSelector = 'button[type="submit"][data-validation-submit]';
const inFormButtons = Array.from(this.form.querySelectorAll(submitSelector));
const externalFormButtons = Array.from(
document.querySelectorAll(`${submitSelector}[form="${this.formId}"]`)
);
this.submitButtons = Array.from(new Set([...inFormButtons, ...externalFormButtons]));
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()) {
// 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 = 'Bitte wählen Sie eine Option.';
}
} else if (field.type === 'date') {
if (!field.value) {
isValid = false;
errorMessage = 'Kein gültiges Datum';
}
} 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 = 'Bitte geben Sie einen gültigen Betrag ein (z. B. 123.45).';
} else {
const fieldLength = fieldValue.length;
if (typeof minLength === 'number' && fieldLength < minLength) {
isValid = false;
errorMessage = `Der Wert muss mindestens ${minLength} Charakter lang sein`;
} else if (typeof maxLength === 'number' && fieldLength > maxLength) {
isValid = false;
errorMessage = `Der Wert muss lönger als ${minLength} Charakter lang sein`;
}
}
}
// 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 = 'Bitte geben Sie einen gültigen Betrag ein (z. B. 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 muss zwischen 15 und 34 Charakter lang sein';
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 = 'Bitte geben Sie eine gültige IBAN an (z.B. 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))) {
isValid = false;
}
};
this.formValidated();
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.callbackAfterEachUpload = !!config.callbackAfterEachUpload;
this.sequentialUpload = !!config.sequentialUpload;
this.debugUpload = !!config.debugUpload;
this.skipDropHandlers = !!config.skipDropHandlers;
this.dragOverTimeout = null;
this.dragDepth = 0;
this.isUploading = false;
this.batchExpected = 0;
this.batchCompleted = 0;
this.lastBatchResponse = null;
this.chunkSize = config.chunkSize ? config.chunkSize : 3 * 1024 * 1024;
this.progress = config.progress;
if (!this.elem) {
return;
}
this._boundDragOverHandler = this.dragOverHandler.bind(this);
this._boundDropHandler = this.dropHandler.bind(this);
this._boundDragLeaveHandler = this.dragLeaveHandler.bind(this);
// Avoid duplicate handlers after HTMX swaps/re-inits.
const prev = this.elem.__cqDirectUploadHandler;
if (prev && typeof prev.destroy === 'function') {
prev.destroy();
}
this.elem.__cqDirectUploadHandler = this;
if (this.debugUpload) {
console.info('CQ-UPLOAD-DEBUG handler-init', {
elem: this.elem && this.elem.id,
uploadURL: this.uploadURL,
callbackAfterEachUpload: this.callbackAfterEachUpload,
callbackActions: this.callbackActions
});
}
if (!this.skipDropHandlers) {
this.elem.addEventListener('dragover', this._boundDragOverHandler);
this.elem.addEventListener('drop', this._boundDropHandler);
this.elem.addEventListener('dragleave', this._boundDragLeaveHandler);
}
this.originalBackgroundColor = this.elem.style.backgroundColor;
}
destroy() {
if (!this.elem) return;
if (!this.skipDropHandlers) {
this.elem.removeEventListener('dragover', this._boundDragOverHandler);
this.elem.removeEventListener('drop', this._boundDropHandler);
this.elem.removeEventListener('dragleave', this._boundDragLeaveHandler);
}
if (this.elem.__cqDirectUploadHandler === this) {
delete this.elem.__cqDirectUploadHandler;
}
this.elem.classList.remove('drag-over');
this.dragDepth = 0;
}
async dropHandler(ev) {
ev.preventDefault();
ev.stopPropagation();
if (this.debugUpload) {
console.info('CQ-UPLOAD-DEBUG drop-event', {
items: ev.dataTransfer && ev.dataTransfer.items ? ev.dataTransfer.items.length : null,
files: ev.dataTransfer && ev.dataTransfer.files ? ev.dataTransfer.files.length : null
});
}
this.dragDepth = 0;
this.elem.classList.remove('drag-over');
const versionDropRoot = this.elem.closest ? this.elem.closest('[data-cq-version-upload="1"]') : null;
if (versionDropRoot) {
versionDropRoot.classList.remove('drag-over');
}
this.elem.style.backgroundColor = this.originalBackgroundColor;
if (this.isUploading) {
if (this.progress) {
this.progress.innerText = 'uploading...';
}
return;
}
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 && entry.isDirectory) {
await processDirectory(entry, item.name, this.filesToUpload);
} else if (entry && entry.isFile) {
let file = item.getAsFile();
if (file) {
this.filesToUpload.push(file);
}
}
} else if (item.kind === 'file') {
let file = item.getAsFile();
if (file) {
this.filesToUpload.push(file);
}
}
}
} else {
this.filesToUpload = ev.dataTransfer.files;
}
this.filesToUpload = Array.from(this.filesToUpload || []).filter(file => !!file);
if (this.debugUpload) {
console.info('CQ-UPLOAD-DEBUG drop-files', this.filesToUpload.map(file => ({
name: file.name,
size: file.size,
type: file.type
})));
}
await this.upload();
}
dragOverHandler(ev) {
ev.preventDefault();
ev.stopPropagation();
try { ev.dataTransfer.dropEffect = 'copy'; } catch (_) {}
this.dragDepth = Math.max(1, this.dragDepth + 1);
this.elem.classList.add('drag-over');
const versionDropRoot = this.elem.closest ? this.elem.closest('[data-cq-version-upload="1"]') : null;
if (versionDropRoot) {
versionDropRoot.classList.add('drag-over');
}
this.elem.style.backgroundColor = this.originalBackgroundColor;
clearTimeout(this.dragOverTimeout);
this.dragOverTimeout = setTimeout(() => {
if (!this.dragDepth && !this.isUploading) {
this.elem.classList.remove('drag-over');
const versionDropRoot = this.elem.closest ? this.elem.closest('[data-cq-version-upload="1"]') : null;
if (versionDropRoot) {
versionDropRoot.classList.remove('drag-over');
}
this.elem.style.backgroundColor = this.originalBackgroundColor;
}
}, 300);
}
dragLeaveHandler(ev) {
ev.preventDefault();
ev.stopPropagation();
this.dragDepth = Math.max(0, this.dragDepth - 1);
if (this.dragDepth === 0 && !this.isUploading) {
this.elem.classList.remove('drag-over');
const versionDropRoot = this.elem.closest ? this.elem.closest('[data-cq-version-upload="1"]') : null;
if (versionDropRoot) {
versionDropRoot.classList.remove('drag-over');
}
this.elem.style.backgroundColor = this.originalBackgroundColor;
}
}
onprogress(event) {
let percent = (event.loaded / event.total) * 100;
let elem = this.progress;
if (elem) {
const current = this.batchCompleted + 1;
const total = this.batchExpected || 1;
elem.innerText = 'uploading... ' + current.toString() + '/' + total.toString() + ' - ' + Math.round(percent).toString() + " %";
}
}
async callback(response) {
if (this.debugUpload) {
console.info('CQ-UPLOAD-DEBUG callback-enter', response);
}
if (typeof response === 'string') {
response = JSON.parse(response);
}
response = response || {};
let elem = this.progress;
if (elem) {
elem.innerText = 'uploaded';
}
if (!this.callbackActions || !this.callbackActions.url) {
if (this.debugUpload) {
console.warn('CQ-UPLOAD-DEBUG callback-no-actions', response);
}
return;
}
this.lastBatchResponse = response;
this.batchCompleted += 1;
if (!this.callbackAfterEachUpload && this.batchExpected > 1 && this.batchCompleted < this.batchExpected) {
return;
}
const req = Object.assign({}, this.callbackActions);
let url = String(req.url || '');
try {
const abs = new URL(url, window.location.origin);
abs.searchParams.set('action', 'uploaded');
if (response['obj_id']) abs.searchParams.set('obj_id', String(response['obj_id']));
if (response['obj_content_hash']) abs.searchParams.set('obj_content_hash', String(response['obj_content_hash']));
// Keep relative URLs relative for consistency with existing code.
url = abs.origin === window.location.origin ? (abs.pathname + abs.search + abs.hash) : abs.toString();
} catch (_) {
const sep = url.includes('?') ? '&' : '?';
url = url + sep + 'action=uploaded'
+ (response['obj_id'] ? ('&obj_id=' + encodeURIComponent(String(response['obj_id']))) : '')
+ (response['obj_content_hash'] ? ('&obj_content_hash=' + encodeURIComponent(String(response['obj_content_hash']))) : '');
}
req.url = url;
delete req.values;
if (!req.target && this.elem) {
req.target = this.elem;
}
if (this.debugUpload) {
console.info('CQ-UPLOAD-DEBUG htmx-refresh-start', {
url: req.url,
target: req.target,
select: req.select,
swap: req.swap
});
}
const scheduler = typeof req.scheduler === 'function'
? req.scheduler
: (typeof req.scheduler === 'string' ? window[req.scheduler] : null);
if (typeof scheduler === 'function') {
scheduler(req.url, req);
return;
}
htmx.ajax('GET', req.url, req);
}
async doUploadFile(file, obj_text, add_to_clipboard, fileIndex) {
if (this.debugUpload) {
console.info('CQ-UPLOAD-DEBUG upload-file-start', {
name: file && file.name,
size: file && file.size
});
}
let dateObj = new Date(file.lastModified); // Create a Date object
let isoDate = dateObj.toISOString(); // Convert to ISO date string
let incomingProps = typeof this.file_properties === 'function'
? this.file_properties(file, fileIndex || 0)
: this.file_properties;
if (typeof incomingProps === 'string') {
try {
incomingProps = JSON.parse(incomingProps);
} catch (e) {
incomingProps = {};
}
}
const effectiveProfile = (incomingProps && incomingProps._obj_profile) ? incomingProps._obj_profile : "cq_file";
let properties = {
"_obj_profile": effectiveProfile,
"_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
};
Object.assign(properties, incomingProps);
properties['_relative_path'] = file._relativefilePath ? file._relativefilePath : (file.webkitRelativePath ? file.webkitRelativePath : "");
let uploadUrl = this.uploadURL ? this.uploadURL : "/dms/upload?type=file&profile=" + encodeURIComponent(effectiveProfile);
if (this.parent) {
uploadUrl = uploadUrl + "&parent=" + this.parent;
}
uploadUrl = uploadUrl + "&ticket=" + this.ticket;
try {
const t0 = performance.now();
const response = await uploadFile(uploadUrl, file, this.onprogress.bind(this), properties, {}, this.chunkSize);
if (this.debugUpload) {
console.info('CQ-UPLOAD-DEBUG upload-file-response', {
name: file && file.name,
elapsedMs: Math.round(performance.now() - t0),
response: response
});
}
await this.callback(response);
} catch (error) {
console.error('Upload failed:', error);
await this.callback({
status: 'error',
error: error && error.message ? error.message : String(error || 'Upload failed'),
file_name: file && file.name ? file.name : ''
});
}
}
async upload() {
const files = Array.from(this.filesToUpload || []).filter(file => !!file);
if (!files.length) {
return;
}
if (this.debugUpload) {
console.info('CQ-UPLOAD-DEBUG batch-start', {
count: files.length,
parallel: !this.sequentialUpload,
names: files.map(file => file.name)
});
}
this.isUploading = true;
this.dragDepth = 0;
this.batchExpected = files.length;
this.batchCompleted = 0;
this.lastBatchResponse = null;
this.elem.classList.remove('drag-over');
try {
if (this.sequentialUpload) {
for (let idx = 0; idx < files.length; idx++) {
await this.doUploadFile(files[idx], '', undefined, idx);
}
} else {
await Promise.all(files.map((file, idx) => this.doUploadFile(file, '', undefined, idx)));
}
} finally {
if (this.debugUpload) {
console.info('CQ-UPLOAD-DEBUG batch-finish', {
expected: this.batchExpected,
completed: this.batchCompleted,
lastBatchResponse: this.lastBatchResponse
});
}
this.isUploading = false;
this.filesToUpload = [];
this.batchExpected = 0;
this.batchCompleted = 0;
this.lastBatchResponse = null;
this.elem.classList.remove('drag-over');
const versionDropRoot = this.elem.closest ? this.elem.closest('[data-cq-version-upload="1"]') : null;
if (versionDropRoot) {
versionDropRoot.classList.remove('drag-over');
}
this.elem.style.backgroundColor = this.originalBackgroundColor;
}
}
async uploadFiles(files) {
this.filesToUpload = Array.from(files || []).filter(file => !!file);
await this.upload();
}
}
function cqInitVersionUpload(root) {
const scope = root && root.querySelectorAll ? root : document;
const versionRoots = [];
if (scope.matches && scope.matches('[data-cq-version-upload="1"]')) {
versionRoots.push(scope);
}
scope.querySelectorAll('[data-cq-version-upload="1"]').forEach(el => versionRoots.push(el));
versionRoots.forEach(versionsRoot => {
const dropzone = versionsRoot.querySelector('#versions_table_dropzone');
if (!dropzone || typeof DirectUploadHandler === 'undefined') return;
const ticket = versionsRoot.getAttribute('data-cq-ticket') || '';
const oid = versionsRoot.getAttribute('data-cq-oid') || '';
const id = versionsRoot.getAttribute('data-cq-id') || oid;
const nextVersion = versionsRoot.getAttribute('data-cq-next-version') || '2.0';
const detailsUrl = versionsRoot.getAttribute('data-cq-details-url') || '/webui/details';
if (!ticket || !oid) return;
const nextVersionNumber = parseFloat(String(nextVersion).replace(',', '.')) || 2;
function versionForUploadIndex(index) {
return (nextVersionNumber + (index || 0)).toFixed(1);
}
function refreshVersions(url, req) {
const target = document.getElementById(versionsRoot.id) || versionsRoot;
if (!target || !window.htmx) return;
req.target = target;
req.select = '#versions';
req.swap = 'outerHTML';
htmx.ajax('GET', url, req);
}
const progress = versionsRoot.querySelector('[data-cq-version-upload-status]')
|| versionsRoot.querySelector('#spinner')
|| document.getElementById('spinner');
const handler = new DirectUploadHandler({
ticket: ticket,
elem: dropzone,
progress: progress,
file_properties: function (file, index) {
return {'_obj_version': versionForUploadIndex(index)};
},
uploadURL: '/dms/upload?id=' + encodeURIComponent(oid),
chunkSize: 10 * 1024 * 1024,
sequentialUpload: true,
callbackActions: {
url: detailsUrl + '?id=' + encodeURIComponent(id) + '&tab=2',
scheduler: refreshVersions,
select: '#versions',
swap: 'outerHTML'
}
});
const uploadButton = versionsRoot.querySelector('[data-cq-version-upload-button]');
const fileInput = versionsRoot.querySelector('[data-cq-version-file-input]');
if (uploadButton && fileInput && !uploadButton.__cqVersionUploadButtonBound) {
uploadButton.__cqVersionUploadButtonBound = true;
uploadButton.addEventListener('click', function () {
fileInput.click();
});
}
if (fileInput && !fileInput.__cqVersionUploadInputBound) {
fileInput.__cqVersionUploadInputBound = true;
fileInput.addEventListener('change', async function () {
const files = Array.from(fileInput.files || []);
if (!files.length) return;
try {
if (progress) {
progress.textContent = 'uploading...';
}
await handler.uploadFiles(files);
} finally {
fileInput.value = '';
}
});
}
});
}
window.cqInitVersionUpload = cqInitVersionUpload;
function cqInitCheckoutUpload(root) {
const scope = root && root.querySelectorAll ? root : document;
const uploadRoots = [];
if (scope.matches && scope.matches('[data-cq-checkout-upload="1"]')) {
uploadRoots.push(scope);
}
scope.querySelectorAll('[data-cq-checkout-upload="1"]').forEach(el => uploadRoots.push(el));
uploadRoots.forEach(uploadRoot => {
if (typeof DirectUploadHandler === 'undefined') return;
const ticket = uploadRoot.getAttribute('data-cq-ticket') || '';
const id = uploadRoot.getAttribute('data-cq-id') || '';
const refreshUrl = uploadRoot.getAttribute('data-cq-refresh-url') || ('/webui/checkout?id=' + encodeURIComponent(id) + '&action=uploaded');
const returnUrl = uploadRoot.getAttribute('data-cq-return-url') || '';
const returnDelay = parseInt(uploadRoot.getAttribute('data-cq-return-delay') || '900', 10);
if (!ticket || !id) return;
function refreshCheckout(url, req) {
if (!window.htmx) return;
const refreshReq = Object.assign({}, req);
delete refreshReq.scheduler;
delete refreshReq.url;
delete refreshReq.values;
const shouldReturn = !!returnUrl;
refreshReq.target = '#main';
refreshReq.select = '#main';
refreshReq.swap = 'outerHTML';
const result = htmx.ajax('GET', url, refreshReq);
if (!shouldReturn || uploadRoot.__cqCheckoutReturnScheduled) return;
uploadRoot.__cqCheckoutReturnScheduled = true;
Promise.resolve(result).finally(function () {
setTimeout(function () {
const archiveReq = {
target: '#main',
select: '#main',
swap: 'outerHTML',
headers: { 'HX-Request': 'true' }
};
const archiveResult = htmx.ajax('GET', returnUrl, archiveReq);
Promise.resolve(archiveResult).finally(function () {
try {
window.history.pushState({}, '', returnUrl);
} catch (_) {}
});
}, Number.isFinite(returnDelay) ? returnDelay : 900);
});
}
const progress = uploadRoot.querySelector('[data-cq-checkout-upload-status]');
const handler = new DirectUploadHandler({
ticket: ticket,
elem: uploadRoot,
progress: progress,
file_properties: {},
uploadURL: '/dms/upload?id=' + encodeURIComponent(id),
chunkSize: 10 * 1024 * 1024,
sequentialUpload: true,
callbackActions: {
url: refreshUrl,
scheduler: refreshCheckout,
select: '#main',
swap: 'outerHTML'
}
});
const uploadButton = uploadRoot.querySelector('[data-cq-checkout-upload-button]');
const fileInput = uploadRoot.querySelector('[data-cq-checkout-file-input]');
if (uploadButton && fileInput && !uploadButton.__cqCheckoutUploadButtonBound) {
uploadButton.__cqCheckoutUploadButtonBound = true;
uploadButton.addEventListener('click', function () {
fileInput.click();
});
}
if (fileInput && !fileInput.__cqCheckoutUploadInputBound) {
fileInput.__cqCheckoutUploadInputBound = true;
fileInput.addEventListener('change', async function () {
const files = Array.from(fileInput.files || []);
if (!files.length) return;
try {
if (progress) {
progress.textContent = 'uploading...';
}
await handler.uploadFiles(files.slice(0, 1));
} finally {
fileInput.value = '';
}
});
}
});
}
window.cqInitCheckoutUpload = cqInitCheckoutUpload;
function cqVersionDragHasFiles(event) {
const types = event && event.dataTransfer && event.dataTransfer.types;
if (!types) return false;
try {
return Array.from(types).indexOf('Files') !== -1;
} catch (_) {
return false;
}
}
function cqVersionRootFromPoint(event) {
if (!event) return null;
const directRoot = event.target && event.target.closest ? event.target.closest('[data-cq-version-upload="1"]') : null;
if (directRoot) return directRoot;
const roots = Array.from(document.querySelectorAll('[data-cq-version-upload="1"]'));
for (const root of roots) {
const rect = root.getBoundingClientRect();
if (
event.clientX >= rect.left &&
event.clientX <= rect.right &&
event.clientY >= rect.top &&
event.clientY <= rect.bottom
) {
return root;
}
}
return null;
}
function cqSetVersionDragVisual(activeRoot) {
document.querySelectorAll('[data-cq-version-upload="1"]').forEach(root => {
const isActive = !!activeRoot && root === activeRoot;
root.classList.toggle('drag-over', isActive);
const dropzone = root.querySelector('#versions_table_dropzone');
if (dropzone) {
dropzone.classList.toggle('drag-over', isActive);
}
});
}
function cqInstallVersionDragVisuals() {
if (window.__cqVersionDragVisualsInstalled) return;
window.__cqVersionDragVisualsInstalled = true;
document.addEventListener('dragenter', function (event) {
if (!cqVersionDragHasFiles(event)) return;
const root = cqVersionRootFromPoint(event);
if (root) cqSetVersionDragVisual(root);
}, true);
document.addEventListener('dragover', function (event) {
if (!cqVersionDragHasFiles(event)) return;
const root = cqVersionRootFromPoint(event);
cqSetVersionDragVisual(root);
}, true);
document.addEventListener('dragleave', function (event) {
if (!cqVersionDragHasFiles(event)) return;
const next = event.relatedTarget;
if (next && next.closest && next.closest('[data-cq-version-upload="1"]')) return;
setTimeout(function () {
const hovered = document.querySelector('[data-cq-version-upload="1"]:hover');
if (!hovered) cqSetVersionDragVisual(null);
}, 40);
}, true);
document.addEventListener('drop', function () {
cqSetVersionDragVisual(null);
}, true);
document.addEventListener('dragend', function () {
cqSetVersionDragVisual(null);
}, true);
}
window.cqInstallVersionDragVisuals = cqInstallVersionDragVisuals;
cqInstallVersionDragVisuals();
document.addEventListener('DOMContentLoaded', function () {
cqInitVersionUpload(document);
cqInitCheckoutUpload(document);
cqInstallVersionDragVisuals();
});
document.addEventListener('htmx:afterSwap', function (event) {
cqInitVersionUpload(document);
cqInitCheckoutUpload(document);
});
document.addEventListener('htmx:afterSettle', function (event) {
cqInitVersionUpload(document);
cqInitCheckoutUpload(document);
});
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 && entry.isDirectory) {
processDirectory(entry, item.name, files);
} else if (entry && entry.isFile) {
let file = item.getAsFile();
if (file) {
files.push(file);
}
}
} else if (item.kind === 'file') {
let file = item.getAsFile();
if (file) {
files.push(file);
}
}
}
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 cqEnsureModalRoot() {
// Prefer a dedicated modal root outside #main (survives HTMX swaps / detail overlay)
return document.getElementById('modal-root') || document.getElementById('dialog') || document.body;
}
// Robust Bootstrap modal stacking (supports modal-in-modal)
(function cqModalStacking(){
if (window.__cqModalStackingInstalled) return;
window.__cqModalStackingInstalled = true;
function getBaseZ() {
// IMPORTANT:
// Avoid "near int32 max" z-index values (2147483xxx).
// Chrome clamps around 2147483647, and then backdrop/modal ordering can flip.
// We keep a sane ladder:
// cq-detail-backdrop ~ 1990
// cq-detail-layer ~ 2000
// bootstrap backdrops/modals start at 3000+
const detailLayer = document.getElementById('detail_layer');
const detailHasContent = !!(detailLayer && (detailLayer.innerHTML || '').trim());
return (document.body.classList.contains('cq-detail-open') || detailHasContent) ? 3000 : 1055;
}
function getTopModalZ(ignoreModal) {
let top = getBaseZ();
document.querySelectorAll('.modal.show').forEach(m => {
if (ignoreModal && m === ignoreModal) return;
const z = parseInt(window.getComputedStyle(m).zIndex || '0', 10);
if (!Number.isNaN(z)) top = Math.max(top, z);
});
return top;
}
window.cqRaiseModal = function (modal) {
if (!modal || !modal.classList || !modal.classList.contains('modal')) return;
const z = getTopModalZ(modal) + 20;
modal.style.zIndex = String(z);
const root = cqEnsureModalRoot();
if (root && modal.parentElement !== root) {
root.appendChild(modal);
}
const backdrops = Array.from(document.querySelectorAll('.modal-backdrop'));
const bd = backdrops.length ? backdrops[backdrops.length - 1] : null;
if (bd) bd.style.zIndex = String(z - 10);
};
document.addEventListener('show.bs.modal', function (e) {
const modal = e.target;
if (!modal || !modal.classList || !modal.classList.contains('modal')) return;
window.cqRaiseModal(modal);
[0, 50, 150].forEach(function (delay) {
setTimeout(function () { window.cqRaiseModal(modal); }, delay);
});
});
// When hiding one modal but others remain open, keep body.modal-open so scrolling doesn't break
document.addEventListener('hidden.bs.modal', function () {
setTimeout(function () {
const anyOpen = !!document.querySelector('.modal.show');
if (anyOpen) document.body.classList.add('modal-open');
}, 0);
});
})();
function showDialog(url, cssSelector) {
const root = cqEnsureModalRoot();
if (!root || !window.htmx || typeof window.htmx.ajax !== 'function') {
return Promise.resolve(null);
}
try {
root.querySelectorAll(cssSelector).forEach(function (existingEl) {
if (existingEl.classList && existingEl.classList.contains('show')) return;
const staleSlot = existingEl.closest('[data-cq-modal-slot="1"]');
if (staleSlot && staleSlot.parentNode) {
staleSlot.parentNode.removeChild(staleSlot);
} else if (existingEl.parentNode) {
existingEl.parentNode.removeChild(existingEl);
}
});
} catch (_) {}
window.__cqDialogInflight = window.__cqDialogInflight || new Set();
const inflightKey = String(cssSelector || '#viewerModal') + '::' + String(url || '');
if (window.__cqDialogInflight.has(inflightKey)) {
return Promise.resolve(null);
}
window.__cqDialogInflight.add(inflightKey);
const slot = document.createElement('div');
slot.setAttribute('data-cq-modal-slot', '1');
root.appendChild(slot);
function cqSanitizeDialogHtml(html) {
let out = String(html || '');
// Old portal edit templates could render bytes tab ids as b'main'. htmx
// builds selectors from response ids before swap and crashes on quotes.
out = out.replace(/(cq-edit-tab-)(?:\\?['"])?b\\?['"]([^'"\\<>]+)\\?['"]b(?:\\?['"])?/g, '$1$2');
out = out.replace(/(cq-edit-tab-)(?:&(?:#x27|#39|apos);)?b(?:&(?:#x27|#39|apos);)([^&"'<>\s]+)(?:&(?:#x27|#39|apos);)b(?:&(?:#x27|#39|apos);)?/g, '$1$2');
return out;
}
function cqSanitizeInlineScriptSource(source) {
return String(source || '').replace(
/\b(?:const|let)\s+(cqParentBackUrl|cqParentBackdropUrl)\s*=/g,
'window.$1 ='
);
}
function cqExecuteDialogScripts(container) {
try {
Array.from(container.querySelectorAll('script')).forEach(function (oldScript) {
const script = document.createElement('script');
Array.from(oldScript.attributes || []).forEach(function (attr) {
script.setAttribute(attr.name, attr.value);
});
script.text = cqSanitizeInlineScriptSource(oldScript.textContent);
oldScript.parentNode.insertBefore(script, oldScript);
oldScript.parentNode.removeChild(oldScript);
});
} catch (_) {}
}
const useRawFetch = String(url || '').indexOf('/portal/contract-documents') !== -1;
const loadDialog = useRawFetch
? fetch(url, {
credentials: 'same-origin',
headers: { 'HX-Request': 'true', 'X-Requested-With': 'XMLHttpRequest' }
}).then(function (response) {
if (!response.ok) throw new Error('Dialog request failed');
return response.text();
}).then(function (html) {
slot.innerHTML = cqSanitizeDialogHtml(html);
cqExecuteDialogScripts(slot);
if (window.htmx && typeof window.htmx.process === 'function') {
window.htmx.process(slot);
}
})
: htmx.ajax('GET', url, { target: slot, swap: 'innerHTML' });
return loadDialog.then(() => {
const el = slot.querySelector(cssSelector);
if (!el) {
if (slot.parentNode) slot.parentNode.removeChild(slot);
return null;
}
const cleanupAfterHide = function () {
try {
if (window.bootstrap && bootstrap.Modal && typeof bootstrap.Modal.getInstance === 'function') {
const inst = bootstrap.Modal.getInstance(el);
if (inst) inst.dispose();
}
} catch (_) {}
try {
if (slot.parentNode) {
slot.parentNode.removeChild(slot);
} else if (el.parentNode) {
el.parentNode.removeChild(el);
}
} catch (_) {}
setTimeout(function () {
const anyOpen = !!document.querySelector('.modal.show');
if (!anyOpen) {
document.querySelectorAll('.modal-backdrop').forEach(function (bd) { bd.remove(); });
document.body.classList.remove('modal-open');
document.body.style.removeProperty('padding-right');
}
}, 0);
};
try {
el.addEventListener('hidden.bs.modal', cleanupAfterHide, { once: true });
} catch (_) {}
// Bootstrap API preferred; fall back to jQuery if present.
// Value assistance can open from the detail drawer; the drawer already owns
// the page backdrop, so a second Bootstrap backdrop may cover the picker.
const isValueAssistance = cssSelector === '#valueAssistanceModal';
if (isValueAssistance) {
el.setAttribute('data-bs-backdrop', 'false');
}
const modalOptions = { backdrop: isValueAssistance ? false : true, focus: true };
try {
const inst = bootstrap.Modal.getOrCreateInstance(el, modalOptions);
if (window.cqRaiseModal) window.cqRaiseModal(el);
inst.show();
if (window.cqRaiseModal) {
[0, 50, 150, 300].forEach(function (delay) {
setTimeout(function () { window.cqRaiseModal(el); }, delay);
});
}
try {
if (window.cqInitInvoiceForms) {
setTimeout(function () { window.cqInitInvoiceForms(el); }, 0);
}
} catch (_) {}
return inst;
} catch (_) {
if (window.$ && $(el).modal) {
$(el).one('hidden.bs.modal', cleanupAfterHide);
if (window.cqRaiseModal) window.cqRaiseModal(el);
$(el).modal(isValueAssistance ? modalOptions : 'show');
if (window.cqRaiseModal) {
[0, 50, 150, 300].forEach(function (delay) {
setTimeout(function () { window.cqRaiseModal(el); }, delay);
});
}
}
try {
if (window.cqInitInvoiceForms) {
setTimeout(function () { window.cqInitInvoiceForms(el); }, 0);
}
} catch (_) {}
return null;
}
}).catch(function () {
try { if (slot.parentNode) slot.parentNode.removeChild(slot); } catch (_) {}
return null;
}).finally(function () {
try { window.__cqDialogInflight.delete(inflightKey); } catch (_) {}
});
}
// Ensure modal-window links always open as Bootstrap modal, even after HTMX table swaps.
(function cqModalLinkInterceptor() {
if (window.__cqModalLinkInterceptorInstalled) return;
window.__cqModalLinkInterceptorInstalled = true;
document.addEventListener('click', function (ev) {
const trigger = ev.target && ev.target.closest ? ev.target.closest('[hx-get]') : null;
if (!trigger) return;
if (trigger.getAttribute('data-cq-no-modal-intercept') === '1') return;
const hxGet = (trigger.getAttribute('hx-get') || '').trim();
if (!hxGet) return;
const low = hxGet.toLowerCase();
const explicit = trigger.getAttribute('data-cq-open-modal') === '1';
const isModalUrl = low.indexOf('modal_window.html') !== -1 || low.indexOf('invoice_modal_window.html') !== -1;
if (!explicit && !isModalUrl) return;
const selector = trigger.getAttribute('data-cq-modal-selector') || '#viewerModal';
ev.preventDefault();
ev.stopPropagation();
showDialog(hxGet, selector);
}, true);
})();
// Portal contract lists keep their legacy document URL as fallback, but normal clicks
// should open the linked contract documents in the same modal pattern as admin pages.
(function cqPortalContractDocumentsInterceptor() {
if (window.__cqPortalContractDocumentsInterceptorInstalled) return;
window.__cqPortalContractDocumentsInterceptorInstalled = true;
function getPortalContractDocumentsUrl(link) {
if (!link) return '';
const href = (link.getAttribute('href') || '').trim();
if (!href) return '';
const url = new URL(href, window.location.href);
const isPortalView = url.pathname === '/portal/view' || (url.pathname === '/view' && window.location.pathname.indexOf('/portal/') === 0);
if (!isPortalView) return '';
const listId = (url.searchParams.get('listx') || url.searchParams.get('list') || '').trim();
const objId = (url.searchParams.get('id') || '').trim();
if (!objId || !/^list_cm_/i.test(listId)) return '';
return '/portal/contract-documents?id=' + encodeURIComponent(objId) + '&listx=' + encodeURIComponent(listId);
}
function normalizePortalContractDocumentLinks(root) {
try {
const scope = root && root.querySelectorAll ? root : document;
scope.querySelectorAll('a[href]').forEach(function (link) {
const modalUrl = getPortalContractDocumentsUrl(link);
if (!modalUrl) return;
link.setAttribute('href', modalUrl);
link.removeAttribute('target');
link.setAttribute('data-cq-contract-documents-modal', '1');
if (!link.getAttribute('title') || link.getAttribute('title') === 'Document') {
link.setAttribute('title', 'Documents');
}
if (!link.getAttribute('aria-label') || link.getAttribute('aria-label') === 'Document') {
link.setAttribute('aria-label', 'Documents');
}
});
} catch (_) {}
}
function installPortalContractDocumentsInterceptor() {
normalizePortalContractDocumentLinks(document);
if (document.body) {
document.body.addEventListener('htmx:afterSwap', function (ev) {
normalizePortalContractDocumentLinks(ev && ev.target ? ev.target : document);
});
}
document.addEventListener('draw.dt', function (ev) {
normalizePortalContractDocumentLinks(ev && ev.target ? ev.target : document);
});
}
if (document.body) {
installPortalContractDocumentsInterceptor();
} else {
document.addEventListener('DOMContentLoaded', installPortalContractDocumentsInterceptor, { once: true });
}
document.addEventListener('click', function (ev) {
try {
const link = ev.target && ev.target.closest ? ev.target.closest('a[href]') : null;
if (!link || ev.defaultPrevented) return;
if (ev.button || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
let modalUrl = link.getAttribute('data-cq-contract-documents-modal') === '1'
? (link.getAttribute('href') || '').trim()
: getPortalContractDocumentsUrl(link);
if (!modalUrl) return;
if (typeof window.showDialog !== 'function') return;
ev.preventDefault();
ev.stopPropagation();
if (typeof ev.stopImmediatePropagation === 'function') ev.stopImmediatePropagation();
window.showDialog(modalUrl, '#viewerModal');
} catch (_) {}
}, true);
})();
function getVAFilterFields(fields, contextEl) {
const data = {};
const ids = Array.isArray(fields) ? fields : [];
const roots = [];
const evtTarget = (function () {
try {
const ev = window.event;
return ev && (ev.target || ev.srcElement) ? (ev.target || ev.srcElement) : null;
} catch (_) {
return null;
}
})();
const pushRoot = (node) => {
if (!node || !node.querySelector || roots.includes(node)) return;
roots.push(node);
};
const getClosest = (node, selector) => {
try {
return node && node.closest ? node.closest(selector) : null;
} catch (_) {
return null;
}
};
const active = document.activeElement;
pushRoot(getClosest(evtTarget, 'form'));
pushRoot(getClosest(evtTarget, '[data-cq-tab-host]'));
pushRoot(getClosest(evtTarget, '#detail_layer'));
pushRoot(getClosest(contextEl, 'form'));
pushRoot(getClosest(active, 'form'));
pushRoot(getClosest(contextEl, '.modal.show'));
pushRoot(getClosest(active, '.modal.show'));
pushRoot(document.querySelector('.modal.show'));
pushRoot(getClosest(contextEl, '#detail_layer'));
pushRoot(getClosest(active, '#detail_layer'));
pushRoot(document.getElementById('detail_layer'));
const pickElementById = (id) => {
const selector = `[id="${String(id).replace(/"/g, '\\"')}"]`;
for (const root of roots) {
const found = root.querySelector(selector);
if (found) return found;
}
const all = Array.from(document.querySelectorAll(selector));
if (!all.length) return null;
const visible = all.find((el) => {
try {
return !!(el.offsetParent || (el.getClientRects && el.getClientRects().length));
} catch (_) {
return false;
}
});
return visible || all[0];
};
ids.forEach((id) => {
const element = pickElementById(id);
if (!element) return;
const name = element.name;
if (!name) return;
data[name] = element.value;
});
return encodeURIComponent(JSON.stringify(data));
}
var ValidationController = window.ValidationController;
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;
if (inputElement._tagify.dropdown) {
inputElement._tagify.settings.dropdown = Object.assign({}, inputElement._tagify.settings.dropdown || {}, {
enabled: 0,
maxItems: 200,
closeOnSelect: false,
highlightFirst: true
});
}
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,
dropdown: {
enabled: 0,
maxItems: 200,
closeOnSelect: false,
highlightFirst: true
}
});
// Store the instance to avoid re-init
inputElement._tagify = tagify;
inputElement.classList.add('cq-tagify-picker-source');
if (tagify.DOM && tagify.DOM.scope) {
tagify.DOM.scope.classList.add('cq-tagify-picker');
tagify.DOM.scope.setAttribute('title', 'Click to choose values');
}
function showAllTagifyOptions() {
try {
tagify.dropdown.show.call(tagify, '');
} catch (e) {
// ignore
}
}
if (tagify.DOM && tagify.DOM.scope) {
tagify.DOM.scope.addEventListener('click', showAllTagifyOptions);
}
if (tagify.DOM && tagify.DOM.input) {
tagify.DOM.input.addEventListener('focus', showAllTagifyOptions);
tagify.DOM.input.addEventListener('click', showAllTagifyOptions);
}
// 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);
ev.target.dispatchEvent(new Event('input'));
})
.on('typeahead:asyncrequest', function() {
})
.on('typeahead:asyncreceive', function() {
})
.on('typeahead:asynccancel', function() {
});
}
var tableSelection = window.tableSelection || (window.tableSelection = {});
function cqSelectionKeysToIds(keys) {
if (window.CQDatatable && typeof window.CQDatatable.keysToIds === 'function') {
return window.CQDatatable.keysToIds(keys);
}
return (keys || [])
.filter(key => typeof key === 'string' && key.startsWith('chk_'))
.map(key => key.replace(/^chk_/, ''));
}
function cqGetCheckedSelectionKeysFromDom() {
if (window.CQDatatable && typeof window.CQDatatable.getCheckedKeys === 'function') {
return window.CQDatatable.getCheckedKeys(document);
}
return Array.from(document.querySelectorAll('.row-checkbox:checked'))
.map(el => el && el.id)
.filter(Boolean);
}
function cqPruneTableSelectionToDom() {
if (window.CQDatatable && typeof window.CQDatatable.pruneToDom === 'function') {
window.CQDatatable.pruneToDom(document);
return;
}
const checkedKeys = new Set(cqGetCheckedSelectionKeysFromDom());
Object.keys(tableSelection).forEach(function(key) {
if (!checkedKeys.has(key)) {
delete tableSelection[key];
}
});
}
function cqGetSelectedIds() {
if (window.CQDatatable && typeof window.CQDatatable.getSelectedIds === 'function') {
return window.CQDatatable.getSelectedIds({ scope: document, preferDom: true });
}
// DOM is the single source of truth for destructive/export actions.
cqPruneTableSelectionToDom();
return cqSelectionKeysToIds(Object.keys(tableSelection));
}
function cqClearTableSelection(uncheckDom = true) {
if (window.CQDatatable && typeof window.CQDatatable.clearSelection === 'function') {
window.CQDatatable.clearSelection({ scope: document, uncheckDom: !!uncheckDom, clearStore: true });
return;
}
Object.keys(tableSelection).forEach(function(key) { delete tableSelection[key]; });
if (uncheckDom) {
document.querySelectorAll('.row-checkbox:checked').forEach(function(cb) { cb.checked = false; });
document.querySelectorAll('input[type="checkbox"][id^="all_"], input[type="checkbox"][id*="all_"]').forEach(function(cb) { cb.checked = false; });
}
showHideDropdownMenu(false);
}
if (!window.__cqSelectionGuardInstalled) {
window.__cqSelectionGuardInstalled = true;
const bindSelectionGuard = function() {
const root = document.body || document;
if (!root || !root.addEventListener) return;
root.addEventListener('htmx:beforeSwap', function(evt) {
const t = evt && evt.detail && evt.detail.target;
if (t && t.id === 'main') {
cqClearTableSelection(false);
}
});
root.addEventListener('htmx:afterSwap', function(evt) {
const t = evt && evt.detail && evt.detail.target;
if (t && t.id === 'main') {
cqClearTableSelection(true);
}
});
};
if (document.body) {
bindSelectionGuard();
} else {
document.addEventListener('DOMContentLoaded', bindSelectionGuard);
}
}
if (!window.__cqRefreshJobs) {
window.__cqRefreshJobs = {};
}
if (!window.cqScheduleRefresh) {
window.cqScheduleRefresh = function(key, runner, opts) {
if (!key || typeof runner !== 'function') return;
const options = opts || {};
const minIntervalMs = Math.max(0, Number(options.minIntervalMs || 0));
const delayMs = Math.max(0, Number(options.delayMs || 0));
const jobs = window.__cqRefreshJobs || (window.__cqRefreshJobs = {});
const job = jobs[key] || (jobs[key] = {
timer: null,
inFlight: false,
pending: false,
lastRunTs: 0,
runner: null,
opts: {}
});
job.runner = runner;
job.opts = options;
if (job.inFlight) {
job.pending = true;
return;
}
if (job.timer) {
window.clearTimeout(job.timer);
job.timer = null;
}
const now = Date.now();
const elapsed = Math.max(0, now - Number(job.lastRunTs || 0));
const waitMs = Math.max(delayMs, minIntervalMs - elapsed);
job.timer = window.setTimeout(function() {
job.timer = null;
if (job.inFlight) {
job.pending = true;
return;
}
job.inFlight = true;
Promise.resolve(job.runner())
.catch(function() { })
.finally(function() {
job.inFlight = false;
job.lastRunTs = Date.now();
if (job.pending) {
job.pending = false;
window.cqScheduleRefresh(key, job.runner, job.opts);
}
});
}, waitMs);
};
}
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 (window.CQDatatable && typeof window.CQDatatable.syncCheckbox === 'function') {
window.CQDatatable.syncCheckbox({ id: objId, checked: !!isChecked }, !!isChecked);
} else if (isChecked) {
tableSelection[objId] = true;
} else {
delete tableSelection[objId];
}
cqPruneTableSelectionToDom();
const hasSelections = Object.keys(tableSelection).length > 0;
showHideDropdownMenu(hasSelections);
}
async function addToFavorites(overview_url) {
let selection = cqGetSelectedIds();
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"});
}
function cqEnsureDeleteIndicator() {
let el = document.getElementById('cq-delete-job-indicator');
if (el) return el;
el = document.createElement('div');
el.id = 'cq-delete-job-indicator';
el.style.position = 'fixed';
el.style.right = '1rem';
el.style.bottom = '1rem';
el.style.zIndex = '2147483000';
el.style.minWidth = '260px';
el.style.maxWidth = '360px';
el.style.padding = '10px 12px';
el.style.borderRadius = '10px';
el.style.background = 'rgba(20,20,24,0.95)';
el.style.color = '#fff';
el.style.boxShadow = '0 10px 24px rgba(0,0,0,0.35)';
el.style.fontSize = '12px';
el.style.lineHeight = '1.35';
el.style.display = 'none';
el.innerHTML =
'' +
' ' +
' Löschen...' +
'
' +
'0/0
' +
'';
document.body.appendChild(el);
return el;
}
function cqUpdateDeleteIndicator(done, total, failed) {
const root = cqEnsureDeleteIndicator();
if (!root) return;
root.style.display = 'block';
const text = root.querySelector('#cq-delete-job-text');
const bar = root.querySelector('#cq-delete-job-bar');
const title = root.querySelector('#cq-delete-job-title');
const safeTotal = Math.max(Number(total || 0), 1);
const pct = Math.max(0, Math.min(100, Math.round((Number(done || 0) / safeTotal) * 100)));
if (title) title.textContent = failed ? `Löschen... (${failed} Fehler)` : 'Löschen...';
if (text) text.textContent = `${done}/${total} verarbeitet`;
if (bar) bar.style.width = `${pct}%`;
}
function cqHideDeleteIndicator() {
const root = document.getElementById('cq-delete-job-indicator');
if (!root) return;
root.style.display = 'none';
}
async function cqRunBulkDelete(selection, overview_url, deleteFn) {
const amount = selection.length;
if (!amount) return;
if (window.__cqBulkDeleteRunning) {
if (window.cqToast && window.cqToast.warning) {
window.cqToast.warning('Loeschvorgang laeuft bereits.', { delay: 2200 });
}
return;
}
window.__cqBulkDeleteRunning = true;
document.dispatchEvent(new CustomEvent('cq:bulk-delete:start', { detail: { total: amount } }));
let done = 0;
let failed = 0;
cqUpdateDeleteIndicator(done, amount, failed);
try {
for (const id of selection) {
try {
const resp = await deleteFn(id);
if (!resp || !resp.ok) failed += 1;
} catch (error) {
failed += 1;
console.error(`Error deleting entry with id ${id}:`, error);
} finally {
done += 1;
cqUpdateDeleteIndicator(done, amount, failed);
document.dispatchEvent(new CustomEvent('cq:bulk-delete:progress', {
detail: { done: done, total: amount, failed: failed }
}));
await new Promise(resolve => window.setTimeout(resolve, 0));
}
}
htmx.ajax('GET', overview_url, { target: "#main", swap: "outerHTML", select: "#main" });
} finally {
window.__cqBulkDeleteRunning = false;
document.dispatchEvent(new CustomEvent('cq:bulk-delete:end', {
detail: { done: done, total: amount, failed: failed }
}));
window.setTimeout(cqHideDeleteIndicator, 900);
}
}
function cqBuildActionUrl(baseUrl, params) {
const url = new URL(baseUrl || window.location.href, window.location.origin);
Object.entries(params || {}).forEach(function(entry) {
const key = entry[0];
const value = entry[1];
if (value !== undefined && value !== null && String(value) !== '') {
url.searchParams.set(key, value);
}
});
return url.toString();
}
async function deleteEntries(overview_url) {
const selection = cqGetSelectedIds();
const amount = selection.length; // Get the number of selected entries
if (!amount) {
alert("Bitte waehle mindestens einen Eintrag aus.");
return;
}
const confirmDeletion = window.confirm(`Are you sure that you want to delete the selected ${amount} entries?`);
if (!confirmDeletion) {
return;
}
await cqRunBulkDelete(selection, overview_url, function(id) {
return fetch(cqBuildActionUrl(overview_url, { action: 'set_delete', oid: id }), {
method: 'GET',
credentials: 'same-origin',
cache: 'no-store',
headers: { 'HX-Request': 'true' }
});
});
}
async function undeleteEntries(overview_url) {
let selection = cqGetSelectedIds();
let amount = selection.length; // Get the number of selected entries
if (!amount) {
alert("Bitte waehle mindestens einen Eintrag aus.");
return;
}
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(cqBuildActionUrl(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 = cqGetSelectedIds();
const amount = selection.length;
if (!amount) {
alert("Bitte waehle mindestens einen Eintrag aus.");
return;
}
// Confirm the merge action
const confirm = window.confirm(`Are you sure that you want to merge the selected ${amount} entries?`);
if (!confirm) {
// 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);
}
}
async function splitEntries(overview_url) {
// 1) Selektion sammeln
const selection = cqGetSelectedIds();
const amount = selection.length;
if (!amount) {
alert("No entries selected.");
return;
}
// 2) Split-Modus wählen
const mode = window.prompt(
"Split mode:\n" +
"1 = Splitte jede N Seite\n" +
"2 = nach Seitenbereichen aufgeteilt (z. B. 1-3, 4-6)\n\n" +
"Geben Sie 1 oder 2 ein:",
"1"
);
if (!mode) return;
let params = new URLSearchParams();
params.set("action", "split_pdf");
params.set("selection", selection.join(","));
if (mode === "1") {
// 3a) Split alle N Seiten
const everyN = window.prompt(
"Alle wie viele Seiten teilen?",
"1"
);
if (!everyN || isNaN(everyN) || Number(everyN) <= 0) {
alert("Ungültige Seitenanzahl.");
return;
}
params.set("mode", "every_n_pages");
params.set("every_n_pages", everyN);
} else if (mode === "2") {
// 3b) Split nach expliziten Bereichen
const ranges = window.prompt(
"Geben Sie Seitenbereiche ein (beginnend mit 1), z. B.\n" +
"1-3,4-6\n\n" +
"Jeder Bereich erstellt eine PDF-Datei.:",
""
);
if (!ranges) return;
params.set("mode", mode);
params.set("ranges", ranges);
params.set("parts", ranges);
} else {
alert("Ungültiger Modus.");
return;
}
// 4) Bestätigung
const confirm = window.confirm(
`Are you sure you want to split ${amount} selected entr${amount === 1 ? "y" : "ies"}?`
);
if (!confirm) return;
// 5) Backend-Aufruf
try {
const response = await fetch(`${overview_url}?${params.toString()}`, {
method: "POST"
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// Optional: reload / refresh
// location.reload();
} catch (error) {
console.error("Error splitting entries:", error);
alert("Fehler beim Teilen von PDF-Dateien. Details finden Sie in der Konsole.");
}
}
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 showVersionDialog(obj_id, version_comment) {
const referer = window.location.href;
const url = `/invoice/includes/modal_window.html?incl=change_version_comment&referer=${encodeURIComponent(referer)}&obj_id=${obj_id}&obj_version_comment=${encodeURIComponent(version_comment)}&title=Change version comment`;
const response = await showDialog(url, '#viewerModal'); // Pass the URL into showDialog
if (response.ok) {
window.location.reload();
} else {
console.error("Server returned error:", response.status);
}
}
async function changeProfile() {
const profileEl = document.getElementById('obj_profile');
if (!profileEl) return;
const profile = profileEl.value;
const urlParams = new URLSearchParams(window.location.search);
function readFirstValue(selectors) {
for (const s of selectors) {
const el = document.querySelector(s);
if (!el) continue;
const v = (el.value || el.getAttribute('value') || '').trim();
if (v) return v;
}
return '';
}
function parseParamFromUrl(raw, key) {
if (!raw) return '';
try {
const u = new URL(raw, window.location.origin);
return (u.searchParams.get(key) || '').trim();
} catch (_) {
return '';
}
}
// IMPORTANT:
// In drawer mode window.location often still points to the parent list URL.
// Therefore prefer IDs from current detail form / current detail link.
const detailHref = (window._cqDetailHref || '').trim();
const id = readFirstValue([
'#_obj_id',
'input[name="_obj_id"]',
'#obj_id',
'input[name="obj_id"]',
'input[name="id"]'
]) || parseParamFromUrl(detailHref, 'id') || (urlParams.get("id") || '').trim();
const parent = readFirstValue([
'#_parent',
'input[name="_parent"]',
'input[name="parent"]'
]) || parseParamFromUrl(detailHref, 'parent') || (urlParams.get("parent") || '').trim();
if (!id) return;
try {
// 1) Persist profile change using archive action (known backend save path)
const saveUrl = `/webui/archive?id=${encodeURIComponent(id)}&action=change_profile&profile=${encodeURIComponent(profile)}`;
const saveResponse = await fetch(saveUrl, {
method: 'GET',
credentials: 'same-origin',
cache: 'no-store',
headers: { 'HX-Request': 'true' }
});
if (!saveResponse.ok) {
console.error("Profile change save failed:", saveResponse.status);
return;
}
// 2) Reload detail mask for the same object (dialog stays open)
const detailUrl = `/webui/details?id=${encodeURIComponent(id)}${parent ? `&parent=${encodeURIComponent(parent)}` : ''}&profile=${encodeURIComponent(profile)}&profile_changed=1`;
if (window.htmx && typeof window.htmx.ajax === 'function') {
const detailLayer = document.getElementById('detail_layer');
const modalBody = document.querySelector('#viewerModal .modal-body');
const isDetailOpen = document.body.classList.contains('cq-detail-open') ||
(!!detailLayer && String(detailLayer.innerHTML || '').trim().length > 0);
if (isDetailOpen && detailLayer) {
window.htmx.ajax('GET', detailUrl, { target: '#detail_layer', swap: 'innerHTML' });
return;
}
if (modalBody) {
window.htmx.ajax('GET', detailUrl, { target: '#viewerModal .modal-body', swap: 'innerHTML' });
return;
}
window.htmx.ajax('GET', detailUrl, { target: '#main', select: '#main', swap: 'outerHTML' });
return;
}
// Fallback only if htmx is not available
window.location.assign(detailUrl);
} catch (error) {
console.error(`Error changing profile for entry with id ${id}:`, error);
}
}
async function deleteTableEntries(overview_url, aspect = null) {
// Auswahl extrahieren
let selection = cqGetSelectedIds();
let amount = selection.length;
// Wenn keine Checkbox ausgewaehlt wurde
if (amount === 0) {
alert("Bitte waehle mindestens einen Eintrag aus, den du loeschen moechtest.");
return;
}
// Bestaetigungsdialog
let confirmDeletion = window.confirm(
`Are you sure that you want to delete the selected ${amount} entries?`
);
if (!confirmDeletion) return;
await cqRunBulkDelete(selection, overview_url, function(id) {
return fetch(cqBuildActionUrl(overview_url, { id: id, action: 'delete', aspect: aspect }), {
method: 'GET',
credentials: 'same-origin',
cache: 'no-store',
headers: { 'HX-Request': 'true' }
});
});
}
/*
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 = cqGetSelectedIds();
if (!ids.length) { alert('Keine Einträge ausgewählt'); 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 = cqGetSelectedIds();
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 addAddYearForProjectEntries(overview_url) {
let selection = cqGetSelectedIds();
if (selection.length === 0) {
alert('Bitte wählen Sie zuerst mindestens einen Eintrag aus');
return;
}
// Jahr abfragen
let year = prompt('Bitte geben Sie das Jahr ein (z. B. 2026):');
if (!year) {
return;
}
year = year.trim();
// einfache Validierung (4-stelliges Jahr)
if (!/^\d{4}$/.test(year)) {
alert('Bitte geben Sie ein gültiges Jahr im Format YYYY ein');
return;
}
let confirmCreation = window.confirm(
`Möchten Sie die Einträge wirklich für das Jahr ${year} erstellen?`
);
if (!confirmCreation) {
return;
}
for (let id of selection) {
try {
await fetch(
`${overview_url}?action=add_add_year_for_project`
+ `&id=${id}`
+ `&references=${id}`
+ `&year=${year}`
);
} catch (error) {
console.error(
`was not able to create new year for project with id ${id}:`,
error
);
}
}
// Nach Abschluss neu laden
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);
}
}
async function changeClientAccounting(overview_url) {
const client = document.getElementById('client');
if (!client) {
return;
}
try {
const url = new URL(overview_url, window.location.origin);
url.searchParams.set('client', client.value || '');
url.searchParams.set('action', 'change_default_client_accounting');
window.location.href = url.toString();
} catch (error) {
console.error("Error changing client accounting:", error);
}
}
class WebSocketClient {
constructor(config = {}) {
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
const path = (config && config.path) ? config.path : '/ws/jsonrpc';
const singleton = !(config && config.singleton === false);
// Reuse one WS client per endpoint in the same browser tab.
if (singleton) {
window.__cqWsClients = window.__cqWsClients || {};
const key = `${proto}//${host}${path}`;
if (window.__cqWsClients[key]) {
return window.__cqWsClients[key];
}
this._wsClientKey = key;
}
// Default configuration
this.config = {
host: host,
protocol: proto,
path: path,
reconnectMaxAttempts: 8,
reconnectBaseDelay: 1000,
reconnectJitterMs: 400,
maxDelay: 30000,
reconnectRetryForever: true,
connectionWarningEnabled: true,
connectionWarningDelayMs: 12000,
debug: false,
...config
};
this.socket = null;
this.reconnectAttempt = 0;
this.reconnectTimeout = null;
this.messageHandler = null;
this.subscriptions = [];
this._closedByUser = false;
this._maxAttemptsNotified = false;
this._connectionWarningTimer = null;
this._connectionWarningShown = false;
this._boundOnlineHandler = this._handleOnline.bind(this);
this._boundVisibilityHandler = this._handleVisibilityChange.bind(this);
window.addEventListener('online', this._boundOnlineHandler);
document.addEventListener('visibilitychange', this._boundVisibilityHandler);
if (singleton && this._wsClientKey) {
window.__cqWsClients[this._wsClientKey] = this;
}
}
connect() {
this._closedByUser = false;
if (this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING)) {
return;
}
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;
this._maxAttemptsNotified = false;
this._clearStaleConnectionWarning();
this._clearConnectionWarningTimer();
// 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) {
this.log('Error processing message:', 'error', error);
}
});
this.socket.addEventListener('close', () => {
this.log('Disconnected from WebSocket server');
this._scheduleConnectionWarning();
this.handleReconnection();
});
this.socket.addEventListener('error', () => {
const state = this.socket ? this.socket.readyState : -1;
this.log(`WebSocket error (readyState=${state}, path=${this.config.path})`, 'warn');
});
}
// Handle reconnection logic
handleReconnection() {
if (this._closedByUser) return;
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
}
const maxAttempts = Number.isFinite(this.config.reconnectMaxAttempts) ? this.config.reconnectMaxAttempts : 0;
const withinAttempts = this.reconnectAttempt < maxAttempts;
const canRetry = withinAttempts || !!this.config.reconnectRetryForever;
if (!canRetry) {
this.log('Maximum reconnection attempts reached', 'warn');
if (this.config.onMaxReconnectAttemptsReached) {
this.config.onMaxReconnectAttemptsReached();
}
return;
}
if (!withinAttempts && !this._maxAttemptsNotified) {
this._maxAttemptsNotified = true;
this.log('Reconnect attempts exceeded, continuing with slow retry mode', 'warn');
// Keep legacy callback only when retry is finite.
if (!this.config.reconnectRetryForever && this.config.onMaxReconnectAttemptsReached) {
this.config.onMaxReconnectAttemptsReached();
}
}
const baseDelay = Math.min(
this.config.reconnectBaseDelay * Math.pow(2, Math.min(this.reconnectAttempt, maxAttempts || 8)),
this.config.maxDelay
);
const jitter = Math.floor(Math.random() * Math.max(0, this.config.reconnectJitterMs || 0));
const delay = baseDelay + jitter;
this.log(
`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempt + 1}${maxAttempts ? ('/' + maxAttempts) : ''})`
);
this.reconnectTimeout = setTimeout(() => {
this.reconnectAttempt++;
this.connect();
}, delay);
}
// Logging utility
log(message, level = 'info', ...args) {
if (this.config.debug) {
const timestamp = new Date().toISOString();
const prefix = `[WebSocketClient ${timestamp}]`;
switch (level) {
case 'error':
console.error(prefix, message, ...args);
break;
case 'warn':
console.warn(prefix, message, ...args);
break;
default:
}
}
}
// Cleanup and close connection
disconnect() {
this._closedByUser = true;
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
}
this._clearConnectionWarningTimer();
if (this.socket) {
this.socket.close();
}
window.removeEventListener('online', this._boundOnlineHandler);
document.removeEventListener('visibilitychange', this._boundVisibilityHandler);
}
_handleOnline() {
if (this._closedByUser) return;
if (!this.isConnected()) {
this.log('Network online event: retrying websocket connect');
this.connect();
}
}
_handleVisibilityChange() {
if (this._closedByUser) return;
if (document.visibilityState !== 'visible') return;
if (!this.isConnected()) {
this.log('Tab visible again: retrying websocket connect');
this.connect();
}
}
_clearStaleConnectionWarning() {
try {
const n = document.getElementById('notification');
if (!n) return;
const txt = String(n.textContent || '').toLowerCase();
if (
txt.indexOf('connection lost') !== -1 ||
txt.indexOf('connection unstable') !== -1 ||
txt.indexOf('reconnecting') !== -1
) {
n.innerHTML = '';
}
} catch (_) {}
this._connectionWarningShown = false;
}
_clearConnectionWarningTimer() {
if (this._connectionWarningTimer) {
clearTimeout(this._connectionWarningTimer);
this._connectionWarningTimer = null;
}
}
_scheduleConnectionWarning() {
if (!this.config.connectionWarningEnabled) return;
if (this._closedByUser) return;
this._clearConnectionWarningTimer();
this._connectionWarningTimer = setTimeout(() => {
if (this._closedByUser) return;
if (this.isConnected()) return;
this._renderConnectionWarning();
}, Math.max(0, Number(this.config.connectionWarningDelayMs) || 0));
}
_renderConnectionWarning() {
if (this._connectionWarningShown) return;
try {
const n = document.getElementById('notification');
if (!n) return;
const current = String(n.textContent || '').trim();
// Do not overwrite active status/progress notifications.
const currentLower = current.toLowerCase();
const isConnWarning =
currentLower.indexOf('connection lost') !== -1 ||
currentLower.indexOf('connection unstable') !== -1 ||
currentLower.indexOf('reconnecting') !== -1;
if (current && !isConnWarning) return;
n.innerHTML =
'' +
'' +
'Connection unstable. Reconnecting...' +
'
';
this._connectionWarningShown = true;
} catch (_) {}
}
}
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.");
}
}
async function addClipboard(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_clipboard",
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 zumr Zwischenablage", err);
alert("Der Eintrag konnte nicht gespeichert werden.");
}
}
/**
* MultiValue "chips" field (Vuexy-like) without Tagify.
*
* Expects markup:
*
*
* Backend expects FIELD_ID to contain a list string (JSON list is fine; convert_multivalue_params uses eval()).
*/
function initMultiValueField(container, whiteList = []) {
if (!container) return null;
// Prevent double init
if (container._mvInit) {
// refresh whitelist if needed
container._mvWhiteList = Array.isArray(whiteList) ? whiteList : [];
return container._mvInstance || null;
}
container._mvInit = true;
const chips = container.querySelector('.mv-chips');
const input = container.querySelector('.mv-input');
const hiddenList = container.querySelector('input.mv-list[type="hidden"]');
const fieldName = container.getAttribute('data-name');
if (!chips || !input || !hiddenList || !fieldName) return null;
container._mvWhiteList = Array.isArray(whiteList) ? whiteList : [];
function syncHiddenList() {
const values = Array.from(chips.querySelectorAll('.mv-chip .mv-label'))
.map(el => (el.textContent || '').trim())
.filter(v => v.length > 0);
// JSON list string; safe for eval() on backend
hiddenList.name = fieldName;
hiddenList.value = JSON.stringify(values);
}
function createChip(value) {
const chip = document.createElement('span');
chip.className = 'mv-chip badge rounded-pill bg-label-primary d-inline-flex align-items-center gap-1';
chip.innerHTML = `
`;
chip.querySelector('.mv-label').textContent = value;
return chip;
}
function addValue(raw) {
if (input.hasAttribute('readonly') || input.disabled) return;
const value = (raw ?? '').toString().trim();
if (!value) return;
const chip = createChip(value);
chips.appendChild(chip);
syncHiddenList();
}
// Expose for Value Assistance selection (table row click)
container._mvAddValue = addValue;
// Enter adds chip (comma is just a character)
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
addValue(input.value);
input.value = '';
}
});
// If value assistance sets a value and triggers change, allow adding on blur/change as well
input.addEventListener('change', (e) => {
// Only auto-add if the value assistance inserted a full value (common UX)
// and user did not press Enter.
const v = (input.value || '').trim();
if (v) {
addValue(v);
input.value = '';
}
});
// Click remove
chips.addEventListener('click', (e) => {
const btn = e.target.closest('.mv-remove');
if (!btn) return;
e.preventDefault();
const chip = btn.closest('.mv-chip');
if (chip) chip.remove();
syncHiddenList();
});
// Drag & drop reorder (DragSort already exists in your stack)
try {
const canDragSort =
typeof DragSort === 'function' &&
chips &&
chips.nodeType === 1 &&
chips.querySelector &&
chips.querySelector('.mv-chip');
if (canDragSort) {
new DragSort(chips, {
selector: '.mv-chip',
callbacks: {
dragEnd: function () {
syncHiddenList();
}
}
});
}
} catch (err) {
// DragSort optional; keep field functional if missing
console && console.warn && console.warn('DragSort init failed for multivalue', err);
}
// Initial sync (in case server values changed or DOM edited)
syncHiddenList();
// expose a small API for external callers if needed
container._mvInstance = { addValue, sync: syncHiddenList };
return container._mvInstance;
}
/* MV Value Assistance -> add row to active multivalue
- The VA modal shows a table; clicking a row should add the value to the currently active mv-field.
- The active field id is set via window._mvActiveFieldId (set in the VA button hx-on).
*/
(function () {
if (window._mvVAHookInstalled) return;
window._mvVAHookInstalled = true;
document.addEventListener('click', function (e) {
const modal = document.getElementById('valueAssistanceModal');
if (!modal) return;
// only handle clicks inside the VA modal
if (!modal.contains(e.target)) return;
const row = e.target.closest('tr');
if (!row) return;
// ignore header rows
if (row.closest('thead')) return;
const activeId = window._mvActiveFieldId;
if (!activeId) return;
// Try to get the selected value from data-attribute or first cell text
let value = row.getAttribute('data-value') || row.getAttribute('data-val') || '';
if (!value) {
const td = row.querySelector('td');
value = td ? (td.textContent || '').trim() : '';
}
if (!value) return;
const container = document.querySelector('.mv-field[data-name="' + activeId + '"]');
if (!container) return;
// ensure initialized
if (typeof initMultiValueField === 'function') {
initMultiValueField(container);
}
if (typeof container._mvAddValue === 'function') {
container._mvAddValue(value);
}
// close modal if bootstrap is available
try {
if (window.bootstrap && typeof bootstrap.Modal?.getInstance === 'function') {
const inst = bootstrap.Modal.getInstance(modal);
if (inst) inst.hide();
}
} catch (err) {
// ignore
}
});
})();
// --- Value Assistance -> MultiValue bridge (row click inserts directly as chip) ---
(function installMultiValueVAListener() {
if (window._mvVAListenerInstalled) return;
window._mvVAListenerInstalled = true;
window.addEventListener('mv:vaSelected', function (e) {
try {
const d = (e && e.detail) ? e.detail : {};
const fieldId = d.fieldId;
const value = d.value;
if (!fieldId || value === undefined || value === null) return;
const container = document.querySelector('.mv-field[data-name="' + fieldId + '"]');
if (!container) return;
// Ensure initialized
if (typeof initMultiValueField === 'function' && !container._mvInit) {
initMultiValueField(container);
}
if (container && typeof container._mvAddValue === 'function') {
container._mvAddValue(String(value));
}
} catch (err) {
console && console.warn && console.warn('mv:vaSelected handler failed', err);
}
});
})();