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:
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user