feat(helpdesk): add customer engagement analytics page [skip ci]

New "Customer analytics" page under Monitoring: surfaces who is actively in
contact vs. who has gone quiet, so the team can reach out before a customer
slips away.

- KPI tiles: total / active / no-contact / never-in-contact
- "Customers without recent contact" — longest silence first
- "Top customers by communication" — most ticket activity in the period
- Configurable no-contact threshold (helpdesk settings, default 60 days)

CustomerEngagementService aggregates the global ticket snapshot per customer
(same single-pull model as RiskRadarService — no N+1 to BC). Last contact is
the most recent ticket activity; silence beyond the threshold flags the
customer. Revenue ranking deferred (BC OData exposes no per-customer ledger).

All within modules/helpdesk/. Tests + PHPStan green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
aminovfariz
2026-06-25 10:30:05 +02:00
parent 2924407450
commit 14da36e83a
16 changed files with 1150 additions and 46 deletions

View File

@@ -24,6 +24,13 @@ class HelpdeskSettingsGateway
public const KEY_OAUTH_TOKEN_ENDPOINT = 'helpdesk.bc_oauth_token_endpoint';
public const KEY_SYSTEM_RECOMMENDATIONS_CONFIG = 'helpdesk.system_recommendations_config';
public const KEY_CONTROLLING_RISK_CONFIG = 'helpdesk.controlling_risk_config';
/** @api Read in helpdesk/settings + analytics pages */
public const KEY_INACTIVITY_THRESHOLD_DAYS = 'helpdesk.inactivity_threshold_days';
/** @api Default surfaced in views + tests */
public const DEFAULT_INACTIVITY_THRESHOLD_DAYS = 60;
private const MIN_INACTIVITY_THRESHOLD_DAYS = 7;
private const MAX_INACTIVITY_THRESHOLD_DAYS = 365;
public const AUTH_MODE_BASIC = 'basic';
public const AUTH_MODE_OAUTH2 = 'oauth2';
@@ -518,6 +525,49 @@ class HelpdeskSettingsGateway
return $normalized;
}
/**
* Days of silence after which a customer is flagged as "no contact".
* Clamped to a sane range; falls back to the default on invalid/empty values.
*
* @api Used by helpdesk/analytics + helpdesk/settings pages
*/
public function getInactivityThresholdDays(): int
{
$value = $this->settingsMetadataGateway->getValue(self::KEY_INACTIVITY_THRESHOLD_DAYS);
$value = trim((string) ($value ?? ''));
if ($value === '' || !is_numeric($value)) {
return self::DEFAULT_INACTIVITY_THRESHOLD_DAYS;
}
return max(
self::MIN_INACTIVITY_THRESHOLD_DAYS,
min(self::MAX_INACTIVITY_THRESHOLD_DAYS, (int) $value)
);
}
/** @api Used by the helpdesk/settings page */
public function setInactivityThresholdDays(?int $days): bool
{
if ($days === null) {
return $this->settingsMetadataGateway->set(
self::KEY_INACTIVITY_THRESHOLD_DAYS,
null,
'helpdesk.inactivity_threshold_days'
);
}
$clamped = max(
self::MIN_INACTIVITY_THRESHOLD_DAYS,
min(self::MAX_INACTIVITY_THRESHOLD_DAYS, $days)
);
return $this->settingsMetadataGateway->set(
self::KEY_INACTIVITY_THRESHOLD_DAYS,
(string) $clamped,
'helpdesk.inactivity_threshold_days'
);
}
/**
* Build the full OData entity URL for a given entity set.
*/