feat(helpdesk): add dashboards, communication feed, settings UI and fix routing

Add support/sales/controlling dashboards with KPIs, trend charts and
risk indicators. Add debitor communication timeline, contact filters,
system recommendations engine, and configurable controlling risk rules.

Rename search→index to fix query-string preservation on back navigation.
Remove fake escalation rate metric, add KPI info tooltips, and switch
trend chart colors to red (created) / green (closed) for clarity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 18:34:03 +02:00
parent e897cc2c56
commit aee9cb10f3
43 changed files with 7451 additions and 635 deletions

View File

@@ -3,6 +3,16 @@
*/
import { showAsyncFlash } from '/js/components/app-async-flash.js';
const settingsForm = document.getElementById('helpdesk-settings-form');
const getMessage = (key, fallback) => {
if (!settingsForm || !settingsForm.dataset) {
return fallback;
}
return settingsForm.dataset[key] || fallback;
};
// Toggle OAuth2 vs Basic Auth field visibility
const authModeSelect = document.getElementById('auth_mode');
const basicFields = document.getElementById('helpdesk-basic-auth-fields');
@@ -12,9 +22,10 @@ const toggleAuthFields = () => {
if (!authModeSelect || !basicFields || !oauth2Fields) {
return;
}
const isOAuth2 = authModeSelect.value === 'oauth2';
basicFields.style.display = isOAuth2 ? 'none' : '';
oauth2Fields.style.display = isOAuth2 ? '' : 'none';
basicFields.toggleAttribute('hidden', isOAuth2);
oauth2Fields.toggleAttribute('hidden', !isOAuth2);
};
if (authModeSelect) {
@@ -24,11 +35,43 @@ if (authModeSelect) {
// Connection test button
const testButton = document.getElementById('helpdesk-test-connection-button');
const testResult = document.getElementById('helpdesk-connection-test-result');
const setTestResult = (variant, message) => {
if (!testResult) {
return;
}
testResult.textContent = message;
testResult.dataset.variant = variant;
testResult.hidden = !message;
};
const setButtonLoadingState = (isLoading) => {
if (!testButton) {
return;
}
const defaultLabel = testButton.dataset.labelDefault || getMessage('testDefaultLabel', 'Test connection');
const loadingLabel = testButton.dataset.labelLoading || getMessage('testLoadingLabel', 'Testing...');
testButton.disabled = isLoading;
testButton.toggleAttribute('aria-busy', isLoading);
testButton.textContent = isLoading ? loadingLabel : defaultLabel;
};
const parseJsonSafely = async (response) => {
try {
return await response.json();
} catch {
return null;
}
};
if (testButton) {
testButton.addEventListener('click', async () => {
testButton.disabled = true;
testButton.setAttribute('aria-busy', 'true');
setButtonLoadingState(true);
setTestResult('info', getMessage('testLoadingLabel', 'Testing...'));
try {
const csrfMeta = document.querySelector('meta[name="csrf-token"]');
@@ -49,22 +92,35 @@ if (testButton) {
},
);
const data = await response.json();
const data = await parseJsonSafely(response);
const successMessage = getMessage('testSuccessMessage', 'Connection successful');
const defaultErrorMessage = getMessage('testErrorMessage', 'Connection failed');
if (data.ok) {
showAsyncFlash('success', data.message || 'Connection successful');
if (response.ok && data?.ok) {
const message = data.message || successMessage;
setTestResult('success', message);
showAsyncFlash('success', message);
} else {
let msg = data.error || 'Connection failed';
if (data.debug?.http_code) {
msg += ' (HTTP ' + data.debug.http_code + ')';
let message = defaultErrorMessage;
if (data?.error) {
message = data.error;
} else if (!response.ok) {
message = `${defaultErrorMessage} (HTTP ${response.status})`;
}
showAsyncFlash('error', msg);
if (data?.debug?.http_code) {
message += ` (HTTP ${data.debug.http_code})`;
}
setTestResult('warning', message);
showAsyncFlash('error', message);
}
} catch {
showAsyncFlash('error', 'Connection failed');
const message = getMessage('testErrorMessage', 'Connection failed');
setTestResult('warning', message);
showAsyncFlash('error', message);
} finally {
testButton.disabled = false;
testButton.removeAttribute('aria-busy');
setButtonLoadingState(false);
}
});
}