1
0

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

@@ -106,6 +106,7 @@ class TicketCommunicationServiceTest extends TestCase
$odataGateway = $this->createMock(BcODataGateway::class);
$odataGateway->method('getTicketsForCustomer')->with('10001', 'Test GmbH')->willReturn($tickets);
$odataGateway->method('getContactsForCustomer')->with('', 'Test GmbH')->willReturn([]);
$odataGateway->method('getTicketLog')->willReturnCallback(static function (string $ticketNo): array {
return [[
'Entry_No' => 1,
@@ -133,6 +134,53 @@ class TicketCommunicationServiceTest extends TestCase
$this->assertContains('T12', $ticketNos);
}
public function testLoadDebitorCommunicationFeedResolvesContactActorsOncePerDebitor(): void
{
$soapGateway = $this->createMock(BcSoapGateway::class);
$soapGateway->method('getTicketCommunicationText')->willReturn([
'ok' => false,
'soap_used' => false,
'return_value' => 400,
'communication_text' => '',
'message_entries' => '',
'error' => 'SOAP unavailable',
]);
$odataGateway = $this->createMock(BcODataGateway::class);
$odataGateway->method('getTicketsForCustomer')->with('10001', 'Test GmbH')->willReturn([
['No' => 'T1', 'Last_Activity_Date' => '2026-04-10T08:00:00Z', 'Created_On' => '2026-04-09T08:00:00Z'],
['No' => 'T2', 'Last_Activity_Date' => '2026-04-09T08:00:00Z', 'Created_On' => '2026-04-08T08:00:00Z'],
]);
$odataGateway->expects($this->once())
->method('getContactsForCustomer')
->with('', 'Test GmbH')
->willReturn([
['No' => 'KT002967', 'Name' => 'Felix Schneider'],
]);
$odataGateway->expects($this->never())
->method('getTicket');
$odataGateway->method('getTicketLog')->willReturn([
[
'Entry_No' => 1,
'Created_By' => 'KT002967',
'Created_By_Type' => 'Contact',
'Creation_Date' => '2026-04-03',
'Creation_Time' => '07:59:00.000',
'Type' => 'Nachricht',
'State_Subtype' => '',
'Record_ID' => 'Hallo',
],
]);
$service = new TicketCommunicationService($soapGateway, $odataGateway);
$result = $service->loadDebitorCommunicationFeed('10001', 'Test GmbH', 2, 40, 250);
$this->assertTrue($result['ok']);
$this->assertNotEmpty($result['entries']);
$actors = array_unique(array_map(static fn (array $entry): string => (string) ($entry['actor'] ?? ''), $result['entries']));
$this->assertSame(['Felix Schneider'], $actors);
}
public function testLoadTicketCommunicationFeedRespectsEntryLimit(): void
{
$soapGateway = $this->createMock(BcSoapGateway::class);