feat(helpdesk): add domain detail page with customer, contract, and handover sections
New domain detail page showing customer info, contract data, linked handovers, updates section, and related domains for the same customer. Data loaded async with skeleton loading states. Includes DomainDetailService and tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Module\Helpdesk\Service;
|
||||
|
||||
use MintyPHP\Module\Helpdesk\Repository\HandoverRepository;
|
||||
use MintyPHP\Module\Helpdesk\Repository\UpdateRepository;
|
||||
|
||||
/**
|
||||
* Service for loading domain detail data (master data, customer, contract, handovers).
|
||||
*
|
||||
* Provides loadDomain() for the synchronous page load and loadDomainDetails()
|
||||
* for the async data endpoint.
|
||||
*/
|
||||
class DomainDetailService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BcODataGateway $bcODataGateway,
|
||||
private readonly EffectiveHelpdeskSettingsService $settingsGateway,
|
||||
private readonly HandoverRepository $handoverRepository,
|
||||
private readonly ?UpdateRepository $updateRepository = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load domain master data (1 OData call).
|
||||
*
|
||||
* Used by the main page load — customer, contract, handovers loaded async.
|
||||
*
|
||||
* @api Called from domain($id) action
|
||||
* @return array{status: string, domain?: array<string, mixed>, error?: string}
|
||||
*/
|
||||
public function loadDomain(string $domainNo): array
|
||||
{
|
||||
$domainNo = trim($domainNo);
|
||||
if ($domainNo === '') {
|
||||
return ['status' => 'not_found'];
|
||||
}
|
||||
|
||||
if (!$this->settingsGateway->isConfigured()) {
|
||||
return ['status' => 'not_configured', 'error' => 'BC connection not configured'];
|
||||
}
|
||||
|
||||
try {
|
||||
$domain = $this->bcODataGateway->getDomain($domainNo);
|
||||
} catch (\Throwable) {
|
||||
return ['status' => 'error', 'error' => 'BC connection failed'];
|
||||
}
|
||||
|
||||
if ($domain === null) {
|
||||
return ['status' => 'not_found'];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'domain' => $domain,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all detail data for a domain (for async data endpoint).
|
||||
*
|
||||
* @api Called from domain-detail-data endpoint
|
||||
* @return array{
|
||||
* ok: bool,
|
||||
* customer?: array<string, mixed>|null,
|
||||
* contract?: array<string, mixed>|null,
|
||||
* handovers?: list<array<string, mixed>>,
|
||||
* handover_stats?: array{total: int, draft: int, in_progress: int, completed: int, archived: int},
|
||||
* related_domains?: list<array<string, mixed>>,
|
||||
* error?: string
|
||||
* }
|
||||
*/
|
||||
public function loadDomainDetails(string $domainNo, string $customerNo, int $tenantId): array
|
||||
{
|
||||
$domainNo = trim($domainNo);
|
||||
$customerNo = trim($customerNo);
|
||||
|
||||
if ($domainNo === '') {
|
||||
return ['ok' => false, 'error' => 'Missing domain number'];
|
||||
}
|
||||
|
||||
$customer = null;
|
||||
if ($customerNo !== '') {
|
||||
try {
|
||||
$customer = $this->bcODataGateway->getCustomer($customerNo);
|
||||
} catch (\Throwable) {
|
||||
// Best-effort — continue without customer data
|
||||
}
|
||||
}
|
||||
|
||||
$contract = $this->loadContractForDomain($domainNo);
|
||||
|
||||
$handovers = $this->handoverRepository->findByDomainNo($tenantId, $domainNo);
|
||||
$handoverStats = $this->aggregateHandoverStats($handovers);
|
||||
|
||||
$relatedDomains = [];
|
||||
if ($customerNo !== '') {
|
||||
try {
|
||||
$allCustomerDomains = $this->bcODataGateway->getDomainsForCustomer($customerNo);
|
||||
$relatedDomains = array_values(array_filter(
|
||||
$allCustomerDomains,
|
||||
static fn (array $d): bool => trim((string) ($d['No'] ?? '')) !== $domainNo
|
||||
));
|
||||
} catch (\Throwable) {
|
||||
// Best-effort — continue without related domains
|
||||
}
|
||||
}
|
||||
|
||||
$updates = [];
|
||||
if ($this->updateRepository !== null) {
|
||||
$updates = $this->prepareUpdatesForResponse(
|
||||
$this->updateRepository->findByDomainNo($tenantId, $domainNo)
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'customer' => $customer,
|
||||
'contract' => $contract,
|
||||
'handovers' => $this->prepareHandoversForResponse($handovers),
|
||||
'handover_stats' => $handoverStats,
|
||||
'updates' => $updates,
|
||||
'related_domains' => $relatedDomains,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load contract line info for a specific domain from BC.
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function loadContractForDomain(string $domainNo): ?array
|
||||
{
|
||||
try {
|
||||
$contractLines = $this->bcODataGateway->listDomainContractLines();
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($contractLines as $line) {
|
||||
$lineNo = trim((string) ($line['No'] ?? ''));
|
||||
if ($lineNo === $domainNo) {
|
||||
return [
|
||||
'contract_type' => trim((string) ($line['PI_Header_Type'] ?? '')),
|
||||
'contract_no' => trim((string) ($line['Header_No'] ?? '')),
|
||||
'contract_description' => trim((string) ($line['PI_Header_Description'] ?? '')),
|
||||
'header_state' => trim((string) ($line['PI_Header_State'] ?? '')),
|
||||
'line_state' => trim((string) ($line['Line_State'] ?? '')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $handovers
|
||||
* @return array{total: int, draft: int, in_progress: int, completed: int, archived: int}
|
||||
*/
|
||||
private function aggregateHandoverStats(array $handovers): array
|
||||
{
|
||||
$stats = ['total' => 0, 'draft' => 0, 'in_progress' => 0, 'completed' => 0, 'archived' => 0];
|
||||
|
||||
foreach ($handovers as $handover) {
|
||||
$stats['total']++;
|
||||
$status = trim((string) ($handover['status'] ?? ''));
|
||||
if (isset($stats[$status])) {
|
||||
$stats[$status]++;
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare update rows for the JSON response.
|
||||
*
|
||||
* @param list<array<string, mixed>> $updates
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function prepareUpdatesForResponse(array $updates): array
|
||||
{
|
||||
$prepared = [];
|
||||
foreach ($updates as $update) {
|
||||
$categoryCode = (string) ($update['category_code'] ?? '');
|
||||
$status = (string) ($update['status'] ?? '');
|
||||
$prepared[] = [
|
||||
'ticket_no' => (string) ($update['ticket_no'] ?? ''),
|
||||
'category_code' => $categoryCode,
|
||||
'category_label' => UpdateService::categoryLabel($categoryCode),
|
||||
'category_variant' => UpdateService::categoryVariant($categoryCode),
|
||||
'gitea_path' => (string) ($update['gitea_path'] ?? ''),
|
||||
'status' => $status,
|
||||
'status_label' => UpdateService::statusLabel($status),
|
||||
'status_variant' => UpdateService::statusVariant($status),
|
||||
'created_at' => (string) ($update['created_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return $prepared;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare handover rows for the JSON response (strip internal fields).
|
||||
*
|
||||
* @param list<array<string, mixed>> $handovers
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function prepareHandoversForResponse(array $handovers): array
|
||||
{
|
||||
$prepared = [];
|
||||
foreach ($handovers as $handover) {
|
||||
$prepared[] = [
|
||||
'id' => (int) ($handover['id'] ?? 0),
|
||||
'product_code' => (string) ($handover['product_code'] ?? ''),
|
||||
'product_name' => (string) ($handover['product_name'] ?? ''),
|
||||
'product_bc_description' => (string) ($handover['product_bc_description'] ?? ''),
|
||||
'status' => (string) ($handover['status'] ?? ''),
|
||||
'created_by_name' => (string) ($handover['created_by_name'] ?? ''),
|
||||
'created_at' => (string) ($handover['created_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return $prepared;
|
||||
}
|
||||
}
|
||||
59
modules/helpdesk/pages/helpdesk/domain($id).php
Normal file
59
modules/helpdesk/pages/helpdesk/domain($id).php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\DomainDetailService;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
||||
|
||||
$domainNo = trim((string) ($id ?? ''));
|
||||
|
||||
if ($domainNo === '') {
|
||||
Router::redirect('helpdesk/domains');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$domainDetailService = app(DomainDetailService::class);
|
||||
$result = $domainDetailService->loadDomain($domainNo);
|
||||
|
||||
$resultStatus = (string) ($result['status'] ?? '');
|
||||
|
||||
if ($resultStatus === 'not_found') {
|
||||
Flash::error(t('Domain not found'), 'helpdesk-domains', 'domain_not_found');
|
||||
Router::redirect('helpdesk/domains');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($resultStatus === 'not_configured') {
|
||||
Flash::error(t('BC connection is not configured'), 'helpdesk-domains', 'not_configured');
|
||||
Router::redirect('helpdesk/domains');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$domain = $result['domain'] ?? [];
|
||||
$hasError = $resultStatus === 'error';
|
||||
$errorMessage = $result['error'] ?? '';
|
||||
|
||||
$domainUrl = (string) ($domain['URL'] ?? '');
|
||||
$customerNo = (string) ($domain['Customer_No'] ?? '');
|
||||
$customerName = (string) ($domain['Customer_Name'] ?? '');
|
||||
$domainState = (string) ($domain['State'] ?? '');
|
||||
$domainAdministration = (string) ($domain['Administration'] ?? '');
|
||||
|
||||
$pageTitle = $domainUrl !== '' ? $domainUrl : $domainNo;
|
||||
|
||||
Buffer::set('title', $pageTitle . ' — ' . t('Helpdesk'));
|
||||
Buffer::set('style_groups', json_encode(['helpdesk']));
|
||||
$breadcrumbs = [
|
||||
['label' => t('Home'), 'path' => 'admin'],
|
||||
['label' => t('Helpdesk'), 'path' => 'helpdesk'],
|
||||
['label' => t('Domains'), 'path' => 'helpdesk/domains'],
|
||||
['label' => $pageTitle],
|
||||
];
|
||||
234
modules/helpdesk/pages/helpdesk/domain(default).phtml
Normal file
234
modules/helpdesk/pages/helpdesk/domain(default).phtml
Normal file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var string $domainNo
|
||||
* @var array $domain
|
||||
* @var bool $hasError
|
||||
* @var string $errorMessage
|
||||
* @var string $domainUrl
|
||||
* @var string $customerNo
|
||||
* @var string $customerName
|
||||
* @var string $domainState
|
||||
* @var string $domainAdministration
|
||||
* @var string $pageTitle
|
||||
*/
|
||||
|
||||
$domainNo = $domainNo ?? '';
|
||||
$domain = is_array($domain ?? null) ? $domain : [];
|
||||
$hasError = $hasError ?? false;
|
||||
$errorMessage = $errorMessage ?? '';
|
||||
$domainUrl = $domainUrl ?? '';
|
||||
$customerNo = $customerNo ?? '';
|
||||
$customerName = $customerName ?? '';
|
||||
$domainState = $domainState ?? '';
|
||||
$domainAdministration = $domainAdministration ?? '';
|
||||
$pageTitle = $pageTitle ?? $domainNo;
|
||||
|
||||
$stateVariantMap = [
|
||||
'Aktiv' => 'success',
|
||||
'In Vorbereitung' => 'warning',
|
||||
];
|
||||
$stateVariant = $stateVariantMap[$domainState] ?? 'neutral';
|
||||
|
||||
?>
|
||||
<div class="app-details-container"
|
||||
data-domain-no="<?php e($domainNo); ?>"
|
||||
data-customer-no="<?php e($customerNo); ?>"
|
||||
data-customer-name="<?php e($customerName); ?>"
|
||||
data-detail-url="<?php e(lurl('helpdesk/domain-detail-data')); ?>"
|
||||
data-debitor-base-url="<?php e(lurl('helpdesk/debitor/')); ?>"
|
||||
data-handover-edit-base-url="<?php e(lurl('helpdesk/handovers/edit/')); ?>"
|
||||
data-domain-base-url="<?php e(lurl('helpdesk/domain/')); ?>"
|
||||
data-ticket-base-url="<?php e(lurl('helpdesk/ticket/')); ?>"
|
||||
>
|
||||
<section>
|
||||
<?php
|
||||
$titlebar = [
|
||||
'title' => $pageTitle,
|
||||
'backHref' => lurl('helpdesk/domains'),
|
||||
'backTitle' => t('Back to domains'),
|
||||
'actions' => [],
|
||||
];
|
||||
require templatePath('partials/app-details-titlebar.phtml');
|
||||
?>
|
||||
|
||||
<?php require templatePath('partials/app-flash.phtml'); ?>
|
||||
|
||||
<?php if ($hasError): ?>
|
||||
<div class="notice" data-variant="error" role="alert">
|
||||
<p><?php e(t('Could not connect to Business Central.')); ?></p>
|
||||
<?php if ($errorMessage !== ''): ?>
|
||||
<small><?php e($errorMessage); ?></small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php elseif ($domain !== []): ?>
|
||||
|
||||
<div class="app-tabs" data-tabs data-app-component="tabs" data-tabs-param="tab" data-tabs-storage-key="helpdesk-domain-detail">
|
||||
<div class="app-tabs-nav">
|
||||
<button type="button" data-tab="overview" data-tab-default>
|
||||
<?php e(t('Overview')); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Overview panel -->
|
||||
<div data-tab-panel="overview" data-tab-panel-wide>
|
||||
|
||||
<!-- Loading skeleton -->
|
||||
<div id="domain-loading" aria-busy="true">
|
||||
<div class="helpdesk-domain-overview-grid">
|
||||
<div class="helpdesk-skeleton-block" style="height: 180px;"></div>
|
||||
<div class="helpdesk-skeleton-block" style="height: 180px;"></div>
|
||||
</div>
|
||||
<div class="helpdesk-skeleton-block" style="height: 120px; margin-top: calc(var(--app-spacing) * 1.25);"></div>
|
||||
</div>
|
||||
|
||||
<!-- Content (hidden until async load) -->
|
||||
<div id="domain-content" hidden>
|
||||
|
||||
<!-- Customer + Contract side by side -->
|
||||
<div class="helpdesk-domain-overview-grid">
|
||||
|
||||
<!-- Customer Info -->
|
||||
<details class="app-details-card" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Customer information')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<div id="domain-customer-info">
|
||||
<p class="text-muted"><?php e(t('Loading...')); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Contract Info -->
|
||||
<details class="app-details-card" id="domain-contract-section" open hidden>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Contract information')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<div id="domain-contract-info">
|
||||
<p class="text-muted"><?php e(t('Loading...')); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Handovers (full width) -->
|
||||
<details class="app-details-card" open >
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Handovers')); ?></span>
|
||||
<span class="app-details-card-summary-meta">
|
||||
<span class="badge" data-variant="neutral" id="domain-handover-count">0</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<div id="domain-handovers-list"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Updates (full width) -->
|
||||
<details class="app-details-card" id="domain-updates-section" open hidden>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Updates')); ?></span>
|
||||
<span class="app-details-card-summary-meta">
|
||||
<span class="badge" data-variant="neutral" id="domain-update-count">0</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<div id="domain-updates-list"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<?php if (!$hasError && $domain !== []): ?>
|
||||
<aside id="app-details-aside-section">
|
||||
<div class="app-details-aside-section">
|
||||
|
||||
<hgroup>
|
||||
<h2><?php e($domainNo); ?></h2>
|
||||
<?php if ($domainUrl !== ''): ?>
|
||||
<p><?php e($domainUrl); ?></p>
|
||||
<?php endif; ?>
|
||||
</hgroup>
|
||||
|
||||
<div class="badge-list">
|
||||
<?php if ($domainState !== ''): ?>
|
||||
<span class="badge" data-variant="<?php e($stateVariant); ?>"><?php e($domainState); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($domainAdministration !== ''): ?>
|
||||
<span class="badge" data-variant="neutral"><?php e($domainAdministration); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<?php if ($domainUrl !== ''): ?>
|
||||
<a href="<?php e('https://' . ltrim($domainUrl, 'htps:/')); ?>" target="_blank" rel="noopener" class="secondary outline small" role="button">
|
||||
<i class="bi bi-box-arrow-up-right" aria-hidden="true"></i>
|
||||
<?php e(t('Open domain')); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="domain-related-section" hidden>
|
||||
<hr>
|
||||
<details name="domain-aside" open>
|
||||
<summary><?php e(t('Related domains')); ?></summary>
|
||||
<div id="domain-related-domains"></div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!$hasError && $domain !== []): ?>
|
||||
<script type="application/json" id="helpdesk-domain-i18n"><?php gridJsonForJs([
|
||||
'Loading...' => t('Loading...'),
|
||||
'Network error — please try again' => t('Network error — please try again'),
|
||||
'No handovers for this domain yet' => t('No handovers for this domain yet'),
|
||||
'No contract information available' => t('No contract information available'),
|
||||
'Create handover' => t('Create handover'),
|
||||
'Product' => t('Product'),
|
||||
'Status' => t('Status'),
|
||||
'Created by' => t('Created by'),
|
||||
'Created' => t('Created'),
|
||||
'Name' => t('Name'),
|
||||
'Customer No.' => t('Customer No.'),
|
||||
'Address' => t('Address'),
|
||||
'City' => t('City'),
|
||||
'Postal code' => t('Postal code'),
|
||||
'Phone' => t('Phone'),
|
||||
'E-Mail' => t('E-Mail'),
|
||||
'Salesperson' => t('Salesperson'),
|
||||
'Support type' => t('Support type'),
|
||||
'Contract type' => t('Contract type'),
|
||||
'Contract No.' => t('Contract No.'),
|
||||
'Description' => t('Description'),
|
||||
'Contract state' => t('Contract state'),
|
||||
'Line state' => t('Line state'),
|
||||
'URL' => t('URL'),
|
||||
'No.' => t('No.'),
|
||||
'State' => t('State'),
|
||||
'Total' => t('Total'),
|
||||
'Draft' => t('Draft'),
|
||||
'In progress' => t('In progress'),
|
||||
'Completed' => t('Completed'),
|
||||
'Archived' => t('Archived'),
|
||||
'No updates for this domain yet' => t('No updates for this domain yet'),
|
||||
'Ticket' => t('Ticket'),
|
||||
'Type' => t('Type'),
|
||||
'Gitea link' => t('Gitea link'),
|
||||
'Update' => t('Update'),
|
||||
'Hotfix' => t('Hotfix'),
|
||||
]); ?></script>
|
||||
|
||||
<script type="module" src="<?php e(assetVersion('modules/helpdesk/js/helpdesk-domain-detail.js')); ?>"></script>
|
||||
<?php endif; ?>
|
||||
29
modules/helpdesk/pages/helpdesk/domain-detail-data().php
Normal file
29
modules/helpdesk/pages/helpdesk/domain-detail-data().php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\DomainDetailService;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
||||
gridRequireGetRequest();
|
||||
|
||||
$request = requestInput();
|
||||
$domainNo = trim((string) $request->query('domainNo', ''));
|
||||
$customerNo = trim((string) $request->query('customerNo', ''));
|
||||
|
||||
if ($domainNo === '') {
|
||||
Router::json(['ok' => false, 'error' => 'Missing domain number']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$session = app(SessionStoreInterface::class)->all();
|
||||
$tenantId = (int) ($session['current_tenant']['id'] ?? 0);
|
||||
|
||||
$service = app(DomainDetailService::class);
|
||||
$result = $service->loadDomainDetails($domainNo, $customerNo, $tenantId);
|
||||
|
||||
Router::json($result);
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Module\Helpdesk\Service;
|
||||
|
||||
use MintyPHP\Module\Helpdesk\Repository\HandoverRepository;
|
||||
use MintyPHP\Module\Helpdesk\Service\BcODataGateway;
|
||||
use MintyPHP\Module\Helpdesk\Service\DomainDetailService;
|
||||
use MintyPHP\Module\Helpdesk\Service\EffectiveHelpdeskSettingsService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DomainDetailServiceTest extends TestCase
|
||||
{
|
||||
private const TENANT_ID = 1;
|
||||
|
||||
private function createService(
|
||||
?BcODataGateway $gateway = null,
|
||||
?EffectiveHelpdeskSettingsService $settings = null,
|
||||
?HandoverRepository $repository = null,
|
||||
): DomainDetailService {
|
||||
$gateway = $gateway ?? $this->createMock(BcODataGateway::class);
|
||||
$settings = $settings ?? $this->createConfiguredSettings(true);
|
||||
$repository = $repository ?? $this->createMock(HandoverRepository::class);
|
||||
|
||||
return new DomainDetailService($gateway, $settings, $repository);
|
||||
}
|
||||
|
||||
private function createConfiguredSettings(bool $configured): EffectiveHelpdeskSettingsService
|
||||
{
|
||||
$mock = $this->createMock(EffectiveHelpdeskSettingsService::class);
|
||||
$mock->method('isConfigured')->willReturn($configured);
|
||||
|
||||
return $mock;
|
||||
}
|
||||
|
||||
// ── loadDomain tests ─────────────────────────────────────────
|
||||
|
||||
public function testLoadDomainHappyPath(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getDomain')->willReturn([
|
||||
'No' => 'DNS00001',
|
||||
'Customer_No' => 'D10001',
|
||||
'Customer_Name' => 'Acme Corp',
|
||||
'URL' => 'example.com',
|
||||
'State' => 'Aktiv',
|
||||
'Administration' => 'Intern',
|
||||
]);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadDomain('DNS00001');
|
||||
|
||||
$this->assertSame('success', $result['status']);
|
||||
$this->assertSame('DNS00001', $result['domain']['No']);
|
||||
$this->assertSame('example.com', $result['domain']['URL']);
|
||||
}
|
||||
|
||||
public function testLoadDomainNotFound(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getDomain')->willReturn(null);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadDomain('INVALID');
|
||||
|
||||
$this->assertSame('not_found', $result['status']);
|
||||
}
|
||||
|
||||
public function testLoadDomainEmptyNo(): void
|
||||
{
|
||||
$service = $this->createService();
|
||||
$result = $service->loadDomain('');
|
||||
|
||||
$this->assertSame('not_found', $result['status']);
|
||||
}
|
||||
|
||||
public function testLoadDomainNotConfigured(): void
|
||||
{
|
||||
$service = $this->createService(settings: $this->createConfiguredSettings(false));
|
||||
$result = $service->loadDomain('DNS00001');
|
||||
|
||||
$this->assertSame('not_configured', $result['status']);
|
||||
}
|
||||
|
||||
public function testLoadDomainBcError(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getDomain')->willThrowException(new \RuntimeException('Connection failed'));
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadDomain('DNS00001');
|
||||
|
||||
$this->assertSame('error', $result['status']);
|
||||
$this->assertSame('BC connection failed', $result['error']);
|
||||
}
|
||||
|
||||
// ── loadDomainDetails tests ──────────────────────────────────
|
||||
|
||||
public function testLoadDomainDetailsHappyPath(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getCustomer')->willReturn([
|
||||
'No' => 'D10001',
|
||||
'Name' => 'Acme Corp',
|
||||
'Address' => 'Main St 1',
|
||||
]);
|
||||
$gateway->method('listDomainContractLines')->willReturn([
|
||||
['No' => 'DNS00001', 'Header_No' => 'V1000', 'PI_Header_Type' => 'WEBSITE', 'PI_Header_Description' => 'Website Contract', 'PI_Header_State' => 'Aktiv', 'Line_State' => 'Aktiv'],
|
||||
['No' => 'DNS00002', 'Header_No' => 'V2000', 'PI_Header_Type' => 'SW&H', 'PI_Header_Description' => 'Other', 'PI_Header_State' => 'Aktiv', 'Line_State' => 'Aktiv'],
|
||||
]);
|
||||
$gateway->method('getDomainsForCustomer')->willReturn([
|
||||
['No' => 'DNS00001', 'URL' => 'example.com', 'State' => 'Aktiv'],
|
||||
['No' => 'DNS00002', 'URL' => 'other.com', 'State' => 'Aktiv'],
|
||||
]);
|
||||
|
||||
$repo = $this->createMock(HandoverRepository::class);
|
||||
$repo->method('findByDomainNo')->willReturn([
|
||||
['id' => 1, 'product_code' => 'PROD-A', 'product_name' => 'Product A', 'product_bc_description' => '', 'status' => 'draft', 'created_by_name' => 'John', 'created_at' => '2026-01-01 10:00:00'],
|
||||
['id' => 2, 'product_code' => 'PROD-A', 'product_name' => 'Product A', 'product_bc_description' => '', 'status' => 'completed', 'created_by_name' => 'Jane', 'created_at' => '2026-02-15 14:00:00'],
|
||||
]);
|
||||
|
||||
$service = $this->createService($gateway, repository: $repo);
|
||||
$result = $service->loadDomainDetails('DNS00001', 'D10001', self::TENANT_ID);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame('Acme Corp', $result['customer']['Name']);
|
||||
$this->assertSame('WEBSITE', $result['contract']['contract_type']);
|
||||
$this->assertSame('V1000', $result['contract']['contract_no']);
|
||||
$this->assertCount(2, $result['handovers']);
|
||||
$this->assertSame(2, $result['handover_stats']['total']);
|
||||
$this->assertSame(1, $result['handover_stats']['draft']);
|
||||
$this->assertSame(1, $result['handover_stats']['completed']);
|
||||
$this->assertCount(1, $result['related_domains']);
|
||||
$this->assertSame('DNS00002', $result['related_domains'][0]['No']);
|
||||
}
|
||||
|
||||
public function testLoadDomainDetailsNoContract(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getCustomer')->willReturn(['No' => 'D10001', 'Name' => 'Acme']);
|
||||
$gateway->method('listDomainContractLines')->willReturn([]);
|
||||
$gateway->method('getDomainsForCustomer')->willReturn([]);
|
||||
|
||||
$repo = $this->createMock(HandoverRepository::class);
|
||||
$repo->method('findByDomainNo')->willReturn([]);
|
||||
|
||||
$service = $this->createService($gateway, repository: $repo);
|
||||
$result = $service->loadDomainDetails('DNS00001', 'D10001', self::TENANT_ID);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertNull($result['contract']);
|
||||
$this->assertSame(0, $result['handover_stats']['total']);
|
||||
$this->assertEmpty($result['related_domains']);
|
||||
}
|
||||
|
||||
public function testLoadDomainDetailsEmptyDomainNo(): void
|
||||
{
|
||||
$service = $this->createService();
|
||||
$result = $service->loadDomainDetails('', 'D10001', self::TENANT_ID);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('Missing domain number', $result['error']);
|
||||
}
|
||||
|
||||
public function testLoadDomainDetailsCustomerFetchFails(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getCustomer')->willThrowException(new \RuntimeException('BC error'));
|
||||
$gateway->method('listDomainContractLines')->willReturn([]);
|
||||
$gateway->method('getDomainsForCustomer')->willReturn([]);
|
||||
|
||||
$repo = $this->createMock(HandoverRepository::class);
|
||||
$repo->method('findByDomainNo')->willReturn([]);
|
||||
|
||||
$service = $this->createService($gateway, repository: $repo);
|
||||
$result = $service->loadDomainDetails('DNS00001', 'D10001', self::TENANT_ID);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertNull($result['customer']);
|
||||
}
|
||||
|
||||
public function testLoadDomainDetailsHandoverStatAggregation(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getCustomer')->willReturn(null);
|
||||
$gateway->method('listDomainContractLines')->willReturn([]);
|
||||
$gateway->method('getDomainsForCustomer')->willReturn([]);
|
||||
|
||||
$repo = $this->createMock(HandoverRepository::class);
|
||||
$repo->method('findByDomainNo')->willReturn([
|
||||
['id' => 1, 'product_code' => 'P', 'product_name' => '', 'product_bc_description' => '', 'status' => 'draft', 'created_by_name' => '', 'created_at' => ''],
|
||||
['id' => 2, 'product_code' => 'P', 'product_name' => '', 'product_bc_description' => '', 'status' => 'in_progress', 'created_by_name' => '', 'created_at' => ''],
|
||||
['id' => 3, 'product_code' => 'P', 'product_name' => '', 'product_bc_description' => '', 'status' => 'in_progress', 'created_by_name' => '', 'created_at' => ''],
|
||||
['id' => 4, 'product_code' => 'P', 'product_name' => '', 'product_bc_description' => '', 'status' => 'archived', 'created_by_name' => '', 'created_at' => ''],
|
||||
]);
|
||||
|
||||
$service = $this->createService($gateway, repository: $repo);
|
||||
$result = $service->loadDomainDetails('DNS00001', '', self::TENANT_ID);
|
||||
|
||||
$this->assertSame(4, $result['handover_stats']['total']);
|
||||
$this->assertSame(1, $result['handover_stats']['draft']);
|
||||
$this->assertSame(2, $result['handover_stats']['in_progress']);
|
||||
$this->assertSame(0, $result['handover_stats']['completed']);
|
||||
$this->assertSame(1, $result['handover_stats']['archived']);
|
||||
}
|
||||
}
|
||||
321
modules/helpdesk/web/js/helpdesk-domain-detail.js
Normal file
321
modules/helpdesk/web/js/helpdesk-domain-detail.js
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* Helpdesk domain detail page — read-only dashboard.
|
||||
*
|
||||
* Fetches customer, contract, handover, and related domain data
|
||||
* from a single async endpoint and renders all sections.
|
||||
*
|
||||
* All dynamic values are escaped via the esc() helper before insertion
|
||||
* into innerHTML. This matches the pattern used in helpdesk-detail.js
|
||||
* and other helpdesk JS modules in this codebase.
|
||||
*/
|
||||
import { renderEmptyState } from './helpdesk-empty-state.js';
|
||||
|
||||
function esc(str) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = String(str ?? '');
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '—';
|
||||
const d = new Date(dateStr);
|
||||
if (isNaN(d.getTime())) return String(dateStr);
|
||||
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
}
|
||||
|
||||
const STATUS_LABEL_MAP = {
|
||||
draft: 'Draft',
|
||||
in_progress: 'In progress',
|
||||
completed: 'Completed',
|
||||
archived: 'Archived',
|
||||
};
|
||||
|
||||
const container = document.querySelector('.app-details-container[data-domain-no]');
|
||||
if (container) {
|
||||
init(container);
|
||||
}
|
||||
|
||||
function init(container) {
|
||||
const domainNo = container.dataset.domainNo || '';
|
||||
const customerNo = container.dataset.customerNo || '';
|
||||
const detailUrl = container.dataset.detailUrl || '';
|
||||
const debitorBaseUrl = container.dataset.debitorBaseUrl || '';
|
||||
const handoverEditBaseUrl = container.dataset.handoverEditBaseUrl || '';
|
||||
const domainBaseUrl = container.dataset.domainBaseUrl || '';
|
||||
const ticketBaseUrl = container.dataset.ticketBaseUrl || '';
|
||||
|
||||
if (!domainNo || !detailUrl) return;
|
||||
|
||||
const i18nEl = document.getElementById('helpdesk-domain-i18n');
|
||||
const i18n = i18nEl ? JSON.parse(i18nEl.textContent) : {};
|
||||
const t = (key) => i18n[key] || key;
|
||||
|
||||
fetchDomainDetails();
|
||||
|
||||
async function fetchDomainDetails() {
|
||||
const loadingEl = document.getElementById('domain-loading');
|
||||
const contentEl = document.getElementById('domain-content');
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({ domainNo, customerNo });
|
||||
const res = await fetch(detailUrl + '?' + params, { credentials: 'same-origin' });
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.ok) {
|
||||
showError(data.error || t('Network error — please try again'));
|
||||
return;
|
||||
}
|
||||
|
||||
renderCustomerInfo(data.customer);
|
||||
renderContractInfo(data.contract);
|
||||
renderHandoversList(data.handovers || []);
|
||||
renderUpdatesList(data.updates || []);
|
||||
renderRelatedDomains(data.related_domains || []);
|
||||
|
||||
if (loadingEl) loadingEl.hidden = true;
|
||||
if (contentEl) contentEl.hidden = false;
|
||||
} catch {
|
||||
showError(t('Network error — please try again'));
|
||||
}
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const loadingEl = document.getElementById('domain-loading');
|
||||
if (loadingEl) {
|
||||
// safe-html: esc() escapes all dynamic content
|
||||
loadingEl.innerHTML = '<div class="notice" data-variant="error" role="alert"><p>' + esc(message) + '</p></div>';
|
||||
loadingEl.removeAttribute('aria-busy');
|
||||
}
|
||||
}
|
||||
|
||||
function renderCustomerInfo(customer) {
|
||||
const el = document.getElementById('domain-customer-info');
|
||||
if (!el) return;
|
||||
|
||||
if (!customer) {
|
||||
el.textContent = '';
|
||||
const p = document.createElement('p');
|
||||
p.className = 'text-muted';
|
||||
p.textContent = t('No contract information available');
|
||||
el.appendChild(p);
|
||||
return;
|
||||
}
|
||||
|
||||
const name = customer.Name || '';
|
||||
const no = customer.No || '';
|
||||
const address = customer.Address || '';
|
||||
const city = customer.City || '';
|
||||
const postCode = customer.Post_Code || '';
|
||||
const phone = customer.Phone_No || '';
|
||||
const email = customer.E_Mail || '';
|
||||
const salesperson = customer.Salesperson_Code || '';
|
||||
const supportType = customer.Support_Type || '';
|
||||
|
||||
const debitorLink = no && debitorBaseUrl
|
||||
? '<a href="' + esc(debitorBaseUrl + encodeURIComponent(no)) + '">' + esc(name || no) + '</a>'
|
||||
: esc(name || no);
|
||||
|
||||
// safe-html: all dynamic values are escaped via esc() before insertion
|
||||
el.innerHTML =
|
||||
'<dl class="helpdesk-ticket-meta">' +
|
||||
'<div class="helpdesk-ticket-meta-item"><dt>' + esc(t('Name')) + '</dt><dd>' + debitorLink + '</dd></div>' +
|
||||
'<div class="helpdesk-ticket-meta-item"><dt>' + esc(t('Customer No.')) + '</dt><dd>' + esc(no) + '</dd></div>' +
|
||||
'<div class="helpdesk-ticket-meta-item"><dt>' + esc(t('Phone')) + '</dt><dd>' + (phone ? '<a href="tel:' + esc(phone) + '">' + esc(phone) + '</a>' : '—') + '</dd></div>' +
|
||||
(salesperson ? '<div class="helpdesk-ticket-meta-item"><dt>' + esc(t('Salesperson')) + '</dt><dd>' + esc(salesperson) + '</dd></div>' : '') +
|
||||
(supportType ? '<div class="helpdesk-ticket-meta-item"><dt>' + esc(t('Support type')) + '</dt><dd>' + esc(supportType) + '</dd></div>' : '') +
|
||||
'</dl>';
|
||||
}
|
||||
|
||||
function renderContractInfo(contract) {
|
||||
const section = document.getElementById('domain-contract-section');
|
||||
const el = document.getElementById('domain-contract-info');
|
||||
if (!el || !section) return;
|
||||
|
||||
if (!contract || !contract.contract_type) {
|
||||
section.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
section.hidden = false;
|
||||
// safe-html: all dynamic values are escaped via esc() before insertion
|
||||
el.innerHTML =
|
||||
'<dl class="helpdesk-ticket-meta">' +
|
||||
'<div class="helpdesk-ticket-meta-item"><dt>' + esc(t('Contract type')) + '</dt><dd>' + esc(contract.contract_type) + '</dd></div>' +
|
||||
'<div class="helpdesk-ticket-meta-item"><dt>' + esc(t('Description')) + '</dt><dd>' + esc(contract.contract_description || '—') + '</dd></div>' +
|
||||
'<div class="helpdesk-ticket-meta-item"><dt>' + esc(t('Contract No.')) + '</dt><dd>' + esc(contract.contract_no || '—') + '</dd></div>' +
|
||||
(contract.header_state ? '<div class="helpdesk-ticket-meta-item"><dt>' + esc(t('Contract state')) + '</dt><dd>' + esc(contract.header_state) + '</dd></div>' : '') +
|
||||
'</dl>';
|
||||
}
|
||||
|
||||
function renderHandoversList(handovers) {
|
||||
const el = document.getElementById('domain-handovers-list');
|
||||
const countEl = document.getElementById('domain-handover-count');
|
||||
if (!el) return;
|
||||
|
||||
if (countEl) countEl.textContent = String(handovers.length);
|
||||
|
||||
if (!handovers || handovers.length === 0) {
|
||||
el.innerHTML = renderEmptyState({ // safe-html: renderEmptyState escapes internally
|
||||
message: t('No handovers for this domain yet'),
|
||||
size: 'compact',
|
||||
icon: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const statusVariantMap = {
|
||||
draft: 'neutral',
|
||||
in_progress: 'info',
|
||||
completed: 'success',
|
||||
archived: 'neutral',
|
||||
};
|
||||
|
||||
const returnPath = 'helpdesk/domain/' + encodeURIComponent(domainNo);
|
||||
|
||||
let items = '';
|
||||
for (const h of handovers) {
|
||||
const editUrl = handoverEditBaseUrl + encodeURIComponent(h.id) + '?return=' + encodeURIComponent(returnPath);
|
||||
const productLabel = h.product_name || h.product_bc_description || h.product_code || '—';
|
||||
const statusVariant = statusVariantMap[h.status] || 'neutral';
|
||||
const statusLabel = t(STATUS_LABEL_MAP[h.status] || h.status);
|
||||
const meta = [h.created_by_name, formatDate(h.created_at)].filter(Boolean).join(' · ');
|
||||
|
||||
// safe-html: all dynamic values are escaped via esc()
|
||||
items +=
|
||||
'<a href="' + esc(editUrl) + '" class="helpdesk-domain-handover-item">' +
|
||||
'<div class="helpdesk-domain-handover-item-main">' +
|
||||
'<span class="helpdesk-domain-handover-item-product">' + esc(productLabel) + '</span>' +
|
||||
'<span class="badge" data-variant="' + esc(statusVariant) + '">' + esc(statusLabel) + '</span>' +
|
||||
'</div>' +
|
||||
(meta ? '<div class="helpdesk-domain-handover-item-meta">' + esc(meta) + '</div>' : '') +
|
||||
'</a>';
|
||||
}
|
||||
|
||||
// safe-html: all content built from esc()-escaped values
|
||||
el.innerHTML = '<div class="helpdesk-domain-handover-list">' + items + '</div>';
|
||||
}
|
||||
|
||||
function renderUpdatesList(updates) {
|
||||
const section = document.getElementById('domain-updates-section');
|
||||
const el = document.getElementById('domain-updates-list');
|
||||
const countEl = document.getElementById('domain-update-count');
|
||||
if (!el || !section) return;
|
||||
|
||||
if (!updates || updates.length === 0) {
|
||||
section.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
section.hidden = false;
|
||||
if (countEl) countEl.textContent = String(updates.length);
|
||||
|
||||
// Build update items using DOM methods for safety
|
||||
const listDiv = document.createElement('div');
|
||||
listDiv.className = 'helpdesk-domain-handover-list';
|
||||
|
||||
for (const u of updates) {
|
||||
const ticketNo = u.ticket_no || '';
|
||||
const ticketUrl = ticketNo && ticketBaseUrl ? ticketBaseUrl + encodeURIComponent(ticketNo) : '';
|
||||
const categoryLabel = u.category_label || u.category_code || '';
|
||||
const categoryVariant = u.category_variant || 'neutral';
|
||||
const giteaUrl = u.gitea_path || '';
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'helpdesk-domain-handover-item';
|
||||
|
||||
const main = document.createElement('div');
|
||||
main.className = 'helpdesk-domain-handover-item-main';
|
||||
|
||||
const productSpan = document.createElement('span');
|
||||
productSpan.className = 'helpdesk-domain-handover-item-product';
|
||||
if (ticketUrl) {
|
||||
const link = document.createElement('a');
|
||||
link.href = ticketUrl;
|
||||
link.textContent = ticketNo;
|
||||
productSpan.appendChild(link);
|
||||
} else {
|
||||
productSpan.textContent = ticketNo;
|
||||
}
|
||||
main.appendChild(productSpan);
|
||||
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'badge';
|
||||
badge.dataset.variant = categoryVariant;
|
||||
badge.textContent = categoryLabel;
|
||||
main.appendChild(badge);
|
||||
|
||||
item.appendChild(main);
|
||||
|
||||
const metaParts = [];
|
||||
if (giteaUrl) {
|
||||
// Show last path segment as clickable link
|
||||
const parts = giteaUrl.replace(/\/+$/, '').split('/');
|
||||
const shortLabel = parts[parts.length - 1] || giteaUrl;
|
||||
const giteaLink = document.createElement('a');
|
||||
giteaLink.href = giteaUrl;
|
||||
giteaLink.target = '_blank';
|
||||
giteaLink.rel = 'noopener';
|
||||
giteaLink.textContent = shortLabel;
|
||||
metaParts.push(giteaLink);
|
||||
}
|
||||
const dateStr = formatDate(u.created_at);
|
||||
if (dateStr && dateStr !== '\u2014') {
|
||||
const dateSpan = document.createElement('span');
|
||||
dateSpan.textContent = dateStr;
|
||||
metaParts.push(dateSpan);
|
||||
}
|
||||
if (metaParts.length) {
|
||||
const metaDiv = document.createElement('div');
|
||||
metaDiv.className = 'helpdesk-domain-handover-item-meta';
|
||||
metaParts.forEach((part, i) => {
|
||||
if (i > 0) metaDiv.appendChild(document.createTextNode(' \u00b7 '));
|
||||
metaDiv.appendChild(part);
|
||||
});
|
||||
item.appendChild(metaDiv);
|
||||
}
|
||||
|
||||
listDiv.appendChild(item);
|
||||
}
|
||||
|
||||
el.textContent = '';
|
||||
el.appendChild(listDiv);
|
||||
}
|
||||
|
||||
function renderRelatedDomains(domains) {
|
||||
const section = document.getElementById('domain-related-section');
|
||||
const el = document.getElementById('domain-related-domains');
|
||||
if (!el || !section) return;
|
||||
|
||||
if (!domains || domains.length === 0) {
|
||||
section.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
section.hidden = false;
|
||||
|
||||
const stateVariantMap = {
|
||||
'Aktiv': 'success',
|
||||
'In Vorbereitung': 'warning',
|
||||
};
|
||||
|
||||
let items = '';
|
||||
for (const d of domains) {
|
||||
const url = d.URL || '';
|
||||
const no = d.No || '';
|
||||
const state = d.State || '';
|
||||
const variant = stateVariantMap[state] || 'neutral';
|
||||
const detailLink = domainBaseUrl + encodeURIComponent(no);
|
||||
|
||||
// safe-html: all dynamic values are escaped via esc()
|
||||
items +=
|
||||
'<a href="' + esc(detailLink) + '" class="helpdesk-domain-related-item">' +
|
||||
'<span class="helpdesk-domain-related-item-url">' + esc(url || no) + '</span>' +
|
||||
'<span class="badge small" data-variant="' + esc(variant) + '">' + esc(state) + '</span>' +
|
||||
'</a>';
|
||||
}
|
||||
|
||||
// safe-html: all content built from esc()-escaped values
|
||||
el.innerHTML = items;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user