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:
@@ -192,4 +192,190 @@ class HelpdeskSettingsGatewayTest extends TestCase
|
||||
$gateway = $this->createGateway($settings);
|
||||
$this->assertFalse($gateway->setOAuthTokenEndpoint('http://login.microsoftonline.com/token'));
|
||||
}
|
||||
|
||||
public function testGetSystemRecommendationsConfigEnvelopeReturnsDefaultsWhenMissing(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturnMap([
|
||||
[HelpdeskSettingsGateway::KEY_SYSTEM_RECOMMENDATIONS_CONFIG, null],
|
||||
]);
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$envelope = $gateway->getSystemRecommendationsConfigEnvelope();
|
||||
|
||||
$this->assertSame('default', $envelope['source']);
|
||||
$this->assertSame(5, $envelope['config']['max_items']);
|
||||
$this->assertTrue($envelope['config']['rules']['stale_open']['enabled']);
|
||||
}
|
||||
|
||||
public function testGetSystemRecommendationsConfigEnvelopeFallsBackOnInvalidJson(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturnMap([
|
||||
[HelpdeskSettingsGateway::KEY_SYSTEM_RECOMMENDATIONS_CONFIG, '{invalid'],
|
||||
]);
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$envelope = $gateway->getSystemRecommendationsConfigEnvelope();
|
||||
|
||||
$this->assertSame('fallback_invalid_json', $envelope['source']);
|
||||
$this->assertSame(5, $envelope['config']['max_items']);
|
||||
}
|
||||
|
||||
public function testGetSystemRecommendationsConfigNormalizesValues(): void
|
||||
{
|
||||
$rawConfig = json_encode([
|
||||
'version' => 99,
|
||||
'max_items' => 99,
|
||||
'rules' => [
|
||||
'high_risk_aging' => [
|
||||
'enabled' => true,
|
||||
'priority' => '120',
|
||||
'codes' => 'MAX, HOCH,',
|
||||
'min_age_hours' => '12',
|
||||
],
|
||||
'customer_backlog' => [
|
||||
'enabled' => '0',
|
||||
'priority' => 0,
|
||||
'min_open_tickets' => -1,
|
||||
],
|
||||
],
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturnMap([
|
||||
[HelpdeskSettingsGateway::KEY_SYSTEM_RECOMMENDATIONS_CONFIG, $rawConfig],
|
||||
]);
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$config = $gateway->getSystemRecommendationsConfig();
|
||||
|
||||
$this->assertSame(20, $config['max_items']);
|
||||
$this->assertSame(['MAX', 'HOCH'], $config['rules']['high_risk_aging']['codes']);
|
||||
$this->assertSame(12, $config['rules']['high_risk_aging']['min_age_hours']);
|
||||
$this->assertFalse($config['rules']['customer_backlog']['enabled']);
|
||||
$this->assertSame(1, $config['rules']['customer_backlog']['priority']);
|
||||
$this->assertSame(1, $config['rules']['customer_backlog']['min_open_tickets']);
|
||||
}
|
||||
|
||||
public function testSetSystemRecommendationsConfigPersistsNormalizedJson(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->expects($this->once())
|
||||
->method('set')
|
||||
->with(
|
||||
HelpdeskSettingsGateway::KEY_SYSTEM_RECOMMENDATIONS_CONFIG,
|
||||
$this->callback(static function (mixed $value): bool {
|
||||
if (!is_string($value) || $value === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$decoded = json_decode($value, true);
|
||||
if (!is_array($decoded)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($decoded['version'] ?? null) === 1
|
||||
&& ($decoded['max_items'] ?? null) === 1
|
||||
&& (($decoded['rules']['high_risk_aging']['codes'] ?? []) === ['MAX', 'HOCH']);
|
||||
}),
|
||||
'helpdesk.system_recommendations_config'
|
||||
)
|
||||
->willReturn(true);
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$result = $gateway->setSystemRecommendationsConfig([
|
||||
'max_items' => -10,
|
||||
'rules' => [
|
||||
'high_risk_aging' => [
|
||||
'codes' => '',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
// --- Controlling Risk Config ---
|
||||
|
||||
public function testGetControllingRiskConfigEnvelopeReturnsDefaultWhenNotSet(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturnMap([
|
||||
[HelpdeskSettingsGateway::KEY_CONTROLLING_RISK_CONFIG, null],
|
||||
]);
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$envelope = $gateway->getControllingRiskConfigEnvelope();
|
||||
|
||||
$this->assertSame('default', $envelope['source']);
|
||||
$this->assertSame(1, $envelope['config']['version']);
|
||||
$this->assertTrue($envelope['config']['rules']['ticket_volume_increase']['enabled']);
|
||||
$this->assertSame(30, $envelope['config']['rules']['ticket_volume_increase']['threshold_percent']);
|
||||
$this->assertSame(72, $envelope['config']['rules']['avg_resolution_high']['threshold_hours']);
|
||||
}
|
||||
|
||||
public function testSetControllingRiskConfigNormalizesValues(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->expects($this->once())
|
||||
->method('set')
|
||||
->with(
|
||||
HelpdeskSettingsGateway::KEY_CONTROLLING_RISK_CONFIG,
|
||||
$this->callback(static function (mixed $value): bool {
|
||||
if (!is_string($value) || $value === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$decoded = json_decode($value, true);
|
||||
if (!is_array($decoded)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($decoded['version'] ?? null) === 1
|
||||
&& ($decoded['rules']['ticket_volume_increase']['threshold_percent'] ?? null) === 50;
|
||||
}),
|
||||
'helpdesk.controlling_risk_config'
|
||||
)
|
||||
->willReturn(true);
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$result = $gateway->setControllingRiskConfig([
|
||||
'rules' => [
|
||||
'ticket_volume_increase' => ['threshold_percent' => 50],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testNormalizeControllingRiskConfigEnforcesMinMax(): void
|
||||
{
|
||||
$rawConfig = json_encode([
|
||||
'version' => 1,
|
||||
'rules' => [
|
||||
'ticket_volume_increase' => ['enabled' => true, 'threshold_percent' => 999],
|
||||
'avg_resolution_high' => ['enabled' => true, 'threshold_hours' => 0],
|
||||
'open_aging' => ['enabled' => true, 'threshold_hours' => 5000],
|
||||
'unassigned_ratio' => ['enabled' => true, 'threshold_percent' => 101],
|
||||
],
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturnMap([
|
||||
[HelpdeskSettingsGateway::KEY_CONTROLLING_RISK_CONFIG, $rawConfig],
|
||||
]);
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$config = $gateway->getControllingRiskConfig();
|
||||
|
||||
// ticket_volume_increase: 999 > 200 → clamped to max 200
|
||||
$this->assertSame(200, $config['rules']['ticket_volume_increase']['threshold_percent']);
|
||||
// avg_resolution_high: 0 < 1 → clamped to min 1
|
||||
$this->assertSame(1, $config['rules']['avg_resolution_high']['threshold_hours']);
|
||||
// open_aging: 5000 > 2160 → clamped to max 2160
|
||||
$this->assertSame(2160, $config['rules']['open_aging']['threshold_hours']);
|
||||
// unassigned_ratio: 101 > 100 → clamped to max 100
|
||||
$this->assertSame(100, $config['rules']['unassigned_ratio']['threshold_percent']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user