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:
@@ -1,3 +1,5 @@
|
||||
import { renderEmptyState } from './helpdesk-empty-state.js';
|
||||
|
||||
function esc(str) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = String(str ?? '');
|
||||
@@ -72,9 +74,12 @@ export function renderCommunicationFeed(entries, options = {}) {
|
||||
|
||||
const list = Array.isArray(entries) ? entries : [];
|
||||
if (list.length === 0) {
|
||||
return `<div class="app-empty-state app-empty-state-compact">
|
||||
<p class="app-empty-state-message">${esc(t(emptyMessage))}</p>
|
||||
</div>`;
|
||||
return renderEmptyState({
|
||||
message: t(emptyMessage),
|
||||
size: 'compact',
|
||||
align: 'center',
|
||||
icon: false,
|
||||
});
|
||||
}
|
||||
|
||||
let html = '<ol class="helpdesk-comm-feed" role="list">';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
32
modules/helpdesk/web/js/helpdesk-empty-state.js
Normal file
32
modules/helpdesk/web/js/helpdesk-empty-state.js
Normal file
@@ -0,0 +1,32 @@
|
||||
function esc(str) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = String(str ?? '');
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
const EMPTY_ICON_SVG = `
|
||||
<svg class="app-empty-state-icon" xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline>
|
||||
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export function renderEmptyState(options = {}) {
|
||||
const message = String(options.message || '').trim();
|
||||
if (!message) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const hint = String(options.hint || '').trim();
|
||||
const sizeRaw = String(options.size || 'compact').trim().toLowerCase();
|
||||
const size = ['default', 'compact', 'small'].includes(sizeRaw) ? sizeRaw : 'compact';
|
||||
const alignRaw = String(options.align || 'center').trim().toLowerCase();
|
||||
const align = ['center', 'left'].includes(alignRaw) ? alignRaw : 'center';
|
||||
const icon = options.icon === true;
|
||||
|
||||
return `<div class="app-empty-state" data-size="${esc(size)}" data-align="${esc(align)}">
|
||||
${icon ? EMPTY_ICON_SVG : ''}
|
||||
<p class="app-empty-state-message">${esc(message)}</p>
|
||||
${hint ? `<small class="app-empty-state-hint">${esc(hint)}</small>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
renderCommunicationFeed,
|
||||
renderCommunicationHint,
|
||||
} from './helpdesk-communication.js';
|
||||
import { renderEmptyState } from './helpdesk-empty-state.js';
|
||||
|
||||
const container = document.querySelector('.app-details-container[data-ticket-no]');
|
||||
if (container) {
|
||||
@@ -95,9 +96,12 @@ function init(container) {
|
||||
|
||||
function renderTimeline(entries) {
|
||||
if (entries.length === 0) {
|
||||
return `<div class="app-empty-state app-empty-state-compact">
|
||||
<p class="app-empty-state-message">${esc(t('No activity found.'))}</p>
|
||||
</div>`;
|
||||
return renderEmptyState({
|
||||
message: t('No activity found.'),
|
||||
size: 'compact',
|
||||
align: 'center',
|
||||
icon: false,
|
||||
});
|
||||
}
|
||||
|
||||
let html = '<div class="helpdesk-timeline">';
|
||||
|
||||
Reference in New Issue
Block a user