Feature: Security Level mit Notiz für Domains
Implementiert ein Dialog-basiertes UI zum Bearbeiten des Security Levels mit optionaler Notiz-Funktion für Helpdesk-Domains. Backend: - DomainSecurityLevelRepository: CRUD für security_levels mit note-Spalte - DomainSecurityLevelService: Business-Logik mit Batch-Enrichment - security-level-data() Endpoint: AJAX + Full-Page POST mit CSRF - Migration 010: note TEXT-Spalte hinzugefügt - Bugfix: findByTenantAndDomainNo gibt null bei leerem Array zurück Frontend (Domain-Liste): - Badge in Grid klickbar → öffnet Modal-Dialog - Dialog mit Level-Select + Notiz-Textarea - AJAX-Save mit Live-Badge-Update ohne Reload - Event-Delegation für dynamische Grid-Inhalte Frontend (Domain-Detail): - Notiz-Feld im Security-Level-Formular - PRG-Pattern mit Flash-Messages Core: - app-http.js: DEFAULT_HEADERS auf XMLHttpRequest für CSRF-Kompatibilität i18n: - Keys: Priority, Note, Optional note, Security level updated Tests: - DomainSecurityLevelServiceTest: CRUD + Batch-Enrichment
This commit is contained in:
@@ -1259,5 +1259,9 @@
|
|||||||
"Delete this image?": "Dieses Bild löschen?",
|
"Delete this image?": "Dieses Bild löschen?",
|
||||||
"Current image": "Aktuelles Bild",
|
"Current image": "Aktuelles Bild",
|
||||||
"Open navigation": "Navigation öffnen",
|
"Open navigation": "Navigation öffnen",
|
||||||
"Close navigation": "Navigation schließen"
|
"Close navigation": "Navigation schließen",
|
||||||
|
"Priority": "Priorität",
|
||||||
|
"Note": "Notiz",
|
||||||
|
"Optional note...": "Optionale Notiz...",
|
||||||
|
"Security level updated": "Sicherheitsstufe aktualisiert"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1259,5 +1259,9 @@
|
|||||||
"Delete this image?": "Delete this image?",
|
"Delete this image?": "Delete this image?",
|
||||||
"Current image": "Current image",
|
"Current image": "Current image",
|
||||||
"Open navigation": "Open navigation",
|
"Open navigation": "Open navigation",
|
||||||
"Close navigation": "Close navigation"
|
"Close navigation": "Close navigation",
|
||||||
|
"Priority": "Priority",
|
||||||
|
"Note": "Note",
|
||||||
|
"Optional note...": "Optional note...",
|
||||||
|
"Security level updated": "Security level updated"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -540,5 +540,10 @@
|
|||||||
"Invalid category": "Ungültige Kategorie",
|
"Invalid category": "Ungültige Kategorie",
|
||||||
"Ticket number is required": "Ticket-Nummer ist erforderlich",
|
"Ticket number is required": "Ticket-Nummer ist erforderlich",
|
||||||
"View software updates list": "Software-Updates-Liste anzeigen",
|
"View software updates list": "Software-Updates-Liste anzeigen",
|
||||||
"Assign and manage software updates": "Software-Updates zuweisen und verwalten"
|
"Assign and manage software updates": "Software-Updates zuweisen und verwalten",
|
||||||
|
"Security level": "Sicherheitsstufe",
|
||||||
|
"Low": "Niedrig",
|
||||||
|
"Normal": "Normal",
|
||||||
|
"High": "Hoch",
|
||||||
|
"Critical": "Kritisch"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -540,5 +540,10 @@
|
|||||||
"Invalid category": "Invalid category",
|
"Invalid category": "Invalid category",
|
||||||
"Ticket number is required": "Ticket number is required",
|
"Ticket number is required": "Ticket number is required",
|
||||||
"View software updates list": "View software updates list",
|
"View software updates list": "View software updates list",
|
||||||
"Assign and manage software updates": "Assign and manage software updates"
|
"Assign and manage software updates": "Assign and manage software updates",
|
||||||
|
"Security level": "Security level",
|
||||||
|
"Low": "Low",
|
||||||
|
"Normal": "Normal",
|
||||||
|
"High": "High",
|
||||||
|
"Critical": "Critical"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ use MintyPHP\Module\Helpdesk\Service\SoftwareProductSyncService;
|
|||||||
use MintyPHP\Module\Helpdesk\Service\SystemRecommendationEngine;
|
use MintyPHP\Module\Helpdesk\Service\SystemRecommendationEngine;
|
||||||
use MintyPHP\Module\Helpdesk\Service\TicketCommunicationService;
|
use MintyPHP\Module\Helpdesk\Service\TicketCommunicationService;
|
||||||
use MintyPHP\Module\Helpdesk\Handler\SoftwareProductSyncJobHandler;
|
use MintyPHP\Module\Helpdesk\Handler\SoftwareProductSyncJobHandler;
|
||||||
|
use MintyPHP\Module\Helpdesk\Repository\DomainSecurityLevelRepository;
|
||||||
use MintyPHP\Module\Helpdesk\Repository\HandoverRepository;
|
use MintyPHP\Module\Helpdesk\Repository\HandoverRepository;
|
||||||
use MintyPHP\Module\Helpdesk\Repository\HandoverRevisionRepository;
|
use MintyPHP\Module\Helpdesk\Repository\HandoverRevisionRepository;
|
||||||
use MintyPHP\Module\Helpdesk\Repository\SoftwareProductRepository;
|
use MintyPHP\Module\Helpdesk\Repository\SoftwareProductRepository;
|
||||||
use MintyPHP\Module\Helpdesk\Repository\UpdateRepository;
|
use MintyPHP\Module\Helpdesk\Repository\UpdateRepository;
|
||||||
|
use MintyPHP\Module\Helpdesk\Service\DomainSecurityLevelService;
|
||||||
use MintyPHP\Module\Helpdesk\Service\HandoverRevisionService;
|
use MintyPHP\Module\Helpdesk\Service\HandoverRevisionService;
|
||||||
use MintyPHP\Module\Helpdesk\Service\HandoverService;
|
use MintyPHP\Module\Helpdesk\Service\HandoverService;
|
||||||
use MintyPHP\Module\Helpdesk\Service\UpdateService;
|
use MintyPHP\Module\Helpdesk\Service\UpdateService;
|
||||||
@@ -119,6 +121,12 @@ final class HelpdeskContainerRegistrar implements ContainerRegistrar
|
|||||||
$c->get(SoftwareProductSyncService::class)
|
$c->get(SoftwareProductSyncService::class)
|
||||||
));
|
));
|
||||||
|
|
||||||
|
$container->set(DomainSecurityLevelRepository::class, static fn (): DomainSecurityLevelRepository => new DomainSecurityLevelRepository());
|
||||||
|
|
||||||
|
$container->set(DomainSecurityLevelService::class, static fn (AppContainer $c): DomainSecurityLevelService => new DomainSecurityLevelService(
|
||||||
|
$c->get(DomainSecurityLevelRepository::class)
|
||||||
|
));
|
||||||
|
|
||||||
$container->set(HandoverRepository::class, static fn (): HandoverRepository => new HandoverRepository());
|
$container->set(HandoverRepository::class, static fn (): HandoverRepository => new HandoverRepository());
|
||||||
|
|
||||||
$container->set(HandoverRevisionRepository::class, static fn (): HandoverRevisionRepository => new HandoverRevisionRepository());
|
$container->set(HandoverRevisionRepository::class, static fn (): HandoverRevisionRepository => new HandoverRevisionRepository());
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Module\Helpdesk\Repository;
|
||||||
|
|
||||||
|
use MintyPHP\DB;
|
||||||
|
|
||||||
|
class DomainSecurityLevelRepository
|
||||||
|
{
|
||||||
|
private const ALLOWED_LEVELS = ['niedrig', 'normal', 'hoch', 'kritisch'];
|
||||||
|
|
||||||
|
public function findByTenantAndDomainNo(int $tenantId, string $domainNo): ?array
|
||||||
|
{
|
||||||
|
if ($domainNo === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = DB::selectOne(
|
||||||
|
'SELECT * FROM helpdesk_domain_security_levels WHERE tenant_id = ? AND domain_no = ? LIMIT 1',
|
||||||
|
(string) $tenantId,
|
||||||
|
$domainNo
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!is_array($row) || $row === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $domainNos
|
||||||
|
* @return array<string, string> Map of domain_no => security_level
|
||||||
|
*/
|
||||||
|
public function listLevelsByDomainNos(int $tenantId, array $domainNos): array
|
||||||
|
{
|
||||||
|
if ($domainNos === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$placeholders = implode(',', array_fill(0, count($domainNos), '?'));
|
||||||
|
$params = array_map('strval', $domainNos);
|
||||||
|
$params[] = (string) $tenantId;
|
||||||
|
|
||||||
|
$rows = DB::select(
|
||||||
|
'SELECT domain_no, security_level FROM helpdesk_domain_security_levels WHERE tenant_id = ? AND domain_no IN (' . $placeholders . ')',
|
||||||
|
...$params
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
if (is_array($rows)) {
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$domainNo = (string) ($row['domain_no'] ?? '');
|
||||||
|
$level = (string) ($row['security_level'] ?? 'normal');
|
||||||
|
if ($domainNo !== '') {
|
||||||
|
$result[$domainNo] = in_array($level, self::ALLOWED_LEVELS, true) ? $level : 'normal';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $domainNos
|
||||||
|
* @return array<string, array{level: string, note: string}> Map of domain_no => {level, note}
|
||||||
|
*/
|
||||||
|
public function listDetailsByDomainNos(int $tenantId, array $domainNos): array
|
||||||
|
{
|
||||||
|
if ($domainNos === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$placeholders = implode(',', array_fill(0, count($domainNos), '?'));
|
||||||
|
$params = array_map('strval', $domainNos);
|
||||||
|
$params[] = (string) $tenantId;
|
||||||
|
|
||||||
|
$rows = DB::select(
|
||||||
|
'SELECT domain_no, security_level, note FROM helpdesk_domain_security_levels WHERE tenant_id = ? AND domain_no IN (' . $placeholders . ')',
|
||||||
|
...$params
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
if (is_array($rows)) {
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$domainNo = (string) ($row['domain_no'] ?? '');
|
||||||
|
$level = (string) ($row['security_level'] ?? 'normal');
|
||||||
|
$note = (string) ($row['note'] ?? '');
|
||||||
|
if ($domainNo !== '') {
|
||||||
|
$result[$domainNo] = [
|
||||||
|
'level' => in_array($level, self::ALLOWED_LEVELS, true) ? $level : 'normal',
|
||||||
|
'note' => $note,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert or update security level for a domain.
|
||||||
|
*
|
||||||
|
* @return bool True on success
|
||||||
|
*/
|
||||||
|
public function upsert(int $tenantId, string $domainNo, string $level, ?int $userId, ?string $note = null): bool
|
||||||
|
{
|
||||||
|
if ($domainNo === '' || !in_array($level, self::ALLOWED_LEVELS, true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $this->findByTenantAndDomainNo($tenantId, $domainNo);
|
||||||
|
|
||||||
|
if ($existing !== null) {
|
||||||
|
$result = DB::update(
|
||||||
|
'UPDATE helpdesk_domain_security_levels SET security_level = ?, note = ?, updated_by = ? WHERE tenant_id = ? AND domain_no = ?',
|
||||||
|
$level,
|
||||||
|
$note,
|
||||||
|
$userId !== null ? (string) $userId : null,
|
||||||
|
(string) $tenantId,
|
||||||
|
$domainNo
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = DB::insert(
|
||||||
|
'INSERT INTO helpdesk_domain_security_levels (tenant_id, domain_no, security_level, note, created_by, updated_by) VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
|
(string) $tenantId,
|
||||||
|
$domainNo,
|
||||||
|
$level,
|
||||||
|
$note,
|
||||||
|
$userId !== null ? (string) $userId : null,
|
||||||
|
$userId !== null ? (string) $userId : null
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api Reserved for future cleanup jobs
|
||||||
|
*/
|
||||||
|
public function deleteByTenantAndDomainNo(int $tenantId, string $domainNo): bool
|
||||||
|
{
|
||||||
|
if ($domainNo === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = DB::update(
|
||||||
|
'DELETE FROM helpdesk_domain_security_levels WHERE tenant_id = ? AND domain_no = ?',
|
||||||
|
(string) $tenantId,
|
||||||
|
$domainNo
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Module\Helpdesk\Service;
|
||||||
|
|
||||||
|
use MintyPHP\Module\Helpdesk\Repository\DomainSecurityLevelRepository;
|
||||||
|
|
||||||
|
class DomainSecurityLevelService
|
||||||
|
{
|
||||||
|
private const ALLOWED_LEVELS = ['niedrig', 'normal', 'hoch', 'kritisch'];
|
||||||
|
|
||||||
|
private const BADGE_VARIANTS = [
|
||||||
|
'niedrig' => 'info',
|
||||||
|
'normal' => 'neutral',
|
||||||
|
'hoch' => 'warning',
|
||||||
|
'kritisch' => 'error',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly DomainSecurityLevelRepository $repository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api Called from page actions and AJAX endpoints
|
||||||
|
*/
|
||||||
|
public function getLevel(int $tenantId, string $domainNo): string
|
||||||
|
{
|
||||||
|
if ($domainNo === '') {
|
||||||
|
return 'normal';
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $this->repository->findByTenantAndDomainNo($tenantId, $domainNo);
|
||||||
|
|
||||||
|
if ($row === null) {
|
||||||
|
return 'normal';
|
||||||
|
}
|
||||||
|
|
||||||
|
$level = (string) ($row['security_level'] ?? 'normal');
|
||||||
|
|
||||||
|
return in_array($level, self::ALLOWED_LEVELS, true) ? $level : 'normal';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api Called from AJAX endpoints — returns level + note
|
||||||
|
* @return array{level: string, note: string}
|
||||||
|
*/
|
||||||
|
public function getDetails(int $tenantId, string $domainNo): array
|
||||||
|
{
|
||||||
|
if ($domainNo === '') {
|
||||||
|
return ['level' => 'normal', 'note' => ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $this->repository->findByTenantAndDomainNo($tenantId, $domainNo);
|
||||||
|
|
||||||
|
if ($row === null) {
|
||||||
|
return ['level' => 'normal', 'note' => ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
$level = (string) ($row['security_level'] ?? 'normal');
|
||||||
|
$note = (string) ($row['note'] ?? '');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'level' => in_array($level, self::ALLOWED_LEVELS, true) ? $level : 'normal',
|
||||||
|
'note' => $note,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api Called from page actions and AJAX endpoints
|
||||||
|
*/
|
||||||
|
public function setLevel(int $tenantId, string $domainNo, string $level, ?int $userId, ?string $note = null): bool
|
||||||
|
{
|
||||||
|
if ($domainNo === '' || !in_array($level, self::ALLOWED_LEVELS, true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->repository->upsert($tenantId, $domainNo, $level, $userId, $note);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api Called from domains-data endpoint for batch enrichment
|
||||||
|
*/
|
||||||
|
public function getAllLevelsForDomains(int $tenantId, array $domainNos): array
|
||||||
|
{
|
||||||
|
if ($domainNos === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->repository->listLevelsByDomainNos($tenantId, $domainNos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api Called from domains-data endpoint for batch enrichment with note
|
||||||
|
* @param list<string> $domainNos
|
||||||
|
* @return array<string, array{level: string, note: string}>
|
||||||
|
*/
|
||||||
|
public function getAllDetailsForDomains(int $tenantId, array $domainNos): array
|
||||||
|
{
|
||||||
|
if ($domainNos === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->repository->listDetailsByDomainNos($tenantId, $domainNos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api Called from views and AJAX responses for badge rendering
|
||||||
|
*/
|
||||||
|
public static function getBadgeVariant(string $level): string
|
||||||
|
{
|
||||||
|
return self::BADGE_VARIANTS[$level] ?? 'neutral';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @api Called from AJAX endpoint for validation
|
||||||
|
*/
|
||||||
|
public static function getAllowedLevels(): array
|
||||||
|
{
|
||||||
|
return self::ALLOWED_LEVELS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- Add domain security levels table
|
||||||
|
-- Stores per-tenant security classification for domains (BC OData is read-only)
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `helpdesk_domain_security_levels` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`tenant_id` INT UNSIGNED NOT NULL,
|
||||||
|
`domain_no` VARCHAR(30) NOT NULL,
|
||||||
|
`security_level` VARCHAR(20) NOT NULL DEFAULT 'normal',
|
||||||
|
`created_by` INT UNSIGNED NULL,
|
||||||
|
`updated_by` INT UNSIGNED NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_tenant_domain` (`tenant_id`, `domain_no`),
|
||||||
|
KEY `idx_tenant_level` (`tenant_id`, `security_level`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- Add optional note column to domain security levels
|
||||||
|
-- Allows users to add a free-text note when setting a security level
|
||||||
|
|
||||||
|
ALTER TABLE `helpdesk_domain_security_levels`
|
||||||
|
ADD COLUMN `note` TEXT NULL DEFAULT NULL AFTER `security_level`;
|
||||||
@@ -17,6 +17,7 @@ return [
|
|||||||
['path' => 'helpdesk/search-data', 'target' => 'helpdesk/search-data'],
|
['path' => 'helpdesk/search-data', 'target' => 'helpdesk/search-data'],
|
||||||
['path' => 'helpdesk/domains', 'target' => 'helpdesk/domains'],
|
['path' => 'helpdesk/domains', 'target' => 'helpdesk/domains'],
|
||||||
['path' => 'helpdesk/domains-data', 'target' => 'helpdesk/domains-data'],
|
['path' => 'helpdesk/domains-data', 'target' => 'helpdesk/domains-data'],
|
||||||
|
['path' => 'helpdesk/domains/security-level-data', 'target' => 'helpdesk/domains/security-level-data'],
|
||||||
['path' => 'helpdesk/domain/{id}', 'target' => 'helpdesk/domain'],
|
['path' => 'helpdesk/domain/{id}', 'target' => 'helpdesk/domain'],
|
||||||
['path' => 'helpdesk/domain-detail-data', 'target' => 'helpdesk/domain-detail-data'],
|
['path' => 'helpdesk/domain-detail-data', 'target' => 'helpdesk/domain-detail-data'],
|
||||||
['path' => 'helpdesk/debitor/{id}', 'target' => 'helpdesk/debitor'],
|
['path' => 'helpdesk/debitor/{id}', 'target' => 'helpdesk/debitor'],
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use MintyPHP\Buffer;
|
use MintyPHP\Buffer;
|
||||||
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||||
use MintyPHP\Module\Helpdesk\Service\DomainDetailService;
|
use MintyPHP\Module\Helpdesk\Service\DomainDetailService;
|
||||||
|
use MintyPHP\Module\Helpdesk\Service\DomainSecurityLevelService;
|
||||||
use MintyPHP\Router;
|
use MintyPHP\Router;
|
||||||
use MintyPHP\Support\Flash;
|
use MintyPHP\Support\Flash;
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
@@ -47,6 +49,14 @@ $customerName = (string) ($domain['Customer_Name'] ?? '');
|
|||||||
$domainState = (string) ($domain['State'] ?? '');
|
$domainState = (string) ($domain['State'] ?? '');
|
||||||
$domainAdministration = (string) ($domain['Administration'] ?? '');
|
$domainAdministration = (string) ($domain['Administration'] ?? '');
|
||||||
|
|
||||||
|
$session = app(SessionStoreInterface::class)->all();
|
||||||
|
$tenantId = (int) ($session['current_tenant']['id'] ?? 0);
|
||||||
|
$securityLevelService = app(DomainSecurityLevelService::class);
|
||||||
|
$securityDetails = $tenantId > 0 ? $securityLevelService->getDetails($tenantId, $domainNo) : ['level' => 'normal', 'note' => ''];
|
||||||
|
$securityLevel = $securityDetails['level'];
|
||||||
|
$securityLevelNote = $securityDetails['note'];
|
||||||
|
$securityLevelVariant = DomainSecurityLevelService::getBadgeVariant($securityLevel);
|
||||||
|
|
||||||
$pageTitle = $domainUrl !== '' ? $domainUrl : $domainNo;
|
$pageTitle = $domainUrl !== '' ? $domainUrl : $domainNo;
|
||||||
|
|
||||||
Buffer::set('title', $pageTitle . ' — ' . t('Helpdesk'));
|
Buffer::set('title', $pageTitle . ' — ' . t('Helpdesk'));
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use MintyPHP\Session;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string $domainNo
|
* @var string $domainNo
|
||||||
* @var array $domain
|
* @var array $domain
|
||||||
@@ -11,6 +13,9 @@
|
|||||||
* @var string $domainState
|
* @var string $domainState
|
||||||
* @var string $domainAdministration
|
* @var string $domainAdministration
|
||||||
* @var string $pageTitle
|
* @var string $pageTitle
|
||||||
|
* @var string $securityLevel
|
||||||
|
* @var string $securityLevelVariant
|
||||||
|
* @var string $securityLevelNote
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$domainNo = $domainNo ?? '';
|
$domainNo = $domainNo ?? '';
|
||||||
@@ -23,6 +28,9 @@ $customerName = $customerName ?? '';
|
|||||||
$domainState = $domainState ?? '';
|
$domainState = $domainState ?? '';
|
||||||
$domainAdministration = $domainAdministration ?? '';
|
$domainAdministration = $domainAdministration ?? '';
|
||||||
$pageTitle = $pageTitle ?? $domainNo;
|
$pageTitle = $pageTitle ?? $domainNo;
|
||||||
|
$securityLevel = $securityLevel ?? 'normal';
|
||||||
|
$securityLevelVariant = $securityLevelVariant ?? 'neutral';
|
||||||
|
$securityLevelNote = $securityLevelNote ?? '';
|
||||||
|
|
||||||
$stateVariantMap = [
|
$stateVariantMap = [
|
||||||
'Aktiv' => 'success',
|
'Aktiv' => 'success',
|
||||||
@@ -113,6 +121,39 @@ $stateVariant = $stateVariantMap[$domainState] ?? 'neutral';
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<!-- Security Level -->
|
||||||
|
<details class="app-details-card" open>
|
||||||
|
<summary>
|
||||||
|
<span class="app-details-card-summary-title"><?php e(t('Security level')); ?></span>
|
||||||
|
</summary>
|
||||||
|
<div class="app-details-card-container">
|
||||||
|
<div id="domain-security-level-form">
|
||||||
|
<form method="POST" action="<?php e(lurl('helpdesk/domains/security-level-data')); ?>" data-security-level-form>
|
||||||
|
<input type="hidden" name="domain_no" value="<?php e($domainNo); ?>">
|
||||||
|
<?php Session::getCsrfInput(); ?>
|
||||||
|
<div class="app-form-grid">
|
||||||
|
<div class="app-form-group">
|
||||||
|
<label for="domain-security-level-select"><?php e(t('Security level')); ?></label>
|
||||||
|
<select name="security_level" id="domain-security-level-select" data-security-level-select>
|
||||||
|
<option value="niedrig" <?php e($securityLevel === 'niedrig' ? 'selected' : ''); ?>><?php e(t('Low')); ?></option>
|
||||||
|
<option value="normal" <?php e($securityLevel === 'normal' ? 'selected' : ''); ?>><?php e(t('Normal')); ?></option>
|
||||||
|
<option value="hoch" <?php e($securityLevel === 'hoch' ? 'selected' : ''); ?>><?php e(t('High')); ?></option>
|
||||||
|
<option value="kritisch" <?php e($securityLevel === 'kritisch' ? 'selected' : ''); ?>><?php e(t('Critical')); ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="app-form-group">
|
||||||
|
<label for="domain-security-level-note"><?php e(t('Note')); ?></label>
|
||||||
|
<textarea name="note" id="domain-security-level-note" rows="3" placeholder="<?php e(t('Optional note...')); ?>"><?php e($securityLevelNote); ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="app-form-actions">
|
||||||
|
<button type="submit" class="secondary outline small"><?php e(t('Save')); ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Handovers (full width) -->
|
<!-- Handovers (full width) -->
|
||||||
@@ -166,6 +207,7 @@ $stateVariant = $stateVariantMap[$domainState] ?? 'neutral';
|
|||||||
<?php if ($domainAdministration !== ''): ?>
|
<?php if ($domainAdministration !== ''): ?>
|
||||||
<span class="badge" data-variant="neutral"><?php e($domainAdministration); ?></span>
|
<span class="badge" data-variant="neutral"><?php e($domainAdministration); ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<span class="badge" data-variant="<?php e($securityLevelVariant); ?>" id="domain-security-badge"><?php e(t(ucfirst($securityLevel))); ?></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use MintyPHP\Http\SessionStoreInterface;
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||||
use MintyPHP\Module\Helpdesk\Service\DomainDetailService;
|
use MintyPHP\Module\Helpdesk\Service\DomainDetailService;
|
||||||
|
use MintyPHP\Module\Helpdesk\Service\DomainSecurityLevelService;
|
||||||
use MintyPHP\Router;
|
use MintyPHP\Router;
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
@@ -26,4 +27,10 @@ $tenantId = (int) ($session['current_tenant']['id'] ?? 0);
|
|||||||
$service = app(DomainDetailService::class);
|
$service = app(DomainDetailService::class);
|
||||||
$result = $service->loadDomainDetails($domainNo, $customerNo, $tenantId);
|
$result = $service->loadDomainDetails($domainNo, $customerNo, $tenantId);
|
||||||
|
|
||||||
|
$securityLevelService = app(DomainSecurityLevelService::class);
|
||||||
|
$securityDetails = $tenantId > 0 ? $securityLevelService->getDetails($tenantId, $domainNo) : ['level' => 'normal', 'note' => ''];
|
||||||
|
$result['security_level'] = $securityDetails['level'];
|
||||||
|
$result['security_level_variant'] = DomainSecurityLevelService::getBadgeVariant($securityDetails['level']);
|
||||||
|
$result['security_level_note'] = $securityDetails['note'];
|
||||||
|
|
||||||
Router::json($result);
|
Router::json($result);
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||||
use MintyPHP\Module\Helpdesk\Service\BcODataGateway;
|
use MintyPHP\Module\Helpdesk\Service\BcODataGateway;
|
||||||
|
use MintyPHP\Module\Helpdesk\Service\DomainSecurityLevelService;
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
Guard::requireLogin();
|
Guard::requireLogin();
|
||||||
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
||||||
gridRequireGetRequest();
|
gridRequireGetRequest();
|
||||||
|
|
||||||
|
$session = app(SessionStoreInterface::class)->all();
|
||||||
|
$tenantId = (int) ($session['current_tenant']['id'] ?? 0);
|
||||||
|
|
||||||
$filters = gridParseFiltersFromSchemaFile(__DIR__ . '/domains/filter-schema.php');
|
$filters = gridParseFiltersFromSchemaFile(__DIR__ . '/domains/filter-schema.php');
|
||||||
|
|
||||||
$search = trim((string) ($filters['search'] ?? ''));
|
$search = trim((string) ($filters['search'] ?? ''));
|
||||||
@@ -17,6 +22,8 @@ $state = trim((string) ($filters['state'] ?? ''));
|
|||||||
$state = $state === 'all' ? '' : $state;
|
$state = $state === 'all' ? '' : $state;
|
||||||
$administration = trim((string) ($filters['administration'] ?? ''));
|
$administration = trim((string) ($filters['administration'] ?? ''));
|
||||||
$administration = $administration === 'all' ? '' : $administration;
|
$administration = $administration === 'all' ? '' : $administration;
|
||||||
|
$securityLevel = trim((string) ($filters['security_level'] ?? ''));
|
||||||
|
$securityLevel = $securityLevel === 'all' ? '' : $securityLevel;
|
||||||
$order = (string) ($filters['order'] ?? 'Customer_Name');
|
$order = (string) ($filters['order'] ?? 'Customer_Name');
|
||||||
$dir = (string) ($filters['dir'] ?? 'asc');
|
$dir = (string) ($filters['dir'] ?? 'asc');
|
||||||
$limit = (int) ($filters['limit'] ?? 10);
|
$limit = (int) ($filters['limit'] ?? 10);
|
||||||
@@ -113,6 +120,20 @@ if ($administration !== '') {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Security level filter
|
||||||
|
if ($securityLevel !== '' && $tenantId > 0) {
|
||||||
|
$securityLevelService = app(DomainSecurityLevelService::class);
|
||||||
|
$allDomainNos = array_map(static fn (array $d): string => trim((string) ($d['No'] ?? '')), $filtered);
|
||||||
|
$allDomainNos = array_values(array_filter($allDomainNos, static fn (string $n): bool => $n !== ''));
|
||||||
|
$filterLookup = $allDomainNos !== [] ? $securityLevelService->getAllLevelsForDomains($tenantId, $allDomainNos) : [];
|
||||||
|
$filtered = array_values(array_filter($filtered, static function (array $domain) use ($securityLevel, $filterLookup): bool {
|
||||||
|
$domainNo = trim((string) ($domain['No'] ?? ''));
|
||||||
|
$currentLevel = $filterLookup[$domainNo] ?? 'normal';
|
||||||
|
|
||||||
|
return $currentLevel === $securityLevel;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
$total = count($filtered);
|
$total = count($filtered);
|
||||||
|
|
||||||
@@ -127,6 +148,17 @@ usort($filtered, static function (array $a, array $b) use ($order, $dir): int {
|
|||||||
// Pagination
|
// Pagination
|
||||||
$rows = array_slice($filtered, $offset, $limit);
|
$rows = array_slice($filtered, $offset, $limit);
|
||||||
|
|
||||||
|
// Security level enrichment for displayed rows
|
||||||
|
$securityLevelLookup = [];
|
||||||
|
if ($tenantId > 0) {
|
||||||
|
$securityLevelService = app(DomainSecurityLevelService::class);
|
||||||
|
$pagedDomainNos = array_map(static fn (array $d): string => trim((string) ($d['No'] ?? '')), $rows);
|
||||||
|
$pagedDomainNos = array_values(array_filter($pagedDomainNos, static fn (string $n): bool => $n !== ''));
|
||||||
|
if ($pagedDomainNos !== []) {
|
||||||
|
$securityLevelLookup = $securityLevelService->getAllDetailsForDomains($tenantId, $pagedDomainNos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Row preparation
|
// Row preparation
|
||||||
$stateVariantMap = [
|
$stateVariantMap = [
|
||||||
'Aktiv' => 'success',
|
'Aktiv' => 'success',
|
||||||
@@ -139,9 +171,13 @@ $preparedRows = [];
|
|||||||
foreach ($rows as $domain) {
|
foreach ($rows as $domain) {
|
||||||
$customerNo = trim((string) ($domain['Customer_No'] ?? ''));
|
$customerNo = trim((string) ($domain['Customer_No'] ?? ''));
|
||||||
$domainState = (string) ($domain['State'] ?? '');
|
$domainState = (string) ($domain['State'] ?? '');
|
||||||
|
$domainNo = (string) ($domain['No'] ?? '');
|
||||||
|
$details = $securityLevelLookup[$domainNo] ?? null;
|
||||||
|
$level = $details['level'] ?? 'normal';
|
||||||
|
$note = $details['note'] ?? '';
|
||||||
|
|
||||||
$preparedRows[] = [
|
$preparedRows[] = [
|
||||||
'No' => (string) ($domain['No'] ?? ''),
|
'No' => $domainNo,
|
||||||
'Customer_No' => $customerNo,
|
'Customer_No' => $customerNo,
|
||||||
'Customer_Name' => (string) ($domain['Customer_Name'] ?? ''),
|
'Customer_Name' => (string) ($domain['Customer_Name'] ?? ''),
|
||||||
'URL' => (string) ($domain['URL'] ?? ''),
|
'URL' => (string) ($domain['URL'] ?? ''),
|
||||||
@@ -151,6 +187,9 @@ foreach ($rows as $domain) {
|
|||||||
'contract_type' => (string) ($domain['contract_type'] ?? ''),
|
'contract_type' => (string) ($domain['contract_type'] ?? ''),
|
||||||
'contract_no' => (string) ($domain['contract_no'] ?? ''),
|
'contract_no' => (string) ($domain['contract_no'] ?? ''),
|
||||||
'debitor_url' => $customerNo !== '' ? $debitorBaseUrl . rawurlencode($customerNo) : '',
|
'debitor_url' => $customerNo !== '' ? $debitorBaseUrl . rawurlencode($customerNo) : '',
|
||||||
|
'security_level' => $level,
|
||||||
|
'security_level_variant' => DomainSecurityLevelService::getBadgeVariant($level),
|
||||||
|
'security_level_note' => $note,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,5 +72,21 @@ return gridFilterSchema([
|
|||||||
],
|
],
|
||||||
'label_attributes' => ['data-filter-optional' => true],
|
'label_attributes' => ['data-filter-optional' => true],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'key' => 'security_level',
|
||||||
|
'type' => 'select',
|
||||||
|
'label' => 'Security level',
|
||||||
|
'input_id' => 'helpdesk-domains-security-level-filter',
|
||||||
|
'default' => 'all',
|
||||||
|
'normalize' => 'all_to_empty',
|
||||||
|
'allowed' => [
|
||||||
|
['id' => 'all', 'description' => 'All'],
|
||||||
|
['id' => 'niedrig', 'description' => 'Low'],
|
||||||
|
['id' => 'normal', 'description' => 'Normal'],
|
||||||
|
['id' => 'hoch', 'description' => 'High'],
|
||||||
|
['id' => 'kritisch', 'description' => 'Critical'],
|
||||||
|
],
|
||||||
|
'label_attributes' => ['data-filter-optional' => true],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ $filterChipMeta = [
|
|||||||
'default' => (string) ($schemaByKey['administration']['default'] ?? 'all'),
|
'default' => (string) ($schemaByKey['administration']['default'] ?? 'all'),
|
||||||
'options' => gridOptionMapFromAllowed((array) ($schemaByKey['administration'] ?? [])),
|
'options' => gridOptionMapFromAllowed((array) ($schemaByKey['administration'] ?? [])),
|
||||||
],
|
],
|
||||||
|
'security_level' => [
|
||||||
|
'label' => t('Security level'),
|
||||||
|
'type' => 'select',
|
||||||
|
'default' => (string) ($schemaByKey['security_level']['default'] ?? 'all'),
|
||||||
|
'options' => gridOptionMapFromAllowed((array) ($schemaByKey['security_level'] ?? [])),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$settingsGateway = app(HelpdeskSettingsGateway::class);
|
$settingsGateway = app(HelpdeskSettingsGateway::class);
|
||||||
|
|||||||
@@ -32,10 +32,46 @@ require templatePath('partials/app-list-filters.phtml');
|
|||||||
<div id="helpdesk-domains-grid"></div>
|
<div id="helpdesk-domains-grid"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
data-app-security-dialog
|
||||||
|
aria-labelledby="sec-dialog-title"
|
||||||
|
aria-describedby="sec-dialog-domain"
|
||||||
|
>
|
||||||
|
<article class="app-confirm-dialog app-dialog-sm">
|
||||||
|
<header>
|
||||||
|
<h2 id="sec-dialog-title"><?php e(t('Security level')); ?></h2>
|
||||||
|
<button type="button" class="close" data-sec-dialog-close aria-label="<?php e(t('Close')); ?>" title="<?php e(t('Close')); ?>"></button>
|
||||||
|
</header>
|
||||||
|
<p id="sec-dialog-domain" class="muted"></p>
|
||||||
|
<div class="app-form-grid">
|
||||||
|
<?php $csrfKey = \MintyPHP\Session::$csrfSessionKey; $csrfToken = (string) ($_SESSION[$csrfKey] ?? ''); ?>
|
||||||
|
<input type="hidden" name="<?php e($csrfKey); ?>" id="sec-dialog-csrf" value="<?php e($csrfToken); ?>">
|
||||||
|
<div class="app-form-group">
|
||||||
|
<label for="sec-dialog-level"><?php e(t('Security level')); ?></label>
|
||||||
|
<select id="sec-dialog-level" name="security_level">
|
||||||
|
<option value="niedrig"><?php e(t('Low')); ?></option>
|
||||||
|
<option value="normal"><?php e(t('Normal')); ?></option>
|
||||||
|
<option value="hoch"><?php e(t('High')); ?></option>
|
||||||
|
<option value="kritisch"><?php e(t('Critical')); ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="app-form-group">
|
||||||
|
<label for="sec-dialog-note"><?php e(t('Note')); ?></label>
|
||||||
|
<textarea id="sec-dialog-note" name="note" rows="3" placeholder="<?php e(t('Optional note...')); ?>"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<button type="button" class="secondary outline" data-sec-dialog-cancel><?php e(t('Cancel')); ?></button>
|
||||||
|
<button type="button" class="primary" data-sec-dialog-save><?php e(t('Save')); ?></button>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<script src="<?php e(assetVersion('vendor/gridjs/gridjs.umd.js')); ?>"></script>
|
<script src="<?php e(assetVersion('vendor/gridjs/gridjs.umd.js')); ?>"></script>
|
||||||
<script type="application/json" id="page-config-helpdesk-domains"><?php gridJsonForJs([
|
<script type="application/json" id="page-config-helpdesk-domains"><?php gridJsonForJs([
|
||||||
'dataUrl' => lurl('helpdesk/domains-data'),
|
'dataUrl' => lurl('helpdesk/domains-data'),
|
||||||
'domainBaseUrl' => lurl('helpdesk/domain/'),
|
'domainBaseUrl' => lurl('helpdesk/domain/'),
|
||||||
|
'securityLevelDataUrl' => lurl('helpdesk/domains/security-level-data'),
|
||||||
'gridLang' => gridLang(),
|
'gridLang' => gridLang(),
|
||||||
'gridSearch' => $searchConfig,
|
'gridSearch' => $searchConfig,
|
||||||
'filterSchema' => $clientFilterSchema,
|
'filterSchema' => $clientFilterSchema,
|
||||||
@@ -47,6 +83,7 @@ require templatePath('partials/app-list-filters.phtml');
|
|||||||
'state' => t('State'),
|
'state' => t('State'),
|
||||||
'administration' => t('Administration'),
|
'administration' => t('Administration'),
|
||||||
'contractType' => t('Contract type'),
|
'contractType' => t('Contract type'),
|
||||||
|
'securityLevel' => t('Security level'),
|
||||||
],
|
],
|
||||||
]); ?></script>
|
]); ?></script>
|
||||||
<script type="module" src="<?php e(assetVersion('modules/helpdesk/js/pages/helpdesk-domains-index.js')); ?>"></script>
|
<script type="module" src="<?php e(assetVersion('modules/helpdesk/js/pages/helpdesk-domains-index.js')); ?>"></script>
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
|
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||||
|
use MintyPHP\Module\Helpdesk\Service\DomainSecurityLevelService;
|
||||||
|
use MintyPHP\Router;
|
||||||
|
use MintyPHP\Session;
|
||||||
|
use MintyPHP\Support\Flash;
|
||||||
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
|
Guard::requireLogin();
|
||||||
|
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
||||||
|
|
||||||
|
$request = requestInput();
|
||||||
|
$session = app(SessionStoreInterface::class)->all();
|
||||||
|
$tenantId = (int) ($session['current_tenant']['id'] ?? 0);
|
||||||
|
$userId = (int) ($session['user']['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($tenantId <= 0) {
|
||||||
|
Router::json(['ok' => false, 'error' => 'No tenant context']);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$domainNo = trim((string) ($request->body('domain_no', '') ?: $request->query('domain_no', '')));
|
||||||
|
|
||||||
|
if ($domainNo === '') {
|
||||||
|
Router::json(['ok' => false, 'error' => 'Missing domain number']);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$service = app(DomainSecurityLevelService::class);
|
||||||
|
|
||||||
|
if ($request->method() === 'POST') {
|
||||||
|
// CSRF check: Compare token from request body with session token
|
||||||
|
$csrfKey = Session::$csrfSessionKey;
|
||||||
|
$csrfSession = (string) ($session[$csrfKey] ?? '');
|
||||||
|
$csrfRequest = (string) $request->body($csrfKey, '');
|
||||||
|
|
||||||
|
if ($csrfRequest === '' || $csrfRequest !== $csrfSession) {
|
||||||
|
// For AJAX requests, return JSON error
|
||||||
|
if ($request->wantsJson()) {
|
||||||
|
Router::json(['ok' => false, 'error' => 'Invalid CSRF token']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// For full-page POST, redirect with flash error
|
||||||
|
Flash::error(t('Invalid CSRF token'), 'helpdesk-domains', 'csrf');
|
||||||
|
Router::redirect('helpdesk/domain/' . rawurlencode($domainNo));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$level = trim((string) $request->body('security_level', ''));
|
||||||
|
|
||||||
|
if ($level === '') {
|
||||||
|
Router::json(['ok' => false, 'error' => 'Missing security level']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed = DomainSecurityLevelService::getAllowedLevels();
|
||||||
|
if (!in_array($level, $allowed, true)) {
|
||||||
|
Router::json(['ok' => false, 'error' => 'Invalid security level']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$note = trim((string) $request->body('note', ''));
|
||||||
|
|
||||||
|
$success = $service->setLevel($tenantId, $domainNo, $level, $userId, $note !== '' ? $note : null);
|
||||||
|
|
||||||
|
if (!$success) {
|
||||||
|
Router::json(['ok' => false, 'error' => 'Failed to save security level']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For AJAX requests, return JSON
|
||||||
|
if ($request->wantsJson()) {
|
||||||
|
Router::json([
|
||||||
|
'ok' => true,
|
||||||
|
'level' => $level,
|
||||||
|
'badge_variant' => DomainSecurityLevelService::getBadgeVariant($level),
|
||||||
|
'note' => $note,
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For full-page POST, redirect with flash success
|
||||||
|
Flash::success(t('Security level updated'), 'helpdesk-domains', 'security_level_saved');
|
||||||
|
Router::redirect('helpdesk/domain/' . rawurlencode($domainNo));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$details = $service->getDetails($tenantId, $domainNo);
|
||||||
|
|
||||||
|
Router::json([
|
||||||
|
'ok' => true,
|
||||||
|
'level' => $details['level'],
|
||||||
|
'badge_variant' => DomainSecurityLevelService::getBadgeVariant($details['level']),
|
||||||
|
'note' => $details['note'],
|
||||||
|
]);
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Tests\Module\Helpdesk\Service;
|
||||||
|
|
||||||
|
use MintyPHP\Module\Helpdesk\Repository\DomainSecurityLevelRepository;
|
||||||
|
use MintyPHP\Module\Helpdesk\Service\DomainSecurityLevelService;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class DomainSecurityLevelServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private const TENANT_ID = 1;
|
||||||
|
private const TENANT_ID_OTHER = 2;
|
||||||
|
|
||||||
|
public function testGetLevelReturnsDefaultForUnknownDomain(): void
|
||||||
|
{
|
||||||
|
$repository = $this->createMock(DomainSecurityLevelRepository::class);
|
||||||
|
$repository->method('findByTenantAndDomainNo')->willReturn(null);
|
||||||
|
|
||||||
|
$service = new DomainSecurityLevelService($repository);
|
||||||
|
|
||||||
|
$this->assertSame('normal', $service->getLevel(self::TENANT_ID, 'DNS00001'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetLevelReturnsStoredValue(): void
|
||||||
|
{
|
||||||
|
$repository = $this->createMock(DomainSecurityLevelRepository::class);
|
||||||
|
$repository->method('findByTenantAndDomainNo')->willReturn([
|
||||||
|
'security_level' => 'kritisch',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new DomainSecurityLevelService($repository);
|
||||||
|
|
||||||
|
$this->assertSame('kritisch', $service->getLevel(self::TENANT_ID, 'DNS00001'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetLevelDefaultsOnInvalidStoredValue(): void
|
||||||
|
{
|
||||||
|
$repository = $this->createMock(DomainSecurityLevelRepository::class);
|
||||||
|
$repository->method('findByTenantAndDomainNo')->willReturn([
|
||||||
|
'security_level' => 'ungueltig',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new DomainSecurityLevelService($repository);
|
||||||
|
|
||||||
|
$this->assertSame('normal', $service->getLevel(self::TENANT_ID, 'DNS00001'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetLevelReturnsDefaultForEmptyDomainNo(): void
|
||||||
|
{
|
||||||
|
$repository = $this->createMock(DomainSecurityLevelRepository::class);
|
||||||
|
$repository->expects($this->never())->method('findByTenantAndDomainNo');
|
||||||
|
|
||||||
|
$service = new DomainSecurityLevelService($repository);
|
||||||
|
|
||||||
|
$this->assertSame('normal', $service->getLevel(self::TENANT_ID, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetLevelRejectsInvalidLevel(): void
|
||||||
|
{
|
||||||
|
$repository = $this->createMock(DomainSecurityLevelRepository::class);
|
||||||
|
$repository->expects($this->never())->method('upsert');
|
||||||
|
|
||||||
|
$service = new DomainSecurityLevelService($repository);
|
||||||
|
|
||||||
|
$this->assertFalse($service->setLevel(self::TENANT_ID, 'DNS00001', 'ungueltig', 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetLevelRejectsEmptyDomainNo(): void
|
||||||
|
{
|
||||||
|
$repository = $this->createMock(DomainSecurityLevelRepository::class);
|
||||||
|
$repository->expects($this->never())->method('upsert');
|
||||||
|
|
||||||
|
$service = new DomainSecurityLevelService($repository);
|
||||||
|
|
||||||
|
$this->assertFalse($service->setLevel(self::TENANT_ID, '', 'hoch', 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetLevelCallsUpsertWithCorrectParams(): void
|
||||||
|
{
|
||||||
|
$repository = $this->createMock(DomainSecurityLevelRepository::class);
|
||||||
|
$repository->expects($this->once())
|
||||||
|
->method('upsert')
|
||||||
|
->with(self::TENANT_ID, 'DNS00001', 'hoch', 42)
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$service = new DomainSecurityLevelService($repository);
|
||||||
|
|
||||||
|
$this->assertTrue($service->setLevel(self::TENANT_ID, 'DNS00001', 'hoch', 42));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetLevelAllowsAllValidLevels(): void
|
||||||
|
{
|
||||||
|
$repository = $this->createMock(DomainSecurityLevelRepository::class);
|
||||||
|
$repository->method('upsert')->willReturn(true);
|
||||||
|
|
||||||
|
$service = new DomainSecurityLevelService($repository);
|
||||||
|
|
||||||
|
foreach (['niedrig', 'normal', 'hoch', 'kritisch'] as $level) {
|
||||||
|
$this->assertTrue($service->setLevel(self::TENANT_ID, 'DNS00001', $level, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAllLevelsForDomainsReturnsEmptyForEmptyInput(): void
|
||||||
|
{
|
||||||
|
$repository = $this->createMock(DomainSecurityLevelRepository::class);
|
||||||
|
$repository->expects($this->never())->method('listLevelsByDomainNos');
|
||||||
|
|
||||||
|
$service = new DomainSecurityLevelService($repository);
|
||||||
|
|
||||||
|
$this->assertSame([], $service->getAllLevelsForDomains(self::TENANT_ID, []));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAllLevelsForDomainsDelegatesToRepository(): void
|
||||||
|
{
|
||||||
|
$expected = ['DNS00001' => 'hoch', 'DNS00002' => 'niedrig'];
|
||||||
|
$repository = $this->createMock(DomainSecurityLevelRepository::class);
|
||||||
|
$repository->method('listLevelsByDomainNos')->willReturn($expected);
|
||||||
|
|
||||||
|
$service = new DomainSecurityLevelService($repository);
|
||||||
|
$result = $service->getAllLevelsForDomains(self::TENANT_ID, ['DNS00001', 'DNS00002']);
|
||||||
|
|
||||||
|
$this->assertSame($expected, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetBadgeVariantMapping(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('info', DomainSecurityLevelService::getBadgeVariant('niedrig'));
|
||||||
|
$this->assertSame('neutral', DomainSecurityLevelService::getBadgeVariant('normal'));
|
||||||
|
$this->assertSame('warning', DomainSecurityLevelService::getBadgeVariant('hoch'));
|
||||||
|
$this->assertSame('error', DomainSecurityLevelService::getBadgeVariant('kritisch'));
|
||||||
|
$this->assertSame('neutral', DomainSecurityLevelService::getBadgeVariant('unknown'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAllowedLevelsReturnsFourLevels(): void
|
||||||
|
{
|
||||||
|
$levels = DomainSecurityLevelService::getAllowedLevels();
|
||||||
|
|
||||||
|
$this->assertCount(4, $levels);
|
||||||
|
$this->assertSame(['niedrig', 'normal', 'hoch', 'kritisch'], $levels);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3109,4 +3109,18 @@
|
|||||||
font-size: var(--text-xs);
|
font-size: var(--text-xs);
|
||||||
color: var(--app-muted, #6c757d);
|
color: var(--app-muted, #6c757d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td.gridjs-td .badge[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: filter 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.gridjs-td .badge[role="button"]:hover {
|
||||||
|
filter: brightness(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
td.gridjs-td .badge[role="button"]:focus {
|
||||||
|
outline: 2px solid var(--app-primary, #0d6efd);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* into innerHTML. This matches the pattern used in helpdesk-detail.js
|
* into innerHTML. This matches the pattern used in helpdesk-detail.js
|
||||||
* and other helpdesk JS modules in this codebase.
|
* and other helpdesk JS modules in this codebase.
|
||||||
*/
|
*/
|
||||||
import { getJson } from '/js/core/app-http.js';
|
import { getJson, postForm } from '/js/core/app-http.js';
|
||||||
import { resolveHost } from '/js/core/app-dom.js';
|
import { resolveHost } from '/js/core/app-dom.js';
|
||||||
import { renderEmptyState } from './helpdesk-empty-state.js';
|
import { renderEmptyState } from './helpdesk-empty-state.js';
|
||||||
|
|
||||||
@@ -54,6 +54,8 @@ function init(container) {
|
|||||||
|
|
||||||
if (!domainNo || !detailUrl) return;
|
if (!domainNo || !detailUrl) return;
|
||||||
|
|
||||||
|
bindSecurityLevelForm();
|
||||||
|
|
||||||
const i18nEl = document.getElementById('helpdesk-domain-i18n');
|
const i18nEl = document.getElementById('helpdesk-domain-i18n');
|
||||||
const i18n = i18nEl ? JSON.parse(i18nEl.textContent) : {};
|
const i18n = i18nEl ? JSON.parse(i18nEl.textContent) : {};
|
||||||
const t = (key) => i18n[key] || key;
|
const t = (key) => i18n[key] || key;
|
||||||
@@ -77,6 +79,7 @@ function init(container) {
|
|||||||
|
|
||||||
renderCustomerInfo(data.customer);
|
renderCustomerInfo(data.customer);
|
||||||
renderContractInfo(data.contract);
|
renderContractInfo(data.contract);
|
||||||
|
renderSecurityLevel(data.security_level, data.security_level_variant);
|
||||||
renderHandoversList(data.handovers || []);
|
renderHandoversList(data.handovers || []);
|
||||||
renderUpdatesList(data.updates || []);
|
renderUpdatesList(data.updates || []);
|
||||||
renderRelatedDomains(data.related_domains || []);
|
renderRelatedDomains(data.related_domains || []);
|
||||||
@@ -159,6 +162,53 @@ function init(container) {
|
|||||||
'</dl>';
|
'</dl>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSecurityLevel(level, variant) {
|
||||||
|
const badge = document.getElementById('domain-security-badge');
|
||||||
|
const select = document.querySelector('[data-security-level-select]');
|
||||||
|
if (!badge && !select) return;
|
||||||
|
|
||||||
|
const levelLabels = { niedrig: t('Low'), normal: t('Normal'), hoch: t('High'), kritisch: t('Critical') };
|
||||||
|
const label = levelLabels[level] ?? level;
|
||||||
|
|
||||||
|
if (badge) {
|
||||||
|
badge.textContent = label;
|
||||||
|
badge.dataset.variant = variant || 'neutral';
|
||||||
|
}
|
||||||
|
if (select) {
|
||||||
|
select.value = level || 'normal';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindSecurityLevelForm() {
|
||||||
|
const form = document.querySelector('[data-security-level-form]');
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const select = form.querySelector('[data-security-level-select]');
|
||||||
|
const domainNo = form.querySelector('input[name="domain_no"]')?.value || '';
|
||||||
|
const level = select?.value || '';
|
||||||
|
if (!domainNo || !level) return;
|
||||||
|
|
||||||
|
const csrfKey = document.querySelector('[data-csrf-key]')?.dataset.csrfKey || '_csrf';
|
||||||
|
const csrfToken = document.querySelector('[data-csrf-key]')?.dataset.csrfToken || '';
|
||||||
|
const body = new URLSearchParams({
|
||||||
|
domain_no: domainNo,
|
||||||
|
security_level: level,
|
||||||
|
[csrfKey]: csrfToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await postForm('/helpdesk/domains/security-level-data', body);
|
||||||
|
if (resp?.ok) {
|
||||||
|
renderSecurityLevel(resp.level, resp.badge_variant);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// silent fail — user can retry
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderHandoversList(handovers) {
|
function renderHandoversList(handovers) {
|
||||||
const el = document.getElementById('domain-handovers-list');
|
const el = document.getElementById('domain-handovers-list');
|
||||||
const countEl = document.getElementById('domain-handover-count');
|
const countEl = document.getElementById('domain-handover-count');
|
||||||
|
|||||||
@@ -1,5 +1,173 @@
|
|||||||
import { createListPageModule } from '/js/core/app-list-page-module.js';
|
import { createListPageModule } from '/js/core/app-list-page-module.js';
|
||||||
import { escapeHtml, withCurrentListReturn } from '/js/pages/app-list-utils.js';
|
import { escapeHtml, withCurrentListReturn, postAction } from '/js/pages/app-list-utils.js';
|
||||||
|
import { postForm } from '/js/core/app-http.js';
|
||||||
|
|
||||||
|
const LEVEL_LABELS = {
|
||||||
|
niedrig: 'Low',
|
||||||
|
normal: 'Normal',
|
||||||
|
hoch: 'High',
|
||||||
|
kritisch: 'Critical',
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveDialog() {
|
||||||
|
const dialog = document.querySelector('[data-app-security-dialog]');
|
||||||
|
if (!(dialog instanceof HTMLDialogElement)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
dialog,
|
||||||
|
domainEl: dialog.querySelector('#sec-dialog-domain'),
|
||||||
|
levelSelect: dialog.querySelector('#sec-dialog-level'),
|
||||||
|
noteTextarea: dialog.querySelector('#sec-dialog-note'),
|
||||||
|
csrfInput: dialog.querySelector('#sec-dialog-csrf'),
|
||||||
|
saveButton: dialog.querySelector('[data-sec-dialog-save]'),
|
||||||
|
cancelButton: dialog.querySelector('[data-sec-dialog-cancel]'),
|
||||||
|
closeButton: dialog.querySelector('[data-sec-dialog-close]'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSecurityDialog(badge, securityLevelUrl, labels) {
|
||||||
|
const els = resolveDialog();
|
||||||
|
if (!els) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domainNo = badge.dataset.domainNo || '';
|
||||||
|
const currentLevel = badge.dataset.level || 'normal';
|
||||||
|
const currentNote = badge.dataset.note || '';
|
||||||
|
const domainName = badge.dataset.domainName || domainNo;
|
||||||
|
|
||||||
|
els.domainEl.textContent = domainName;
|
||||||
|
els.levelSelect.value = currentLevel;
|
||||||
|
els.noteTextarea.value = currentNote;
|
||||||
|
els.saveButton.disabled = false;
|
||||||
|
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
let saved = false;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
els.saveButton.removeEventListener('click', onSave);
|
||||||
|
els.cancelButton.removeEventListener('click', onCancel);
|
||||||
|
if (els.closeButton) {
|
||||||
|
els.closeButton.removeEventListener('click', onCancel);
|
||||||
|
}
|
||||||
|
els.dialog.removeEventListener('close', onClose);
|
||||||
|
els.dialog.removeEventListener('cancel', onCancelDialog);
|
||||||
|
els.dialog.removeEventListener('click', onBackdrop);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
saved = true;
|
||||||
|
cleanup();
|
||||||
|
try {
|
||||||
|
if (els.dialog.open) {
|
||||||
|
els.dialog.close();
|
||||||
|
}
|
||||||
|
} catch { /* no-op */ }
|
||||||
|
document.body.classList.remove('modal-is-open');
|
||||||
|
if (activeElement && document.contains(activeElement)) {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
activeElement.focus({ preventScroll: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSave = async () => {
|
||||||
|
const newLevel = els.levelSelect.value;
|
||||||
|
const note = els.noteTextarea.value.trim();
|
||||||
|
const csrfKey = els.csrfInput?.name || 'csrf_token';
|
||||||
|
const csrfToken = els.csrfInput?.value || '';
|
||||||
|
|
||||||
|
if (!csrfToken) {
|
||||||
|
console.error('[security-dialog] No CSRF token available');
|
||||||
|
els.saveButton.disabled = false;
|
||||||
|
els.saveButton.removeAttribute('aria-busy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
els.saveButton.disabled = true;
|
||||||
|
els.saveButton.setAttribute('aria-busy', 'true');
|
||||||
|
|
||||||
|
const body = new URLSearchParams({
|
||||||
|
domain_no: domainNo,
|
||||||
|
security_level: newLevel,
|
||||||
|
note,
|
||||||
|
[csrfKey]: csrfToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await postForm(securityLevelUrl, body);
|
||||||
|
if (resp?.ok) {
|
||||||
|
const levelLabel = labels?.[resp.level] ?? LEVEL_LABELS[resp.level] ?? resp.level;
|
||||||
|
const textNode = Array.from(badge.childNodes).find(
|
||||||
|
(n) => n.nodeType === Node.TEXT_NODE && n.textContent.trim() !== ''
|
||||||
|
);
|
||||||
|
if (textNode) {
|
||||||
|
textNode.textContent = levelLabel;
|
||||||
|
}
|
||||||
|
badge.dataset.variant = resp.badge_variant ?? 'neutral';
|
||||||
|
badge.dataset.level = resp.level ?? newLevel;
|
||||||
|
badge.dataset.note = resp.note ?? note;
|
||||||
|
closeDialog();
|
||||||
|
} else {
|
||||||
|
console.warn('[security-dialog] Save returned ok=false:', resp);
|
||||||
|
els.saveButton.disabled = false;
|
||||||
|
els.saveButton.removeAttribute('aria-busy');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[security-dialog] Save failed:', error);
|
||||||
|
els.saveButton.disabled = false;
|
||||||
|
els.saveButton.removeAttribute('aria-busy');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancelDialog = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
if (!saved) {
|
||||||
|
saved = true;
|
||||||
|
cleanup();
|
||||||
|
document.body.classList.remove('modal-is-open');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBackdrop = (event) => {
|
||||||
|
if (event.target === els.dialog) {
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
els.saveButton.addEventListener('click', onSave);
|
||||||
|
els.cancelButton.addEventListener('click', onCancel);
|
||||||
|
if (els.closeButton) {
|
||||||
|
els.closeButton.addEventListener('click', onCancel);
|
||||||
|
}
|
||||||
|
els.dialog.addEventListener('cancel', onCancelDialog);
|
||||||
|
els.dialog.addEventListener('close', onClose);
|
||||||
|
els.dialog.addEventListener('click', onBackdrop);
|
||||||
|
|
||||||
|
document.body.classList.add('modal-is-open');
|
||||||
|
|
||||||
|
try {
|
||||||
|
els.dialog.showModal();
|
||||||
|
} catch {
|
||||||
|
cleanup();
|
||||||
|
document.body.classList.remove('modal-is-open');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
els.levelSelect.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
createListPageModule({
|
createListPageModule({
|
||||||
configId: 'helpdesk-domains',
|
configId: 'helpdesk-domains',
|
||||||
@@ -9,8 +177,34 @@ createListPageModule({
|
|||||||
setup: ({ config, gridjs, appBase, initListPage }) => {
|
setup: ({ config, gridjs, appBase, initListPage }) => {
|
||||||
const labels = config.labels || {};
|
const labels = config.labels || {};
|
||||||
const domainBaseUrl = config.domainBaseUrl || '';
|
const domainBaseUrl = config.domainBaseUrl || '';
|
||||||
|
const securityLevelUrl = (config.securityLevelDataUrl || '').trim();
|
||||||
const domainUrlIndex = 7;
|
const domainUrlIndex = 7;
|
||||||
|
|
||||||
|
const gridContainer = document.querySelector('#helpdesk-domains-grid');
|
||||||
|
|
||||||
|
gridContainer.addEventListener('click', (event) => {
|
||||||
|
const badge = event.target.closest('td .badge[data-domain-no][role="button"]');
|
||||||
|
if (!badge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
openSecurityDialog(badge, securityLevelUrl, labels);
|
||||||
|
});
|
||||||
|
|
||||||
|
gridContainer.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key !== 'Enter' && event.key !== ' ') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const badge = event.target.closest('.badge[data-domain-no][role="button"]');
|
||||||
|
if (!badge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
openSecurityDialog(badge, securityLevelUrl, labels);
|
||||||
|
});
|
||||||
|
|
||||||
const { gridConfig } = initListPage({
|
const { gridConfig } = initListPage({
|
||||||
grid: {
|
grid: {
|
||||||
gridjs,
|
gridjs,
|
||||||
@@ -56,10 +250,24 @@ createListPageModule({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ name: labels.administration || 'Administration', sort: true },
|
{ name: labels.administration || 'Administration', sort: true },
|
||||||
|
{
|
||||||
|
name: labels.securityLevel || 'Security level',
|
||||||
|
sort: true,
|
||||||
|
formatter: (cell) => {
|
||||||
|
const level = cell?.level || 'normal';
|
||||||
|
const variant = cell?.variant || 'neutral';
|
||||||
|
const domainNo = cell?.domainNo || '';
|
||||||
|
const note = cell?.note || '';
|
||||||
|
const domainName = cell?.domainName || domainNo;
|
||||||
|
const levelLabel = labels?.[level] ?? LEVEL_LABELS[level] ?? level;
|
||||||
|
const noteAttr = note ? ` data-note="${escapeHtml(note)}"` : '';
|
||||||
|
return gridjs.html(`<span class="badge" data-variant="${escapeHtml(variant)}" data-domain-no="${escapeHtml(domainNo)}" data-level="${escapeHtml(level)}" data-domain-name="${escapeHtml(domainName)}" role="button" tabindex="0"${noteAttr}>${escapeHtml(levelLabel)}</span>`);
|
||||||
|
},
|
||||||
|
},
|
||||||
{ name: 'debitor_url', hidden: true },
|
{ name: 'debitor_url', hidden: true },
|
||||||
{ name: 'domain_detail_url', hidden: true },
|
{ name: 'domain_detail_url', hidden: true },
|
||||||
],
|
],
|
||||||
sortColumns: ['No', 'Customer_Name', 'URL', 'contract_type', 'State', 'Administration', null, null],
|
sortColumns: ['No', 'Customer_Name', 'URL', 'contract_type', 'State', 'Administration', 'security_level', null, null],
|
||||||
paginationLimit: 10,
|
paginationLimit: 10,
|
||||||
language: config.gridLang || {},
|
language: config.gridLang || {},
|
||||||
mapData: (data) => (data.data || []).map((row) => {
|
mapData: (data) => (data.data || []).map((row) => {
|
||||||
@@ -73,6 +281,7 @@ createListPageModule({
|
|||||||
row.contract_type || '',
|
row.contract_type || '',
|
||||||
{ state: row.State || '', variant: row.state_variant || 'neutral' },
|
{ state: row.State || '', variant: row.state_variant || 'neutral' },
|
||||||
row.Administration || '',
|
row.Administration || '',
|
||||||
|
{ level: row.security_level || 'normal', variant: row.security_level_variant || 'neutral', domainNo: row.No || '', note: row.security_level_note || '', domainName: row.Customer_Name || row.No || '' },
|
||||||
row.debitor_url || '',
|
row.debitor_url || '',
|
||||||
domainDetailUrl,
|
domainDetailUrl,
|
||||||
];
|
];
|
||||||
@@ -83,6 +292,7 @@ createListPageModule({
|
|||||||
rowInteraction: { linkColumn: 0 },
|
rowInteraction: { linkColumn: 0 },
|
||||||
rowDblClick: {
|
rowDblClick: {
|
||||||
getUrl: (rowData) => rowData?.cells?.[domainUrlIndex]?.data || '',
|
getUrl: (rowData) => rowData?.cells?.[domainUrlIndex]?.data || '',
|
||||||
|
exclude: '.badge[role="button"], .grid-actions, button, a, input, select, textarea',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { telemetry } from './app-telemetry.js';
|
|||||||
const JSON_ACCEPT = 'application/json';
|
const JSON_ACCEPT = 'application/json';
|
||||||
const DEFAULT_HEADERS = Object.freeze({
|
const DEFAULT_HEADERS = Object.freeze({
|
||||||
'Accept': JSON_ACCEPT,
|
'Accept': JSON_ACCEPT,
|
||||||
'X-Requested-With': 'fetch',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
});
|
});
|
||||||
|
|
||||||
const toObject = (value) => {
|
const toObject = (value) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user