(function (window, document) { 'use strict'; var FIELD_CUSTOMIZER_ID = 'a555f4fc-d6a6-4421-8189-457449d9bbde'; var SITE_DEFAULTS_PROPERTY = 'PortalSettings.ExpiryIndicator.Defaults'; var WEB_OVERRIDE_PROPERTY = 'PortalSettings.ExpiryIndicator.Override'; var state = { context: null, field: null, config: null, loading: null }; function configuredExpiryField() { var scripts = document.getElementsByTagName('script'); for (var index = scripts.length - 1; index >= 0; index--) { var source = String(scripts[index].src || ''); if (source.indexOf('ExpiryIndicatorClassic.js') < 0) { continue; } var match = /[?&]expiryField=([^&]+)/i.exec(source); if (match) { return decodeURIComponent(match[1]); } } return ''; } function discoverExpiryField() { var id = listId(null); if (!id) { return ''; } try { var xhr = new XMLHttpRequest(); xhr.open('GET', listUrl(id) + '/fields?$select=InternalName,ClientSideComponentId', false); xhr.setRequestHeader('Accept', 'application/json;odata=verbose'); xhr.setRequestHeader('OData-Version', '3.0'); xhr.send(null); if (xhr.status < 200 || xhr.status >= 300) { return ''; } var data = JSON.parse(xhr.responseText || '{}'); var fields = data.d ? data.d.results : (data.value || []); for (var index = 0; index < fields.length; index++) { var componentId = String(fields[index].ClientSideComponentId || '').replace(/[{}]/g, '').toLowerCase(); if (componentId === FIELD_CUSTOMIZER_ID) { return fields[index].InternalName; } } } catch (ignore) { } return ''; } function pageContext() { return window._spPageContextInfo || {}; } function webUrl() { return String(pageContext().webAbsoluteUrl || '').replace(/\/$/, ''); } function siteUrl() { return String(pageContext().siteAbsoluteUrl || webUrl()).replace(/\/$/, ''); } function listId(context) { return String((context && context.listName) || pageContext().pageListId || '').replace(/[{}]/g, ''); } function request(method, url, body) { return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open(method, url, true); xhr.setRequestHeader('Accept', 'application/json;odata=verbose'); xhr.setRequestHeader('Content-Type', 'application/json;odata=verbose'); xhr.setRequestHeader('OData-Version', '3.0'); if (method !== 'GET') { var digest = document.getElementById('__REQUESTDIGEST'); if (digest) { xhr.setRequestHeader('X-RequestDigest', digest.value); } } xhr.onreadystatechange = function () { if (xhr.readyState !== 4) { return; } if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr.responseText ? JSON.parse(xhr.responseText) : {}); } else { var message = xhr.status + ' ' + xhr.statusText; try { var error = JSON.parse(xhr.responseText); message = error.error && error.error.message ? error.error.message.value : message; } catch (ignore) { } reject(new Error(message)); } }; xhr.send(body ? JSON.stringify(body) : null); }); } function listUrl(id) { return webUrl() + "/_api/web/lists(guid'" + id + "')"; } function defaults(expiryField) { return { baseField: 'Created', expiryField: expiryField || 'ExpiryDate', default: { lifeTime: { value: 2, unit: 'years' }, columnRule: [] }, rules: [], nullText: 'Kein Ablaufdatum', confirmExtension: true }; } function mergeConfig(baseValue, overrideValue) { var base = baseValue || defaults(); var override = overrideValue || {}; var result = { baseField: override.baseField || base.baseField || 'Created', expiryField: override.expiryField || base.expiryField || 'ExpiryDate', default: { lifeTime: override.default && override.default.lifeTime ? override.default.lifeTime : (base.default && base.default.lifeTime ? base.default.lifeTime : { value: 2, unit: 'years' }), columnRule: override.default && override.default.columnRule instanceof Array ? override.default.columnRule : (base.default && base.default.columnRule instanceof Array ? base.default.columnRule : []) }, rules: override.rules instanceof Array ? override.rules : (base.rules instanceof Array ? base.rules : []), nullText: typeof override.nullText === 'string' ? override.nullText : base.nullText, confirmExtension: typeof override.confirmExtension === 'boolean' ? override.confirmExtension : base.confirmExtension }; return result; } function propertyValue(payload, key) { var properties = payload && payload.d ? payload.d : (payload || {}); var expected = String(key || '').replace(/_x002e_/gi, '.').toLowerCase(); for (var name in properties) { var normalizedName = String(name || '').replace(/_x002e_/gi, '.').toLowerCase(); if (Object.prototype.hasOwnProperty.call(properties, name) && normalizedName === expected) { try { return typeof properties[name] === 'string' ? JSON.parse(properties[name]) : properties[name]; } catch (ignore) { return null; } } } return null; } function isBindingOnly(value) { if (!value || typeof value !== 'object') { return false; } return typeof value.expiryField === 'string' && !value.default && !value.rules && !value.baseField && typeof value.nullText === 'undefined' && typeof value.confirmExtension === 'undefined'; } function loadConfig(context) { var id = listId(context); if (!id) { return Promise.reject(new Error('Keine Liste erkannt.')); } if (state.loading && state.context === id) { return state.loading; } state.context = id; var fieldRequest = request('GET', listUrl(id) + '/fields?$select=Id,InternalName,ClientSideComponentId,ClientSideComponentProperties'); var sitePropertiesRequest = request('GET', siteUrl() + '/_api/web/AllProperties').catch(function () { return {}; }); var webPropertiesRequest = siteUrl().toLowerCase() === webUrl().toLowerCase() ? Promise.resolve({}) : request('GET', webUrl() + '/_api/web/AllProperties').catch(function () { return {}; }); state.loading = Promise.all([fieldRequest, sitePropertiesRequest, webPropertiesRequest]) .then(function (results) { var data = results[0]; var fields = data.d ? data.d.results : (data.value || []); var field; for (var index = 0; index < fields.length; index++) { var componentId = String(fields[index].ClientSideComponentId || '').replace(/[{}]/g, '').toLowerCase(); if (componentId === FIELD_CUSTOMIZER_ID) { field = fields[index]; break; } } if (!field) { throw new Error('Kein ExpiryIndicator-Feld gebunden.'); } var inherited = mergeConfig(defaults(field.InternalName), propertyValue(results[1], SITE_DEFAULTS_PROPERTY)); inherited = mergeConfig(inherited, propertyValue(results[2], WEB_OVERRIDE_PROPERTY)); var config = inherited; try { if (field.ClientSideComponentProperties) { var parsed = JSON.parse(field.ClientSideComponentProperties); if (parsed.inheritSiteDefaults === true || parsed.override || isBindingOnly(parsed)) { config = mergeConfig(inherited, parsed.override || {}); } else if (parsed.inheritSiteDefaults === false) { config = mergeConfig(defaults(field.InternalName), parsed.override || {}); } else { config = mergeConfig(defaults(field.InternalName), parsed); } } } catch (ignore) { } config.expiryField = field.InternalName; state.field = field; state.config = config; return config; }); return state.loading; } function ruleFields(config) { var result = []; function add(name) { if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name || '') && result.indexOf(name) < 0) { result.push(name); } } function visit(condition) { if (!condition) { return; } if (condition.conditions instanceof Array) { for (var index = 0; index < condition.conditions.length; index++) { visit(condition.conditions[index]); } } else { add(condition.columnName); } } for (var index = 0; index < (config.rules || []).length; index++) { var rule = config.rules[index]; if (rule.condition) { visit(rule.condition); } else { add(rule.columnName); } } return result; } function equalsIgnoreCase(left, right) { return String(left).toLowerCase() === String(right).toLowerCase(); } function valueMatches(rawValue, configuredValue) { if (rawValue === null || typeof rawValue === 'undefined') { return String(configuredValue) === ''; } var values = rawValue instanceof Array ? rawValue : (rawValue.results instanceof Array ? rawValue.results : [rawValue]); for (var index = 0; index < values.length; index++) { var value = values[index]; if (value && typeof value === 'object') { if (value.TermGuid && equalsIgnoreCase(value.TermGuid, configuredValue)) { return true; } value = value.Title || value.LookupValue || value.Label || value.Value || ''; } var text = String(value); if (text === String(configuredValue)) { return true; } var separator = text.lastIndexOf('|'); if (separator >= 0 && equalsIgnoreCase(text.substring(separator + 1), configuredValue)) { return true; } } return false; } function conditionMatches(condition, values) { if (condition && condition.conditions instanceof Array) { if (condition.operator === 'and') { for (var andIndex = 0; andIndex < condition.conditions.length; andIndex++) { if (!conditionMatches(condition.conditions[andIndex], values)) { return false; } } return condition.conditions.length > 0; } if (condition.operator === 'or') { for (var orIndex = 0; orIndex < condition.conditions.length; orIndex++) { if (conditionMatches(condition.conditions[orIndex], values)) { return true; } } } return false; } return !!condition && Object.prototype.hasOwnProperty.call(values, condition.columnName) && valueMatches(values[condition.columnName], condition.columnValue); } function validateRules(rules) { var errors = []; function safeName(value) { return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value || ''); } function validateGroup(group, path, depth) { if (depth > 10) { errors.push(path + ': maximale Verschachtelungstiefe 10 überschritten.'); return; } if (!group || (group.operator !== 'and' && group.operator !== 'or')) { errors.push(path + '.operator muss "and" oder "or" sein.'); } if (!group || !(group.conditions instanceof Array) || !group.conditions.length) { errors.push(path + '.conditions muss mindestens eine Bedingung enthalten.'); return; } for (var index = 0; index < group.conditions.length; index++) { var condition = group.conditions[index]; var childPath = path + '.conditions[' + index + ']'; if (condition && typeof condition.operator !== 'undefined') { validateGroup(condition, childPath, depth + 1); } else if (!condition || !safeName(condition.columnName) || typeof condition.columnValue === 'undefined') { errors.push(childPath + ': columnName/columnValue fehlen oder sind ungültig.'); } } } if (!(rules instanceof Array)) { return ['rules muss ein JSON-Array sein.']; } for (var index = 0; index < rules.length; index++) { var rule = rules[index]; if (rule && rule.condition) { validateGroup(rule.condition, 'rules[' + index + '].condition', 0); } else if (!rule || !safeName(rule.columnName) || typeof rule.columnValue === 'undefined') { errors.push('rules[' + index + '] benötigt condition oder columnName/columnValue.'); } } return errors; } function behavior(config, values) { for (var index = 0; index < (config.rules || []).length; index++) { var rule = config.rules[index]; var matched = rule.condition ? conditionMatches(rule.condition, values) : Object.prototype.hasOwnProperty.call(values, rule.columnName) && valueMatches(values[rule.columnName], rule.columnValue); if (matched) { return rule; } } return config.default; } function parseDate(value) { if (!value) { return null; } var match = /\/Date\((-?\d+)/.exec(String(value)); var result = match ? new Date(Number(match[1])) : new Date(value); return isNaN(result.getTime()) ? null : result; } function formatDate(value) { function pad(number) { return number < 10 ? '0' + number : String(number); } return pad(value.getDate()) + '.' + pad(value.getMonth() + 1) + '.' + value.getFullYear(); } function addDuration(source, duration) { var result = new Date(source.getTime()); if (duration.unit === 'days') { result.setUTCDate(result.getUTCDate() + duration.value); return result; } var months = duration.unit === 'years' ? duration.value * 12 : duration.value; var day = result.getUTCDate(); result.setUTCDate(1); result.setUTCMonth(result.getUTCMonth() + months); var lastDay = new Date(Date.UTC(result.getUTCFullYear(), result.getUTCMonth() + 1, 0)).getUTCDate(); result.setUTCDate(Math.min(day, lastDay)); return result; } function dayDifference(from, to) { var fromDay = Date.UTC(from.getUTCFullYear(), from.getUTCMonth(), from.getUTCDate()); var toDay = Date.UTC(to.getUTCFullYear(), to.getUTCMonth(), to.getUTCDate()); return Math.round((toDay - fromDay) / 86400000); } function colorRule(days, rules) { function matches(operator, threshold) { if (operator === 'lessThan') { return days < threshold; } if (operator === 'lessOrEqual') { return days <= threshold; } if (operator === 'equal') { return days === threshold; } if (operator === 'greaterOrEqual') { return days >= threshold; } return operator === 'greaterThan' && days > threshold; } for (var index = 0; index < (rules || []).length; index++) { if (matches(rules[index].operator, rules[index].daysUntilExpiry)) { return rules[index]; } } return null; } function findRow(id) { var rows = document.querySelectorAll('tr[iid]'); for (var index = 0; index < rows.length; index++) { var parts = String(rows[index].getAttribute('iid') || '').split(','); if (String(parts[1]) === String(id)) { return rows[index]; } } return null; } function applyGradient(row, color, textColor, title) { var allCells = row.querySelectorAll('td'); var cells = []; for (var index = 0; index < allCells.length; index++) { if (!allCells[index].querySelector('input[type="checkbox"]') && String(allCells[index].className).indexOf('ms-vb-itmcbx') < 0) { cells.push(allCells[index]); } } if (!cells.length) { return; } var first = cells[0].getBoundingClientRect(); var last = cells[cells.length - 1].getBoundingClientRect(); var width = Math.max(1, last.right - first.left); var gradient = 'linear-gradient(to right, #ffffff, ' + color + ')'; for (var cellIndex = 0; cellIndex < cells.length; cellIndex++) { var rect = cells[cellIndex].getBoundingClientRect(); cells[cellIndex].style.backgroundColor = '#ffffff'; cells[cellIndex].style.backgroundImage = gradient; cells[cellIndex].style.backgroundSize = width + 'px 100%'; cells[cellIndex].style.backgroundPosition = (-(rect.left - first.left)) + 'px 0'; cells[cellIndex].style.backgroundRepeat = 'no-repeat'; cells[cellIndex].style.color = textColor; cells[cellIndex].title = title; } } function render(context) { loadConfig(context).then(function (config) { var rows = context && context.ListData && context.ListData.Row ? context.ListData.Row : []; var ids = []; for (var index = 0; index < rows.length; index++) { var id = Number(rows[index].ID || rows[index].Id); if (id > 0) { ids.push(id); } } if (!ids.length) { return; } var fields = [config.baseField, config.expiryField].concat(ruleFields(config)); var select = ['Id']; for (var fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) { if (select.indexOf(fields[fieldIndex]) < 0) { select.push(fields[fieldIndex]); } } var filters = []; for (var idIndex = 0; idIndex < ids.length; idIndex++) { filters.push('Id eq ' + ids[idIndex]); } return request('GET', listUrl(listId(context)) + '/items?$select=' + select.join(',') + '&$filter=' + encodeURIComponent(filters.join(' or ')) + '&$top=' + ids.length) .then(function (data) { var items = data.d ? data.d.results : (data.value || []); for (var itemIndex = 0; itemIndex < items.length; itemIndex++) { var item = items[itemIndex]; var created = parseDate(item[config.baseField]); var expiry = parseDate(item[config.expiryField]); if (!created && !expiry) { continue; } var selected = behavior(config, item); var effective = expiry || addDuration(created, selected.lifeTime); var days = dayDifference(new Date(), effective); var rule = colorRule(days, selected.columnRule); var row = findRow(item.Id); var marker = row ? row.querySelector('[data-expiry-classic-item-id="' + item.Id + '"]') : null; if (marker) { marker.innerHTML = ''; marker.appendChild(document.createTextNode(formatDate(effective) + (rule && rule.label ? ' ' + rule.label : ''))); marker.title = days + ' Resttage' + (!expiry ? ' – aus ' + config.baseField + ' berechnet' : ''); } if (row && rule) { applyGradient(row, rule.backgroundColor, rule.textColor, days + ' Resttage' + (rule.label ? ' – ' + rule.label : '')); } } }); }).catch(function (error) { if (window.console && console.warn) { console.warn('ExpiryIndicator Classic:', error.message); } }); } function selectedIds() { try { var selected = SP.ListOperation.Selection.getSelectedItems(); var ids = []; for (var index = 0; index < selected.length; index++) { ids.push(Number(selected[index].id)); } return ids; } catch (ignore) { return []; } } function extendOne(id, config) { var fields = [config.baseField, config.expiryField].concat(ruleFields(config)); return request('GET', listUrl(state.context) + '/items(' + id + ')?$select=Id,' + fields.join(',')) .then(function (data) { var item = data.d || data; var created = parseDate(item[config.baseField]); var expiry = parseDate(item[config.expiryField]); var base = expiry || addDuration(created, behavior(config, item).lifeTime); var updated = addDuration(base, { value: 1, unit: 'years' }); var body = { '__metadata': { 'type': item.__metadata.type } }; body[config.expiryField] = updated.toISOString(); return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open('POST', listUrl(state.context) + '/items(' + id + ')', true); xhr.setRequestHeader('Accept', 'application/json;odata=verbose'); xhr.setRequestHeader('Content-Type', 'application/json;odata=verbose'); xhr.setRequestHeader('OData-Version', '3.0'); xhr.setRequestHeader('IF-MATCH', '*'); xhr.setRequestHeader('X-HTTP-Method', 'MERGE'); var digest = document.getElementById('__REQUESTDIGEST'); if (digest) { xhr.setRequestHeader('X-RequestDigest', digest.value); } xhr.onreadystatechange = function () { if (xhr.readyState !== 4) { return; } if (xhr.status >= 200 && xhr.status < 300) { resolve(); } else { reject(new Error('#' + id + ': ' + xhr.status + ' ' + xhr.statusText)); } }; xhr.send(JSON.stringify(body)); }); }); } function extendSelected() { var ids = selectedIds(); if (!ids.length) { alert('Bitte mindestens ein Element auswählen.'); return; } loadConfig(state.context).then(function (config) { if (config.confirmExtension && !confirm('Ablaufdatum für ' + ids.length + ' Element(e) um ein Jahr verlängern?')) { return; } return Promise.all(ids.map(function (id) { return extendOne(id, config).then(function () { return { id: id, succeeded: true }; }, function (error) { return { id: id, succeeded: false, error: error.message }; }); })).then(function (results) { var succeeded = 0; var errors = []; for (var index = 0; index < results.length; index++) { if (results[index].succeeded) { succeeded++; } else { errors.push('#' + results[index].id + ': ' + results[index].error); } } var message = 'Erfolgreich aktualisiert: ' + succeeded + '; Fehler: ' + errors.length; if (errors.length) { message += '\n\n' + errors.slice(0, 10).join('\n'); } alert(message); if (succeeded > 0) { window.location.reload(); } }); }).catch(function (error) { alert(error.message); }); } function canExtend() { return selectedIds().length > 0; } function settingsRequested() { return /(?:\?|&)expiryIndicatorSettings=(?:1|true|yes)(?:&|$)/i.test(window.location.search); } function openSettings() { loadConfig(state.context).then(function (config) { var overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;z-index:100000;inset:0;background:rgba(0,0,0,.35);padding:5vh 10vw'; var panel = document.createElement('div'); panel.style.cssText = 'background:#fff;padding:20px;height:80vh;font-family:Segoe UI,Arial;box-sizing:border-box'; panel.innerHTML = '

ExpiryIndicator-Einstellungen

' + '

Vollständige JSON-Konfiguration für diese Liste/Bibliothek:

' + '' + '
' + '
' + '
'; overlay.appendChild(panel); document.body.appendChild(overlay); var textarea = panel.querySelector('textarea'); textarea.value = JSON.stringify(config, null, 2); panel.querySelector('[data-cancel]').onclick = function () { document.body.removeChild(overlay); }; panel.querySelector('[data-save]').onclick = function () { var parsed; try { parsed = JSON.parse(textarea.value); } catch (error) { panel.querySelector('[data-error]').innerHTML = 'Ungültiges JSON: ' + error.message; return; } var validationErrors = validateRules(parsed.rules); if (validationErrors.length) { panel.querySelector('[data-error]').innerHTML = validationErrors.join('
'); return; } parsed.expiryField = state.field.InternalName; var body = { '__metadata': { 'type': state.field.__metadata && state.field.__metadata.type || 'SP.Field' }, 'ClientSideComponentProperties': JSON.stringify(parsed) }; var fieldId = String(state.field.Id).replace(/[{}]/g, ''); var xhr = new XMLHttpRequest(); xhr.open('POST', listUrl(state.context) + "/fields(guid'" + fieldId + "')", true); xhr.setRequestHeader('Accept', 'application/json;odata=verbose'); xhr.setRequestHeader('Content-Type', 'application/json;odata=verbose'); xhr.setRequestHeader('OData-Version', '3.0'); xhr.setRequestHeader('IF-MATCH', '*'); xhr.setRequestHeader('X-HTTP-Method', 'MERGE'); var digest = document.getElementById('__REQUESTDIGEST'); if (digest) { xhr.setRequestHeader('X-RequestDigest', digest.value); } xhr.onreadystatechange = function () { if (xhr.readyState !== 4) { return; } if (xhr.status >= 200 && xhr.status < 300) { window.location.reload(); } else { panel.querySelector('[data-error]').innerHTML = 'Speichern fehlgeschlagen: ' + xhr.statusText; } }; xhr.send(JSON.stringify(body)); }; }).catch(function (error) { alert(error.message); }); } window.ExpiryIndicatorClassic = { canExtend: canExtend, extendSelected: extendSelected, openSettings: openSettings, render: render }; function register() { if (window.SPClientTemplates && SPClientTemplates.TemplateManager) { var overrides = { OnPostRender: render }; var expiryField = configuredExpiryField() || discoverExpiryField(); if (expiryField) { overrides.Templates = { Fields: {} }; overrides.Templates.Fields[expiryField] = { View: function (context) { var item = context.CurrentItem || {}; var id = Number(item.ID || item.Id); var date = parseDate(item[expiryField]); var value = date ? formatDate(date) : ''; var encoded = window.STSHtmlEncode ? STSHtmlEncode(value) : value; return '' + encoded + ''; } }; } SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrides); } if (settingsRequested()) { setTimeout(openSettings, 0); } } if (window.SPClientTemplates) { register(); } else if (window._spBodyOnLoadFunctionNames) { window._spBodyOnLoadFunctionNames.push('ExpiryIndicatorClassicRegister'); } window.ExpiryIndicatorClassicRegister = register; if (window.NotifyScriptLoadedAndExecuteWaitingJobs) { window.NotifyScriptLoadedAndExecuteWaitingJobs('ExpiryIndicatorClassic.js'); } })(window, document);