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:
@@ -21,8 +21,12 @@ class TicketCommunicationService
|
||||
* error?: string
|
||||
* }
|
||||
*/
|
||||
public function loadTicketCommunicationFeed(string $ticketNo, int $maxEntries = 40): array
|
||||
{
|
||||
public function loadTicketCommunicationFeed(
|
||||
string $ticketNo,
|
||||
int $maxEntries = 40,
|
||||
array $actorNameMap = [],
|
||||
bool $allowPerTicketActorLookup = true
|
||||
): array {
|
||||
$ticketNo = trim($ticketNo);
|
||||
if ($ticketNo === '') {
|
||||
return ['ok' => false, 'error' => 'Missing ticket number'];
|
||||
@@ -42,18 +46,18 @@ class TicketCommunicationService
|
||||
$messageEntries = $this->buildSoapMessageEntriesFromTicketLog($ticketLog);
|
||||
$soapResult = $this->bcSoapGateway->getTicketCommunicationText($ticketNo, $messageEntries);
|
||||
$soapError = (string) ($soapResult['error'] ?? '');
|
||||
$soapUsed = (bool) ($soapResult['soap_used'] ?? false);
|
||||
$soapUsed = (bool) $soapResult['soap_used'];
|
||||
$soapEntries = [];
|
||||
$soapMessageMap = [];
|
||||
if (($soapResult['ok'] ?? false) === true) {
|
||||
if ($soapResult['ok'] === true) {
|
||||
$soapEntries = $this->parseSoapEntries(
|
||||
$ticketNo,
|
||||
(string) ($soapResult['communication_text'] ?? ''),
|
||||
(string) ($soapResult['message_entries'] ?? '')
|
||||
(string) $soapResult['communication_text'],
|
||||
(string) $soapResult['message_entries']
|
||||
);
|
||||
$soapMessageMap = $this->extractSoapMessageMap(
|
||||
(string) ($soapResult['communication_text'] ?? ''),
|
||||
(string) ($soapResult['message_entries'] ?? '')
|
||||
(string) $soapResult['communication_text'],
|
||||
(string) $soapResult['message_entries']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,10 +65,15 @@ class TicketCommunicationService
|
||||
$odataEntries = $this->mapODataEntries($ticketNo, $ticketLog, $soapMessageMap);
|
||||
}
|
||||
|
||||
$actorNameMap = $this->resolveContactActorNames($ticketNo, $odataEntries, $soapEntries);
|
||||
if ($actorNameMap !== []) {
|
||||
$odataEntries = $this->applyActorNameMap($odataEntries, $actorNameMap);
|
||||
$soapEntries = $this->applyActorNameMap($soapEntries, $actorNameMap);
|
||||
} elseif ($allowPerTicketActorLookup) {
|
||||
$resolvedActorNameMap = $this->resolveContactActorNames($ticketNo, $odataEntries, $soapEntries);
|
||||
if ($resolvedActorNameMap !== []) {
|
||||
$odataEntries = $this->applyActorNameMap($odataEntries, $resolvedActorNameMap);
|
||||
$soapEntries = $this->applyActorNameMap($soapEntries, $resolvedActorNameMap);
|
||||
}
|
||||
}
|
||||
|
||||
$merged = $this->mergeEntries($odataEntries, $soapEntries, $maxEntries);
|
||||
@@ -161,11 +170,17 @@ class TicketCommunicationService
|
||||
$allEntries = [];
|
||||
$ticketsWithData = 0;
|
||||
$soapPartialFailures = 0;
|
||||
$actorNameMap = $this->buildContactActorNameMapForDebitor($customerName);
|
||||
|
||||
foreach (array_keys($ticketsToProcess) as $ticketNo) {
|
||||
$feed = $this->loadTicketCommunicationFeed($ticketNo, $maxEntriesPerTicket);
|
||||
$feed = $this->loadTicketCommunicationFeed(
|
||||
$ticketNo,
|
||||
$maxEntriesPerTicket,
|
||||
$actorNameMap,
|
||||
false
|
||||
);
|
||||
$ticketEntries = [];
|
||||
if ($feed['ok'] ?? false) {
|
||||
if ($feed['ok']) {
|
||||
$ticketEntries = is_array($feed['entries'] ?? null) ? $feed['entries'] : [];
|
||||
}
|
||||
|
||||
@@ -428,7 +443,7 @@ class TicketCommunicationService
|
||||
|
||||
if (preg_match('/^(\d{4}-\d{2}-\d{2})(?:[T\s](\d{2}:\d{2}(?::\d{2})?))?\s*[-|]\s*(.+)$/u', $working, $m) === 1) {
|
||||
$date = $m[1];
|
||||
$time = isset($m[2]) ? $this->normalizeTime($m[2]) : '';
|
||||
$time = $this->normalizeTime($m[2]);
|
||||
$timestampIso = $this->combineDateTimeToIso($date, $time);
|
||||
$working = trim($m[3]);
|
||||
} elseif (preg_match('/^(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2}(?::\d{2})?)\s*[-|]\s*(.+)$/u', $working, $m) === 1) {
|
||||
@@ -837,6 +852,38 @@ class TicketCommunicationService
|
||||
return $value !== '' && preg_match('/^KT\d+$/', $value) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function buildContactActorNameMapForDebitor(string $customerName): array
|
||||
{
|
||||
$customerName = trim($customerName);
|
||||
if ($customerName === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$contacts = $this->bcODataGateway->getContactsForCustomer('', $customerName);
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$nameMap = [];
|
||||
foreach ($contacts as $contact) {
|
||||
if (!is_array($contact)) {
|
||||
continue;
|
||||
}
|
||||
$contactNo = strtoupper(trim((string) ($contact['No'] ?? '')));
|
||||
$contactName = trim((string) ($contact['Name'] ?? ''));
|
||||
if ($contactNo === '' || $contactName === '') {
|
||||
continue;
|
||||
}
|
||||
$nameMap[$contactNo] = $contactName;
|
||||
}
|
||||
|
||||
return $nameMap;
|
||||
}
|
||||
|
||||
private function normalizeActorRole(string $createdByType, string $actor): string
|
||||
{
|
||||
$type = mb_strtolower(trim($createdByType));
|
||||
|
||||
Reference in New Issue
Block a user