feat(helpdesk): add Domains list page with BC OData contract type enrichment

Add a new "Domains" page to the helpdesk module that fetches domain data
from the FS_Contract_Domains OData endpoint and enriches each domain with
its contract type (PI_Header_Type) by joining with FS_Contract_Lines_Test
where Type = 'Domain'.

New files:
- domains/index().php, index(default).phtml, filter-schema.php — list page
- domains-data().php — data endpoint with PHP-side filtering/sorting
- helpdesk-domains-index.js — Grid.js via initStandardListPage()

Gateway additions:
- listDomains() — fetch all contract domains
- listDomainContractLines() — fetch domain-type contract lines for type lookup

Sidebar restructured into three collapsible groups (Lookup, Monitoring,
Administration) with per-group icons and color coding, matching the core
admin panel pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 21:34:32 +02:00
parent 3f0db68b27
commit d472026df4
11 changed files with 586 additions and 38 deletions

View File

@@ -0,0 +1,157 @@
<?php
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
use MintyPHP\Module\Helpdesk\Service\BcODataGateway;
use MintyPHP\Support\Guard;
Guard::requireLogin();
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
gridRequireGetRequest();
$filters = gridParseFiltersFromSchemaFile(__DIR__ . '/domains/filter-schema.php');
$search = trim((string) ($filters['search'] ?? ''));
$customer = trim((string) ($filters['customer'] ?? ''));
$contractType = trim((string) ($filters['contract_type'] ?? ''));
$state = trim((string) ($filters['state'] ?? ''));
$state = $state === 'all' ? '' : $state;
$administration = trim((string) ($filters['administration'] ?? ''));
$administration = $administration === 'all' ? '' : $administration;
$order = (string) ($filters['order'] ?? 'Customer_Name');
$dir = (string) ($filters['dir'] ?? 'asc');
$limit = (int) ($filters['limit'] ?? 10);
$offset = (int) ($filters['offset'] ?? 0);
$gateway = app(BcODataGateway::class);
try {
$allDomains = $gateway->listDomains();
} catch (\Throwable) {
gridJsonDataResult([], 0);
return;
}
// Build contract type lookup: DNS number → {contract_type, contract_no, contract_description}
$contractLookup = [];
try {
$contractLines = $gateway->listDomainContractLines();
foreach ($contractLines as $line) {
$dnsNo = trim((string) ($line['No'] ?? ''));
if ($dnsNo !== '' && !isset($contractLookup[$dnsNo])) {
$contractLookup[$dnsNo] = [
'contract_type' => trim((string) ($line['PI_Header_Type'] ?? '')),
'contract_no' => trim((string) ($line['Header_No'] ?? '')),
'contract_description' => trim((string) ($line['PI_Header_Description'] ?? '')),
];
}
}
} catch (\Throwable) {
// Contract enrichment is best-effort — continue without it
}
// Enrich domains with contract type before filtering (so search/sort can use it)
foreach ($allDomains as &$domain) {
$domainNo = trim((string) ($domain['No'] ?? ''));
$contract = $contractLookup[$domainNo] ?? null;
$domain['contract_type'] = $contract['contract_type'] ?? '';
$domain['contract_no'] = $contract['contract_no'] ?? '';
$domain['contract_description'] = $contract['contract_description'] ?? '';
}
unset($domain);
$filtered = $allDomains;
// Text search across Customer_No, Customer_Name, URL, No, contract_type
if ($search !== '') {
$searchLower = mb_strtolower($search);
$filtered = array_values(array_filter($filtered, static function (array $domain) use ($searchLower): bool {
$haystack = mb_strtolower(
($domain['Customer_No'] ?? '') . ' '
. ($domain['Customer_Name'] ?? '') . ' '
. ($domain['URL'] ?? '') . ' '
. ($domain['No'] ?? '') . ' '
. ($domain['contract_type'] ?? '')
);
return str_contains($haystack, $searchLower);
}));
}
// Customer filter (partial match on Customer_No + Customer_Name)
if ($customer !== '') {
$customerLower = mb_strtolower($customer);
$filtered = array_values(array_filter($filtered, static function (array $domain) use ($customerLower): bool {
$haystack = mb_strtolower(
($domain['Customer_No'] ?? '') . ' '
. ($domain['Customer_Name'] ?? '')
);
return str_contains($haystack, $customerLower);
}));
}
// Contract type filter (partial match)
if ($contractType !== '') {
$contractTypeLower = mb_strtolower($contractType);
$filtered = array_values(array_filter($filtered, static function (array $domain) use ($contractTypeLower): bool {
return str_contains(mb_strtolower((string) ($domain['contract_type'] ?? '')), $contractTypeLower);
}));
}
// State filter (exact match)
if ($state !== '') {
$filtered = array_values(array_filter($filtered, static function (array $domain) use ($state): bool {
return trim((string) ($domain['State'] ?? '')) === $state;
}));
}
// Administration filter (exact match)
if ($administration !== '') {
$filtered = array_values(array_filter($filtered, static function (array $domain) use ($administration): bool {
return trim((string) ($domain['Administration'] ?? '')) === $administration;
}));
}
// 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
$stateVariantMap = [
'Aktiv' => 'success',
'In Vorbereitung' => 'warning',
];
$debitorBaseUrl = lurl('helpdesk/debitor/');
$preparedRows = [];
foreach ($rows as $domain) {
$customerNo = trim((string) ($domain['Customer_No'] ?? ''));
$domainState = (string) ($domain['State'] ?? '');
$preparedRows[] = [
'No' => (string) ($domain['No'] ?? ''),
'Customer_No' => $customerNo,
'Customer_Name' => (string) ($domain['Customer_Name'] ?? ''),
'URL' => (string) ($domain['URL'] ?? ''),
'State' => $domainState,
'state_variant' => $stateVariantMap[$domainState] ?? 'neutral',
'Administration' => (string) ($domain['Administration'] ?? ''),
'contract_type' => (string) ($domain['contract_type'] ?? ''),
'contract_no' => (string) ($domain['contract_no'] ?? ''),
'debitor_url' => $customerNo !== '' ? $debitorBaseUrl . rawurlencode($customerNo) : '',
];
}
gridJsonDataResult($preparedRows, $total);

View File

@@ -0,0 +1,76 @@
<?php
return gridFilterSchema([
'query' => [
'search' => ['type' => 'string'],
'customer' => ['type' => 'string'],
'contract_type' => ['type' => 'string'],
'state' => ['type' => 'string', 'default' => 'all'],
'administration' => ['type' => 'string', 'default' => 'all'],
'limit' => ['type' => 'int', 'default' => 10, 'min' => 1, 'max' => 100],
'offset' => ['type' => 'int', 'default' => 0, 'min' => 0],
'order' => [
'type' => 'order',
'allowed' => ['Customer_No', 'Customer_Name', 'URL', 'State', 'Administration', 'No', 'contract_type'],
'default' => 'Customer_Name',
],
'dir' => ['type' => 'dir', 'default' => 'asc'],
],
'toolbar' => [
[
'key' => 'search',
'type' => 'text',
'label' => 'Search',
'placeholder' => 'Search domains...',
'input_id' => 'helpdesk-domains-search-input',
'search' => true,
'query' => ['type' => 'string'],
],
[
'key' => 'customer',
'type' => 'text',
'label' => 'Customer',
'placeholder' => 'Customer name or no...',
'input_id' => 'helpdesk-domains-customer-filter',
'query' => ['type' => 'string'],
'label_attributes' => ['data-filter-optional' => true],
],
[
'key' => 'contract_type',
'type' => 'text',
'label' => 'Contract type',
'placeholder' => 'e.g. WEBSITE, SW&H...',
'input_id' => 'helpdesk-domains-contract-type-filter',
'query' => ['type' => 'string'],
'label_attributes' => ['data-filter-optional' => true],
],
[
'key' => 'state',
'type' => 'select',
'label' => 'State',
'input_id' => 'helpdesk-domains-state-filter',
'default' => 'all',
'normalize' => 'all_to_empty',
'allowed' => [
['id' => 'all', 'description' => 'All'],
['id' => 'Aktiv', 'description' => 'Aktiv'],
['id' => 'In Vorbereitung', 'description' => 'In Vorbereitung'],
],
'label_attributes' => ['data-filter-optional' => true],
],
[
'key' => 'administration',
'type' => 'select',
'label' => 'Administration',
'input_id' => 'helpdesk-domains-admin-filter',
'default' => 'all',
'normalize' => 'all_to_empty',
'allowed' => [
['id' => 'all', 'description' => 'All'],
['id' => 'Intern', 'description' => 'Intern'],
['id' => 'Extern', 'description' => 'Extern'],
],
'label_attributes' => ['data-filter-optional' => true],
],
],
]);

View File

@@ -0,0 +1,62 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
use MintyPHP\Module\Helpdesk\Service\HelpdeskSettingsGateway;
use MintyPHP\Support\Guard;
Guard::requireLogin();
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
$request = requestInput();
$query = $request->queryAll();
$filterSchema = require __DIR__ . '/filter-schema.php';
$listFilterContext = gridBuildListFilterContext($filterSchema, [
'query' => $query,
'search_keys' => ['search'],
]);
$toolbarFilterSchema = $listFilterContext['toolbarFilterSchema'];
$searchToolbarFilterSchema = $listFilterContext['searchToolbarFilterSchema'];
$drawerToolbarFilterSchema = $listFilterContext['drawerToolbarFilterSchema'];
$toolbarFilterState = $listFilterContext['toolbarFilterState'];
$clientFilterSchema = $listFilterContext['clientFilterSchema'];
$searchConfig = $listFilterContext['searchConfig'];
$schemaByKey = $listFilterContext['schemaByKey'] ?? [];
$filterChipMeta = [
'search' => [
'label' => t('Search'),
'type' => 'text',
],
'customer' => [
'label' => t('Customer'),
'type' => 'text',
],
'contract_type' => [
'label' => t('Contract type'),
'type' => 'text',
],
'state' => [
'label' => t('State'),
'type' => 'select',
'default' => (string) ($schemaByKey['state']['default'] ?? 'all'),
'options' => gridOptionMapFromAllowed((array) ($schemaByKey['state'] ?? [])),
],
'administration' => [
'label' => t('Administration'),
'type' => 'select',
'default' => (string) ($schemaByKey['administration']['default'] ?? 'all'),
'options' => gridOptionMapFromAllowed((array) ($schemaByKey['administration'] ?? [])),
],
];
$settingsGateway = app(HelpdeskSettingsGateway::class);
$isConfigured = $settingsGateway->isConfigured();
Buffer::set('title', t('Domains'));
Buffer::set('style_groups', json_encode(['helpdesk']));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Helpdesk'), 'path' => 'helpdesk'],
['label' => t('Domains')],
];

View File

@@ -0,0 +1,51 @@
<?php
$toolbarFilterSchema = is_array($toolbarFilterSchema ?? null) ? $toolbarFilterSchema : [];
$searchToolbarFilterSchema = is_array($searchToolbarFilterSchema ?? null) ? $searchToolbarFilterSchema : [];
$drawerToolbarFilterSchema = is_array($drawerToolbarFilterSchema ?? null) ? $drawerToolbarFilterSchema : [];
$toolbarFilterState = is_array($toolbarFilterState ?? null) ? $toolbarFilterState : [];
$toolbarOptionSets = is_array($toolbarOptionSets ?? null) ? $toolbarOptionSets : [];
$clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchema : [];
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
$filterChipMeta = is_array($filterChipMeta ?? null) ? $filterChipMeta : [];
$isConfigured = $isConfigured ?? false;
?>
<?php require templatePath('partials/app-flash.phtml'); ?>
<?php if (!$isConfigured): ?>
<div class="notice" data-variant="warning" role="alert">
<p><?php e(t('BC connection is not configured. Please configure the connection in the settings.')); ?></p>
<a href="<?php e(lurl('helpdesk/settings')); ?>" role="button" class="secondary outline small">
<?php e(t('Open settings')); ?>
</a>
</div>
<?php endif; ?>
<?php
$listTitle = t('Domains');
require templatePath('partials/app-list-titlebar.phtml');
?>
<?php
$filterUiNamespace = 'helpdesk-domains';
require templatePath('partials/app-list-filters.phtml');
?>
<div class="app-list-table">
<div id="helpdesk-domains-grid"></div>
</div>
<script src="<?php e(assetVersion('vendor/gridjs/gridjs.umd.js')); ?>"></script>
<script type="application/json" id="page-config-helpdesk-domains"><?php gridJsonForJs([
'dataUrl' => lurl('helpdesk/domains-data'),
'gridLang' => gridLang(),
'gridSearch' => $searchConfig,
'filterSchema' => $clientFilterSchema,
'filterChipMeta' => $filterChipMeta,
'labels' => [
'no' => t('No.'),
'customerName' => t('Customer Name'),
'url' => t('URL'),
'state' => t('State'),
'administration' => t('Administration'),
'contractType' => t('Contract type'),
],
]); ?></script>
<script type="module" src="<?php e(assetVersion('modules/helpdesk/js/pages/helpdesk-domains-index.js')); ?>"></script>