1
0
Files
breadcrumb-the-shire/modules/helpdesk/pages/helpdesk/debitor-contacts-data().php
fs 2f0f407268 feat(helpdesk): redesign dashboards with KPI groups, skeleton loading, and unified spacing
Restructure Support dashboard into 2 KPI groups (Tickets + Contracts) with
section dividers, merge escalations and recommendations into "Attention needed".
Redesign Sales dashboard with clickable contract timeline and dialog.
Add CSS shimmer skeleton loading for all dashboard tabs and aside.
Unify vertical rhythm via * + * sibling combinator on tab content containers.
Remove ~265 lines of dead CSS (contract cards, escalation styles) and unused JS
functions. Refactor hydrateTicketCategoryFilter into generic hydrateSelectFilter.
Fix role="button" CSS bleed from shell and remove extra future year from timeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 16:32:34 +02:00

199 lines
6.4 KiB
PHP

<?php
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
use MintyPHP\Module\Helpdesk\Service\DebitorCacheControl;
use MintyPHP\Module\Helpdesk\Service\DebitorDetailService;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
gridRequireGetRequest();
$filters = gridParseFiltersFromSchemaFile(__DIR__ . '/debitor-contacts-filter-schema.php');
$request = requestInput();
$customerNo = (string) ($filters['customerNo'] ?? '');
$customerName = (string) ($filters['customerName'] ?? '');
$refreshRequested = DebitorCacheControl::parseRefreshFlag($request->query('refresh', ''));
if ($customerNo === '' || $customerName === '') {
Router::json([
'data' => [],
'total' => 0,
'type_options' => [],
'meta' => [
'cache_used' => false,
'cache_bypassed' => $refreshRequested,
'cache_refreshed' => false,
],
]);
return;
}
// --- Session cache (lazy TTL = 300s) ---
$sessionStore = app(SessionStoreInterface::class);
$session = $sessionStore->all();
$tenantScope = DebitorCacheControl::resolveTenantScope($session);
if ($refreshRequested) {
DebitorCacheControl::invalidateDebitorCaches($sessionStore, $tenantScope, $customerNo, true);
}
$cacheKey = DebitorCacheControl::contactsKey($tenantScope, $customerNo);
$cacheTtl = DebitorCacheControl::TTL_STANDARD_SECONDS;
$cached = $sessionStore->get($cacheKey);
$allContacts = null;
$cacheUsed = false;
$serviceMeta = [
'source' => 'primary.company_name',
'fallback_used' => false,
'fallback_reason' => '',
];
if (!$refreshRequested && is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
$allContacts = $cached['contacts'] ?? [];
$cacheUsed = true;
if (is_array($cached['service_meta'] ?? null)) {
$serviceMeta = [
'source' => (string) ($cached['service_meta']['source'] ?? $serviceMeta['source']),
'fallback_used' => (bool) ($cached['service_meta']['fallback_used'] ?? false),
'fallback_reason' => (string) ($cached['service_meta']['fallback_reason'] ?? ''),
];
}
}
if ($allContacts === null) {
$service = app(DebitorDetailService::class);
$result = $service->loadContacts($customerNo, $customerName);
if (!($result['ok'] ?? false)) {
$resultMeta = is_array($result['meta'] ?? null) ? $result['meta'] : [];
Router::json([
'data' => [],
'total' => 0,
'type_options' => [],
'meta' => [
'cache_used' => $cacheUsed,
'cache_bypassed' => $refreshRequested,
'cache_refreshed' => $refreshRequested,
'source' => (string) ($resultMeta['source'] ?? $serviceMeta['source']),
'fallback_used' => (bool) ($resultMeta['fallback_used'] ?? false),
'fallback_reason' => (string) ($resultMeta['fallback_reason'] ?? ''),
],
]);
return;
}
$allContacts = $result['contacts'] ?? [];
$resultMeta = is_array($result['meta'] ?? null) ? $result['meta'] : [];
$serviceMeta = [
'source' => (string) ($resultMeta['source'] ?? $serviceMeta['source']),
'fallback_used' => (bool) ($resultMeta['fallback_used'] ?? false),
'fallback_reason' => (string) ($resultMeta['fallback_reason'] ?? ''),
];
$sessionStore->set($cacheKey, [
'contacts' => $allContacts,
'service_meta' => $serviceMeta,
'fetched_at' => time(),
]);
}
// --- Collect unique types for filter options ---
$typeSet = [];
foreach ($allContacts as $contact) {
$type = trim((string) ($contact['Type'] ?? ''));
if ($type !== '' && !isset($typeSet[$type])) {
$typeSet[$type] = true;
}
}
ksort($typeSet);
$typeOptions = [];
foreach (array_keys($typeSet) as $type) {
$typeOptions[] = ['id' => $type, 'description' => $type];
}
// --- PHP-side filtering ---
$search = trim((string) ($filters['search'] ?? ''));
$typeFilter = trim((string) ($filters['type'] ?? ''));
$order = (string) ($filters['order'] ?? 'Name');
$dir = (string) ($filters['dir'] ?? 'asc');
$limit = (int) ($filters['limit'] ?? 10);
$offset = (int) ($filters['offset'] ?? 0);
$filtered = $allContacts;
// Search: stripos on Name + Job_Title + E_Mail + Phone_No + Mobile_Phone_No
if ($search !== '') {
$searchLower = mb_strtolower($search);
$filtered = array_values(array_filter($filtered, static function (array $contact) use ($searchLower): bool {
$haystack = mb_strtolower(
($contact['Name'] ?? '') . ' '
. ($contact['Job_Title'] ?? '') . ' '
. ($contact['E_Mail'] ?? '') . ' '
. ($contact['Phone_No'] ?? '') . ' '
. ($contact['Mobile_Phone_No'] ?? '')
);
return str_contains($haystack, $searchLower);
}));
}
// Type filter
if ($typeFilter !== '') {
$filtered = array_values(array_filter($filtered, static function (array $contact) use ($typeFilter): bool {
return trim((string) ($contact['Type'] ?? '')) === $typeFilter;
}));
}
// --- Sorting ---
$total = count($filtered);
usort($filtered, static function (array $a, array $b) use ($order, $dir): int {
$va = (string) ($a[$order] ?? '');
$vb = (string) ($b[$order] ?? '');
$cmp = strnatcasecmp($va, $vb);
return $dir === 'desc' ? -$cmp : $cmp;
});
// --- Pagination ---
$rows = array_slice($filtered, $offset, $limit);
// --- Row preparation ---
$preparedRows = [];
foreach ($rows as $contact) {
$preparedRows[] = [
'Name' => (string) ($contact['Name'] ?? ''),
'Type' => (string) ($contact['Type'] ?? ''),
'Job_Title' => (string) ($contact['Job_Title'] ?? ''),
'E_Mail' => (string) ($contact['E_Mail'] ?? ''),
'Phone_No' => (string) ($contact['Phone_No'] ?? ''),
'Mobile_Phone_No' => (string) ($contact['Mobile_Phone_No'] ?? ''),
];
}
Router::json([
'data' => array_values($preparedRows),
'total' => max(0, $total),
'type_options' => $typeOptions,
'meta' => [
'cache_used' => $cacheUsed,
'cache_bypassed' => $refreshRequested,
'cache_refreshed' => $refreshRequested,
'source' => $serviceMeta['source'],
'fallback_used' => $serviceMeta['fallback_used'],
'fallback_reason' => $serviceMeta['fallback_reason'],
],
]);