feat(helpdesk): add dashboards, communication feed, settings UI and fix routing
Add support/sales/controlling dashboards with KPIs, trend charts and risk indicators. Add debitor communication timeline, contact filters, system recommendations engine, and configurable controlling risk rules. Rename search→index to fix query-string preservation on back navigation. Remove fake escalation rate metric, add KPI info tooltips, and switch trend chart colors to red (created) / green (closed) for clarity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ $request = requestInput();
|
||||
$customerNo = trim((string) ($id ?? ''));
|
||||
|
||||
if ($customerNo === '') {
|
||||
Router::redirect('helpdesk/debitor');
|
||||
Router::redirect('helpdesk');
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -26,15 +26,15 @@ $detail = $detailService->loadCustomer($customerNo);
|
||||
$detailStatus = (string) ($detail['status'] ?? '');
|
||||
|
||||
if ($detailStatus === 'not_found') {
|
||||
Flash::error(t('Debtor not found'), 'helpdesk/debitor', 'debtor_not_found');
|
||||
Router::redirect('helpdesk/debitor');
|
||||
Flash::error(t('Debtor not found'), 'helpdesk', 'debtor_not_found');
|
||||
Router::redirect('helpdesk');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($detailStatus === 'not_configured') {
|
||||
Flash::error(t('BC connection is not configured'), 'helpdesk/debitor', 'not_configured');
|
||||
Router::redirect('helpdesk/debitor');
|
||||
Flash::error(t('BC connection is not configured'), 'helpdesk', 'not_configured');
|
||||
Router::redirect('helpdesk');
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -52,10 +52,11 @@ if ($returnTarget !== '') {
|
||||
$searchQuery = trim((string) $request->query('search', ''));
|
||||
|
||||
$backUrl = $searchQuery !== ''
|
||||
? lurl('helpdesk/debitor') . '?search=' . rawurlencode($searchQuery)
|
||||
: lurl('helpdesk/debitor');
|
||||
? lurl('helpdesk') . '?search=' . rawurlencode($searchQuery)
|
||||
: lurl('helpdesk');
|
||||
}
|
||||
|
||||
// --- Tickets filter context ---
|
||||
$ticketsFilterSchema = require __DIR__ . '/debitor-tickets-filter-schema.php';
|
||||
$ticketsFilterState = gridParseFilters($request->queryAll(), gridSchemaQuery($ticketsFilterSchema));
|
||||
$ticketsToolbarOptionSets = [
|
||||
@@ -92,5 +93,36 @@ $filterChipMeta = [
|
||||
],
|
||||
];
|
||||
|
||||
// --- Contacts filter context ---
|
||||
$contactsFilterSchema = require __DIR__ . '/debitor-contacts-filter-schema.php';
|
||||
$contactsFilterState = gridParseFilters($request->queryAll(), gridSchemaQuery($contactsFilterSchema));
|
||||
$contactsToolbarOptionSets = [
|
||||
'type_items' => [],
|
||||
];
|
||||
$contactsListFilterContext = gridBuildListFilterContext($contactsFilterSchema, [
|
||||
'filter_state' => $contactsFilterState,
|
||||
'search_keys' => ['search'],
|
||||
'toolbar_option_sets' => $contactsToolbarOptionSets,
|
||||
]);
|
||||
$contactsSearchToolbarFilterSchema = $contactsListFilterContext['searchToolbarFilterSchema'];
|
||||
$contactsDrawerToolbarFilterSchema = $contactsListFilterContext['drawerToolbarFilterSchema'];
|
||||
$contactsToolbarFilterState = $contactsListFilterContext['toolbarFilterState'];
|
||||
$contactsToolbarOptionSets = $contactsListFilterContext['toolbarOptionSets'];
|
||||
$contactsSchemaByKey = $contactsListFilterContext['schemaByKey'];
|
||||
$contactsClientFilterSchema = $contactsListFilterContext['clientFilterSchema'];
|
||||
$contactsSearchConfig = $contactsListFilterContext['searchConfig'];
|
||||
$contactsFilterChipMeta = [
|
||||
'search' => [
|
||||
'label' => t('Search'),
|
||||
'type' => 'text',
|
||||
],
|
||||
'type' => [
|
||||
'label' => t('Type'),
|
||||
'type' => 'select',
|
||||
'default' => (string) (($contactsSchemaByKey['type']['default'] ?? '')),
|
||||
'options' => gridOptionMapFromItems((array) ($contactsToolbarOptionSets['type_items'] ?? [])),
|
||||
],
|
||||
];
|
||||
|
||||
Buffer::set('title', $customerName . ' — ' . t('Helpdesk'));
|
||||
Buffer::set('style_groups', json_encode(['helpdesk']));
|
||||
|
||||
@@ -14,7 +14,7 @@ $customer = is_array($customer ?? null) ? $customer : [];
|
||||
$hasError = $hasError ?? false;
|
||||
$errorMessage = $errorMessage ?? '';
|
||||
$customerName = $customerName ?? '';
|
||||
$backUrl = $backUrl ?? lurl('helpdesk/debitor');
|
||||
$backUrl = $backUrl ?? lurl('helpdesk');
|
||||
|
||||
$searchToolbarFilterSchema = is_array($searchToolbarFilterSchema ?? null) ? $searchToolbarFilterSchema : [];
|
||||
$drawerToolbarFilterSchema = is_array($drawerToolbarFilterSchema ?? null) ? $drawerToolbarFilterSchema : [];
|
||||
@@ -24,38 +24,44 @@ $filterChipMeta = is_array($filterChipMeta ?? null) ? $filterChipMeta : [];
|
||||
$clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchema : [];
|
||||
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
|
||||
|
||||
$supportType = (string) ($customer['Support_Type'] ?? '');
|
||||
$salesperson = (string) ($customer['Salesperson_Code'] ?? '');
|
||||
$city = (string) ($customer['City'] ?? '');
|
||||
$postCode = (string) ($customer['Post_Code'] ?? '');
|
||||
$locationDisplay = $postCode !== '' && $city !== '' ? $postCode . ' ' . $city : ($city ?: $postCode);
|
||||
|
||||
?>
|
||||
<div class="app-details-container"
|
||||
data-customer-no="<?php e($customerNo); ?>"
|
||||
data-customer-name="<?php e($customerName); ?>"
|
||||
data-support-dashboard-url="<?php e(lurl('helpdesk/debitor-support-dashboard-data')); ?>"
|
||||
data-contacts-url="<?php e(lurl('helpdesk/debitor-contacts-data')); ?>"
|
||||
data-tickets-url="<?php e(lurl('helpdesk/debitor-tickets-data')); ?>"
|
||||
data-ticket-base-url="<?php e(lurl('helpdesk/ticket/')); ?>"
|
||||
data-summary-url="<?php e(lurl('helpdesk/debitor-summary-data')); ?>"
|
||||
data-communication-url="<?php e(lurl('helpdesk/debitor-communication-data')); ?>"
|
||||
data-sales-dashboard-url="<?php e(lurl('helpdesk/debitor-sales-dashboard-data')); ?>"
|
||||
data-controlling-dashboard-url="<?php e(lurl('helpdesk/debitor-controlling-dashboard-data')); ?>"
|
||||
>
|
||||
<section>
|
||||
<?php
|
||||
$breadcrumbs = [
|
||||
['label' => t('Home'), 'path' => 'admin'],
|
||||
['label' => t('Helpdesk'), 'path' => 'helpdesk/debitor'],
|
||||
['label' => t('Helpdesk'), 'path' => 'helpdesk'],
|
||||
['label' => $customerName],
|
||||
];
|
||||
require templatePath('partials/app-breadcrumb.phtml');
|
||||
?>
|
||||
|
||||
<?php
|
||||
$titlebarActions = [];
|
||||
if (!$hasError && $customer !== []) {
|
||||
$titlebarActions[] = [
|
||||
'label' => t('Refresh data'),
|
||||
'type' => 'button',
|
||||
'name' => 'support-refresh',
|
||||
'class' => 'secondary outline',
|
||||
];
|
||||
}
|
||||
|
||||
$titlebar = [
|
||||
'title' => $customerName,
|
||||
'backHref' => $backUrl,
|
||||
'backTitle' => t('Back'),
|
||||
'actions' => [],
|
||||
'actions' => $titlebarActions,
|
||||
];
|
||||
require templatePath('partials/app-details-titlebar.phtml');
|
||||
?>
|
||||
@@ -71,154 +77,177 @@ $locationDisplay = $postCode !== '' && $city !== '' ? $postCode . ' ' . $city :
|
||||
</div>
|
||||
<?php elseif ($customer !== []): ?>
|
||||
|
||||
<!-- KPI tiles — values updated by JS after async load -->
|
||||
<div class="app-tiles">
|
||||
<span data-kpi="open-tickets">
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => '#',
|
||||
'label' => t('Open tickets'),
|
||||
'count' => '—',
|
||||
'icon' => 'bi bi-ticket-detailed-fill',
|
||||
'iconBg' => '#f3e8ff',
|
||||
'iconColor' => '#7c3aed',
|
||||
]);
|
||||
?>
|
||||
</span>
|
||||
<span data-kpi="last-activity">
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => '#',
|
||||
'label' => t('Last activity'),
|
||||
'count' => '—',
|
||||
'icon' => 'bi bi-clock-history',
|
||||
'iconBg' => '#fde8d8',
|
||||
'iconColor' => '#b35c14',
|
||||
]);
|
||||
?>
|
||||
</span>
|
||||
<span data-kpi="contacts">
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => '#',
|
||||
'label' => t('Contacts'),
|
||||
'count' => '—',
|
||||
'icon' => 'bi bi-people-fill',
|
||||
'iconBg' => '#d9f2e6',
|
||||
'iconColor' => '#1f6a3a',
|
||||
]);
|
||||
?>
|
||||
</span>
|
||||
<?php
|
||||
MintyPHP\Support\Tile::render([
|
||||
'href' => '#',
|
||||
'label' => t('Support type'),
|
||||
'count' => $supportType !== '' ? $supportType : '—',
|
||||
'icon' => 'bi bi-headset',
|
||||
'iconBg' => '#d9e8fb',
|
||||
'iconColor' => '#1a56db',
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
|
||||
<!-- Tabs: Overview / Tickets / Contacts -->
|
||||
<!-- Tabs: Support / Sales / Controlling -->
|
||||
<div class="app-tabs" data-tabs data-app-component="tabs" data-tabs-param="tab" data-tabs-storage-key="helpdesk-debitor-detail">
|
||||
<div class="app-tabs-nav">
|
||||
<button type="button" data-tab="overview" data-tab-default>
|
||||
<?php e(t('Overview')); ?>
|
||||
<button type="button" data-tab="support" data-tab-default>
|
||||
<?php e(t('Support')); ?>
|
||||
</button>
|
||||
<button type="button" data-tab="tickets">
|
||||
<?php e(t('Tickets')); ?> <span class="badge" id="tab-badge-tickets"></span>
|
||||
<button type="button" data-tab="sales">
|
||||
<?php e(t('Sales')); ?>
|
||||
</button>
|
||||
<button type="button" data-tab="contacts">
|
||||
<?php e(t('Contacts')); ?> <span class="badge" id="tab-badge-contacts"></span>
|
||||
<button type="button" data-tab="controlling">
|
||||
<?php e(t('Controlling')); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Overview panel -->
|
||||
<div data-tab-panel="overview">
|
||||
<div id="overview-loading" aria-busy="true"><?php e(t('Loading...')); ?></div>
|
||||
<div id="overview-content" hidden>
|
||||
<div class="grid grid-2">
|
||||
<!-- Open tickets (max 10) -->
|
||||
<div class="app-stats-table">
|
||||
<div class="app-stats-table-header">
|
||||
<?php e(t('Open tickets')); ?>
|
||||
<a href="#" class="helpdesk-tab-link" data-switch-tab="tickets"><?php e(t('All tickets')); ?> →</a>
|
||||
</div>
|
||||
<div id="overview-open-tickets"></div>
|
||||
<!-- Support panel -->
|
||||
<div data-tab-panel="support">
|
||||
<div id="support-loading" aria-busy="true"><?php e(t('Loading...')); ?></div>
|
||||
<div id="support-content" hidden>
|
||||
<div class="helpdesk-support-metrics">
|
||||
<article class="helpdesk-support-metric helpdesk-kpi-clickable" data-kpi-filter="open">
|
||||
<p class="helpdesk-support-metric-label"><?php e(t('Open tickets')); ?></p>
|
||||
<p class="helpdesk-support-metric-value" id="support-kpi-open-tickets">—</p>
|
||||
<p class="helpdesk-support-metric-trend" id="support-kpi-open-trend"></p>
|
||||
</article>
|
||||
<article class="helpdesk-support-metric helpdesk-kpi-clickable" data-kpi-filter="">
|
||||
<p class="helpdesk-support-metric-label"><?php e(t('New (30d)')); ?></p>
|
||||
<p class="helpdesk-support-metric-value" id="support-kpi-created-30d">—</p>
|
||||
<p class="helpdesk-support-metric-trend" id="support-kpi-created-trend"></p>
|
||||
</article>
|
||||
<article class="helpdesk-support-metric helpdesk-kpi-clickable" data-kpi-filter="closed">
|
||||
<p class="helpdesk-support-metric-label"><?php e(t('Resolved (30d)')); ?></p>
|
||||
<p class="helpdesk-support-metric-value" id="support-kpi-closed-30d">—</p>
|
||||
<p class="helpdesk-support-metric-trend" id="support-kpi-closed-trend"></p>
|
||||
</article>
|
||||
<article class="helpdesk-support-metric">
|
||||
<p class="helpdesk-support-metric-label"><?php e(t('Avg. age open')); ?></p>
|
||||
<p class="helpdesk-support-metric-value" id="support-kpi-avg-age">—</p>
|
||||
<p class="helpdesk-support-metric-trend" id="support-kpi-age-hint"></p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<h3 class="helpdesk-support-section-title"><?php e(t('Analysis')); ?></h3>
|
||||
<div class="grid grid-2" id="support-analysis-grid">
|
||||
<div class="app-stats-table" id="support-recommendations-wrapper">
|
||||
<div class="app-stats-table-header"><?php e(t('System recommendations')); ?></div>
|
||||
<div id="support-system-recommendations"></div>
|
||||
</div>
|
||||
|
||||
<!-- Recent activity (5 newest by Last_Activity_Date) -->
|
||||
<div class="app-stats-table">
|
||||
<div class="app-stats-table-header"><?php e(t('Recent activity')); ?></div>
|
||||
<div id="overview-recent-activity"></div>
|
||||
<div class="app-stats-table" id="support-contracts-wrapper">
|
||||
<div class="app-stats-table-header"><?php e(t('Contracts & products')); ?></div>
|
||||
<div id="support-contracts-widget"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<!-- Top contacts (5) -->
|
||||
<div class="app-stats-table">
|
||||
<div class="app-stats-table-header">
|
||||
<?php e(t('Contacts')); ?>
|
||||
<a href="#" class="helpdesk-tab-link" data-switch-tab="contacts"><?php e(t('All contacts')); ?> →</a>
|
||||
</div>
|
||||
<div id="overview-contacts"></div>
|
||||
</div>
|
||||
|
||||
<!-- Support info & master data -->
|
||||
<div class="app-stats-table">
|
||||
<div class="app-stats-table-header"><?php e(t('Support info & master data')); ?></div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong><?php e(t('Debtor No.')); ?></strong></td>
|
||||
<td><?php e((string) ($customer['No'] ?? '')); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong><?php e(t('Name')); ?></strong></td>
|
||||
<td><?php e((string) ($customer['Name'] ?? '')); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong><?php e(t('Support type')); ?></strong></td>
|
||||
<td><?php e($supportType !== '' ? $supportType : '—'); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong><?php e(t('Salesperson')); ?></strong></td>
|
||||
<td><?php e($salesperson !== '' ? $salesperson : '—'); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong><?php e(t('Location')); ?></strong></td>
|
||||
<td><?php e($locationDisplay !== '' ? $locationDisplay : '—'); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong><?php e(t('Phone')); ?></strong></td>
|
||||
<td><?php e((string) ($customer['Phone_No'] ?? '')); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong><?php e(t('E-Mail')); ?></strong></td>
|
||||
<td><?php e((string) ($customer['E_Mail'] ?? '')); ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="app-stats-table" id="support-escalation-wrapper">
|
||||
<div class="app-stats-table-header"><?php e(t('Escalated tickets')); ?></div>
|
||||
<div id="support-escalation-widget"></div>
|
||||
</div>
|
||||
|
||||
<section class="helpdesk-support-ticket-list">
|
||||
<h3 class="helpdesk-support-section-title"><?php e(t('Ticket list')); ?></h3>
|
||||
<?php
|
||||
$filterUiNamespace = 'helpdesk-tickets';
|
||||
require templatePath('partials/app-list-filters.phtml');
|
||||
?>
|
||||
<div id="helpdesk-tickets-grid"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tickets panel (Grid.js) -->
|
||||
<div data-tab-panel="tickets" hidden>
|
||||
<?php
|
||||
$filterUiNamespace = 'helpdesk-tickets';
|
||||
require templatePath('partials/app-list-filters.phtml');
|
||||
?>
|
||||
<div id="helpdesk-tickets-grid"></div>
|
||||
<!-- Sales panel -->
|
||||
<div data-tab-panel="sales" hidden>
|
||||
<div id="sales-loading" aria-busy="true"><?php e(t('Loading...')); ?></div>
|
||||
<div id="sales-content" hidden>
|
||||
<div class="helpdesk-support-metrics">
|
||||
<article class="helpdesk-support-metric">
|
||||
<p class="helpdesk-support-metric-label"><?php e(t('Monthly volume')); ?></p>
|
||||
<p class="helpdesk-support-metric-value" id="sales-kpi-monthly-volume">0</p>
|
||||
<p class="helpdesk-support-metric-trend" id="sales-kpi-monthly-volume-trend"></p>
|
||||
</article>
|
||||
<article class="helpdesk-support-metric">
|
||||
<p class="helpdesk-support-metric-label"><?php e(t('Active contracts')); ?></p>
|
||||
<p class="helpdesk-support-metric-value" id="sales-kpi-active-contracts">0</p>
|
||||
<p class="helpdesk-support-metric-trend" id="sales-kpi-active-contracts-trend"></p>
|
||||
</article>
|
||||
<article class="helpdesk-support-metric">
|
||||
<p class="helpdesk-support-metric-label"><?php e(t('Avg. contract value')); ?></p>
|
||||
<p class="helpdesk-support-metric-value" id="sales-kpi-avg-value">0</p>
|
||||
<p class="helpdesk-support-metric-trend" id="sales-kpi-avg-value-trend"></p>
|
||||
</article>
|
||||
<article class="helpdesk-support-metric">
|
||||
<p class="helpdesk-support-metric-label"><?php e(t('Next invoicing')); ?></p>
|
||||
<p class="helpdesk-support-metric-value" id="sales-kpi-next-invoicing">—</p>
|
||||
<p class="helpdesk-support-metric-trend" id="sales-kpi-next-invoicing-trend"></p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div id="sales-contracts-detail"></div>
|
||||
|
||||
<section id="sales-contacts-section">
|
||||
<h3 class="helpdesk-support-section-title"><?php e(t('Contacts')); ?></h3>
|
||||
<?php
|
||||
$filterUiNamespace = 'helpdesk-contacts';
|
||||
$searchToolbarFilterSchema = $contactsSearchToolbarFilterSchema;
|
||||
$drawerToolbarFilterSchema = $contactsDrawerToolbarFilterSchema;
|
||||
$toolbarFilterState = $contactsToolbarFilterState;
|
||||
$toolbarOptionSets = $contactsToolbarOptionSets;
|
||||
require templatePath('partials/app-list-filters.phtml');
|
||||
?>
|
||||
<div id="helpdesk-contacts-grid"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contacts panel -->
|
||||
<div data-tab-panel="contacts" hidden>
|
||||
<div id="contacts-loading" aria-busy="true"><?php e(t('Loading...')); ?></div>
|
||||
<div id="contacts-content" hidden></div>
|
||||
<!-- Controlling panel -->
|
||||
<div data-tab-panel="controlling" hidden>
|
||||
<div id="controlling-spinner" class="helpdesk-tab-spinner">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
<div id="controlling-content" hidden>
|
||||
<div class="helpdesk-segment-control" id="controlling-period-selector">
|
||||
<input type="radio" name="controlling-period" id="controlling-period-30" value="30">
|
||||
<label for="controlling-period-30"><?php e(t('30 days')); ?></label>
|
||||
<input type="radio" name="controlling-period" id="controlling-period-90" value="90" checked>
|
||||
<label for="controlling-period-90"><?php e(t('90 days')); ?></label>
|
||||
<input type="radio" name="controlling-period" id="controlling-period-180" value="180">
|
||||
<label for="controlling-period-180"><?php e(t('6 months')); ?></label>
|
||||
<input type="radio" name="controlling-period" id="controlling-period-365" value="365">
|
||||
<label for="controlling-period-365"><?php e(t('1 year')); ?></label>
|
||||
</div>
|
||||
|
||||
<div class="helpdesk-support-metrics" id="controlling-kpis">
|
||||
<div class="helpdesk-support-metric">
|
||||
<div class="helpdesk-support-metric-label"><?php e(t('Ticket volume')); ?></div>
|
||||
<div class="helpdesk-support-metric-value" id="controlling-kpi-volume">—</div>
|
||||
<p class="helpdesk-support-metric-trend" id="controlling-kpi-volume-trend"></p>
|
||||
</div>
|
||||
<div class="helpdesk-support-metric">
|
||||
<div class="helpdesk-support-metric-label"><?php e(t('Close rate')); ?> <span class="helpdesk-kpi-info" data-tooltip="<?php e(t('kpi_tooltip_close_rate')); ?>" data-tooltip-pos="top" tabindex="0">i</span></div>
|
||||
<div class="helpdesk-support-metric-value" id="controlling-kpi-closerate">—</div>
|
||||
<p class="helpdesk-support-metric-trend" id="controlling-kpi-closerate-trend"></p>
|
||||
</div>
|
||||
<div class="helpdesk-support-metric">
|
||||
<div class="helpdesk-support-metric-label"><?php e(t('Avg. resolution')); ?> <span class="helpdesk-kpi-info" data-tooltip="<?php e(t('kpi_tooltip_resolution')); ?>" data-tooltip-pos="top" tabindex="0">i</span></div>
|
||||
<div class="helpdesk-support-metric-value" id="controlling-kpi-resolution">—</div>
|
||||
<p class="helpdesk-support-metric-trend" id="controlling-kpi-resolution-trend"></p>
|
||||
</div>
|
||||
<div class="helpdesk-support-metric">
|
||||
<div class="helpdesk-support-metric-label"><?php e(t('Open backlog')); ?> <span class="helpdesk-kpi-info" data-tooltip="<?php e(t('kpi_tooltip_backlog')); ?>" data-tooltip-pos="top" tabindex="0">i</span></div>
|
||||
<div class="helpdesk-support-metric-value" id="controlling-kpi-backlog">—</div>
|
||||
<p class="helpdesk-support-metric-trend" id="controlling-kpi-backlog-trend"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-stats-table">
|
||||
<div class="app-stats-table-header"><?php e(t('Ticket trend')); ?></div>
|
||||
<div class="helpdesk-controlling-chart" id="controlling-trend-chart"></div>
|
||||
</div>
|
||||
|
||||
<div class="app-stats-table">
|
||||
<div class="app-stats-table-header"><?php e(t('Support efficiency')); ?></div>
|
||||
<div id="controlling-efficiency"></div>
|
||||
</div>
|
||||
|
||||
<div class="app-stats-table">
|
||||
<div class="app-stats-table-header"><?php e(t('Risk indicators')); ?></div>
|
||||
<div id="controlling-risk-indicators"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="controlling-error" hidden></div>
|
||||
<div id="controlling-empty" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
@@ -264,11 +293,32 @@ $locationDisplay = $postCode !== '' && $city !== '' ? $postCode . ' ' . $city :
|
||||
'filterChipMeta' => $filterChipMeta,
|
||||
]); ?></script>
|
||||
|
||||
<!-- Grid.js contacts config -->
|
||||
<script type="application/json" id="page-config-helpdesk-contacts"><?php gridJsonForJs([
|
||||
'dataUrl' => lurl('helpdesk/debitor-contacts-data'),
|
||||
'customerNo' => $customerNo,
|
||||
'customerName' => $customerName,
|
||||
'gridLang' => gridLang(),
|
||||
'labels' => [
|
||||
'name' => t('Name'),
|
||||
'type' => t('Type'),
|
||||
'jobTitle' => t('Job title'),
|
||||
'email' => t('E-Mail'),
|
||||
'phone' => t('Phone'),
|
||||
'mobile' => t('Mobile'),
|
||||
],
|
||||
'gridSearch' => $contactsSearchConfig,
|
||||
'filterSchema' => $contactsClientFilterSchema,
|
||||
'filterChipMeta' => $contactsFilterChipMeta,
|
||||
]); ?></script>
|
||||
|
||||
<!-- i18n strings for JS rendering -->
|
||||
<script type="application/json" id="helpdesk-i18n"><?php gridJsonForJs([
|
||||
'Ticket No.' => t('Ticket No.'),
|
||||
'Description' => t('Description'),
|
||||
'Status' => t('Status'),
|
||||
'Sales' => t('Sales'),
|
||||
'Controlling' => t('Controlling'),
|
||||
'Category' => t('Category'),
|
||||
'Contact' => t('Contact'),
|
||||
'Support' => t('Support'),
|
||||
@@ -286,7 +336,71 @@ $locationDisplay = $postCode !== '' && $city !== '' ? $postCode . ' ' . $city :
|
||||
'No tickets are linked to this debtor in Business Central.' => t('No tickets are linked to this debtor in Business Central.'),
|
||||
'No contacts found' => t('No contacts found'),
|
||||
'No contacts are linked to this debtor in Business Central.' => t('No contacts are linked to this debtor in Business Central.'),
|
||||
'No recent activity found.' => t('No recent activity found.'),
|
||||
'Total tickets' => t('Total tickets'),
|
||||
'Critical (>48h)' => t('Critical (>48h)'),
|
||||
'Oldest open' => t('Oldest open'),
|
||||
'Ticket list' => t('Ticket list'),
|
||||
'System recommendations' => t('System recommendations'),
|
||||
'Action' => t('Action'),
|
||||
'Age (h)' => t('Age (h)'),
|
||||
'No system recommendations right now.' => t('No system recommendations right now.'),
|
||||
'helpdesk.recommendation.escalation_overdue' => t('Escalation SLA exceeded by {overdue_hours}h ({escalation_code}).'),
|
||||
'helpdesk.recommendation.high_risk_aging' => t('High-risk ticket ({escalation_code}) is open for {age_hours}h.'),
|
||||
'helpdesk.recommendation.unassigned_open' => t('Ticket is unassigned for {age_hours}h.'),
|
||||
'helpdesk.recommendation.stale_open' => t('No activity for {age_hours}h.'),
|
||||
'helpdesk.recommendation.customer_backlog' => t('Customer backlog: {open_tickets} open tickets.'),
|
||||
'Contracts & products' => t('Contracts & products'),
|
||||
'No contracts found for this customer.' => t('No contracts found for this customer.'),
|
||||
'Contract No.' => t('Contract No.'),
|
||||
'Product type' => t('Product type'),
|
||||
'Next invoicing' => t('Next invoicing'),
|
||||
'Amount' => t('Amount'),
|
||||
'Active contracts' => t('Active contracts'),
|
||||
'Total contracts' => t('Total contracts'),
|
||||
'Could not load contracts.' => t('Could not load contracts.'),
|
||||
'Escalations' => t('Escalations'),
|
||||
'Escalations are not available yet.' => t('Escalations are not available yet.'),
|
||||
'Escalated tickets' => t('Escalated tickets'),
|
||||
'No escalated tickets right now.' => t('No escalated tickets right now.'),
|
||||
'Could not load escalated tickets.' => t('Could not load escalated tickets.'),
|
||||
'Escalation code' => t('Escalation code'),
|
||||
'SLA target' => t('SLA target'),
|
||||
'Overdue by' => t('Overdue by'),
|
||||
'day' => t('day'),
|
||||
'days' => t('days'),
|
||||
'Ticket volume' => t('Ticket volume'),
|
||||
'Close rate' => t('Close rate'),
|
||||
'Avg. resolution' => t('Avg. resolution'),
|
||||
'Open backlog' => t('Open backlog'),
|
||||
'Backlog shrinking' => t('Backlog shrinking'),
|
||||
'Backlog growing' => t('Backlog growing'),
|
||||
'unassigned' => t('unassigned'),
|
||||
'Ticket trend' => t('Ticket trend'),
|
||||
'Support efficiency' => t('Support efficiency'),
|
||||
'Risk indicators' => t('Risk indicators'),
|
||||
'vs. prev. period' => t('vs. prev. period'),
|
||||
'Low' => t('Low'),
|
||||
'Medium' => t('Medium'),
|
||||
'High' => t('High'),
|
||||
'flags' => t('flags'),
|
||||
'No controlling data available.' => t('No controlling data available.'),
|
||||
'Ticket volume increase' => t('Ticket volume increase'),
|
||||
'High avg. resolution time' => t('High avg. resolution time'),
|
||||
'Open ticket aging' => t('Open ticket aging'),
|
||||
'Unassigned ticket ratio' => t('Unassigned ticket ratio'),
|
||||
'Could not load controlling dashboard.' => t('Could not load controlling dashboard.'),
|
||||
'Without assignee' => t('Without assignee'),
|
||||
'Created' => t('Created'),
|
||||
'Closed' => t('Closed'),
|
||||
'30 days' => t('30 days'),
|
||||
'90 days' => t('90 days'),
|
||||
'6 months' => t('6 months'),
|
||||
'1 year' => t('1 year'),
|
||||
'Oldest open' => t('Oldest open'),
|
||||
'Open ticket without activity for' => t('Open ticket without activity for'),
|
||||
'hours' => t('hours'),
|
||||
'Open ticket without assigned support user' => t('Open ticket without assigned support user'),
|
||||
'Follow up oldest open ticket' => t('Follow up oldest open ticket'),
|
||||
'Could not load contacts.' => t('Could not load contacts.'),
|
||||
'Could not load tickets.' => t('Could not load tickets.'),
|
||||
'Ticket communication' => t('Ticket communication'),
|
||||
@@ -307,6 +421,38 @@ $locationDisplay = $postCode !== '' && $city !== '' ? $postCode . ' ' . $city :
|
||||
'SOAP' => t('SOAP'),
|
||||
'Activity fallback' => t('Activity fallback'),
|
||||
'Loading...' => t('Loading...'),
|
||||
'Refresh data' => t('Refresh data'),
|
||||
'Active contracts' => t('Active contracts'),
|
||||
'Monthly volume' => t('Monthly volume'),
|
||||
'Support hours' => t('Support hours'),
|
||||
'Next invoicing' => t('Next invoicing'),
|
||||
'Contract overview' => t('Contract overview'),
|
||||
'No contract details available.' => t('No contract details available.'),
|
||||
'Could not load sales dashboard.' => t('Could not load sales dashboard.'),
|
||||
'Quantity' => t('Quantity'),
|
||||
'Unit price' => t('Unit price'),
|
||||
'Line amount' => t('Line amount'),
|
||||
'Line state' => t('Line state'),
|
||||
'Starting date' => t('Starting date'),
|
||||
'Invoicing period' => t('Invoicing period'),
|
||||
'Salesperson' => t('Salesperson'),
|
||||
'Contacts' => t('Contacts'),
|
||||
'positions' => t('positions'),
|
||||
'per month' => t('per month'),
|
||||
'Last invoicing' => t('Last invoicing'),
|
||||
'Close' => t('Close'),
|
||||
'New (30d)' => t('New (30d)'),
|
||||
'Resolved (30d)' => t('Resolved (30d)'),
|
||||
'Avg. age open' => t('Avg. age open'),
|
||||
'vs. prev. 30d' => t('vs. prev. 30d'),
|
||||
'net (30d)' => t('net (30d)'),
|
||||
'critical' => t('critical'),
|
||||
'Analysis' => t('Analysis'),
|
||||
'Open tickets' => t('Open tickets'),
|
||||
'Avg. contract value' => t('Avg. contract value'),
|
||||
'of {total} total' => t('of {total} total'),
|
||||
'in {days} days' => t('in {days} days'),
|
||||
'largest' => t('largest'),
|
||||
]); ?></script>
|
||||
|
||||
<script src="<?php e(assetVersion('vendor/gridjs/gridjs.umd.js')); ?>"></script>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorCacheControl;
|
||||
use MintyPHP\Module\Helpdesk\Service\TicketCommunicationService;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
@@ -19,6 +20,7 @@ if ($request->method() !== 'GET') {
|
||||
|
||||
$customerNo = trim((string) $request->query('customerNo', ''));
|
||||
$customerName = trim((string) $request->query('customerName', ''));
|
||||
$refreshRequested = DebitorCacheControl::parseRefreshFlag($request->query('refresh', ''));
|
||||
|
||||
if ($customerNo === '' || $customerName === '') {
|
||||
http_response_code(400);
|
||||
@@ -29,14 +31,25 @@ if ($customerNo === '' || $customerName === '') {
|
||||
|
||||
$sessionStore = app(SessionStoreInterface::class);
|
||||
$session = $sessionStore->all();
|
||||
$tenantId = (int) (($session['current_tenant']['id'] ?? 0));
|
||||
$tenantScope = $tenantId > 0 ? (string) $tenantId : 'global';
|
||||
$cacheKey = 'module.helpdesk.debitor_comm_cache.v3.' . $tenantScope . '.' . $customerNo;
|
||||
$cacheTtl = 120;
|
||||
$cached = $sessionStore->get($cacheKey);
|
||||
$tenantScope = DebitorCacheControl::resolveTenantScope($session);
|
||||
if ($refreshRequested) {
|
||||
DebitorCacheControl::invalidateDebitorCaches($sessionStore, $tenantScope, $customerNo, true);
|
||||
}
|
||||
|
||||
if (is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
|
||||
Router::json((array) ($cached['payload'] ?? ['ok' => false]));
|
||||
$cacheKey = DebitorCacheControl::communicationKey($tenantScope, $customerNo);
|
||||
$cacheTtl = DebitorCacheControl::TTL_STANDARD_SECONDS;
|
||||
$cached = $sessionStore->get($cacheKey);
|
||||
$cacheUsed = false;
|
||||
|
||||
if (!$refreshRequested && is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
|
||||
$cacheUsed = true;
|
||||
$payload = (array) ($cached['payload'] ?? ['ok' => false]);
|
||||
$meta = is_array($payload['meta'] ?? null) ? $payload['meta'] : [];
|
||||
$meta['cache_used'] = true;
|
||||
$meta['cache_bypassed'] = false;
|
||||
$meta['cache_refreshed'] = false;
|
||||
$payload['meta'] = $meta;
|
||||
Router::json($payload);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -50,4 +63,10 @@ if (($result['ok'] ?? false) === true) {
|
||||
]);
|
||||
}
|
||||
|
||||
$meta = is_array($result['meta'] ?? null) ? $result['meta'] : [];
|
||||
$meta['cache_used'] = $cacheUsed;
|
||||
$meta['cache_bypassed'] = $refreshRequested;
|
||||
$meta['cache_refreshed'] = $refreshRequested;
|
||||
$result['meta'] = $meta;
|
||||
|
||||
Router::json($result);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorCacheControl;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorDetailService;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
@@ -8,25 +10,163 @@ use MintyPHP\Support\Guard;
|
||||
Guard::requireLogin();
|
||||
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
||||
|
||||
gridRequireGetRequest();
|
||||
|
||||
$filters = gridParseFiltersFromSchemaFile(__DIR__ . '/debitor-contacts-filter-schema.php');
|
||||
$request = requestInput();
|
||||
if ($request->method() !== 'GET') {
|
||||
http_response_code(405);
|
||||
Router::json(['ok' => false, 'error' => 'method_not_allowed']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$customerNo = trim((string) $request->query('customerNo', ''));
|
||||
$customerName = trim((string) $request->query('customerName', ''));
|
||||
$customerNo = (string) ($filters['customerNo'] ?? '');
|
||||
$customerName = (string) ($filters['customerName'] ?? '');
|
||||
$refreshRequested = DebitorCacheControl::parseRefreshFlag($request->query('refresh', ''));
|
||||
|
||||
if ($customerNo === '' || $customerName === '') {
|
||||
http_response_code(400);
|
||||
Router::json(['ok' => false, 'error' => 'missing_parameters']);
|
||||
Router::json([
|
||||
'data' => [],
|
||||
'total' => 0,
|
||||
'type_options' => [],
|
||||
'meta' => [
|
||||
'cache_used' => false,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$service = app(DebitorDetailService::class);
|
||||
$result = $service->loadContacts($customerNo, $customerName);
|
||||
// --- Session cache (lazy TTL = 300s) ---
|
||||
|
||||
Router::json($result);
|
||||
$sessionStore = app(SessionStoreInterface::class);
|
||||
$session = $sessionStore->all();
|
||||
$tenantScope = DebitorCacheControl::resolveTenantScope($session);
|
||||
if ($refreshRequested) {
|
||||
DebitorCacheControl::invalidateDebitorCaches($sessionStore, $tenantScope, $customerNo, true);
|
||||
}
|
||||
|
||||
$cacheKey = DebitorCacheControl::contactsKey($tenantScope, $customerNo);
|
||||
$cacheTtl = DebitorCacheControl::TTL_STANDARD_SECONDS;
|
||||
$cached = $sessionStore->get($cacheKey);
|
||||
$allContacts = null;
|
||||
$cacheUsed = false;
|
||||
|
||||
if (!$refreshRequested && is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
|
||||
$allContacts = $cached['contacts'] ?? [];
|
||||
$cacheUsed = true;
|
||||
}
|
||||
|
||||
if ($allContacts === null) {
|
||||
$service = app(DebitorDetailService::class);
|
||||
$result = $service->loadContacts($customerNo, $customerName);
|
||||
|
||||
if (!($result['ok'] ?? false)) {
|
||||
Router::json([
|
||||
'data' => [],
|
||||
'total' => 0,
|
||||
'type_options' => [],
|
||||
'meta' => [
|
||||
'cache_used' => $cacheUsed,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => $refreshRequested,
|
||||
],
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$allContacts = $result['contacts'] ?? [];
|
||||
|
||||
$sessionStore->set($cacheKey, [
|
||||
'contacts' => $allContacts,
|
||||
'fetched_at' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
// --- Collect unique types for filter options ---
|
||||
|
||||
$typeSet = [];
|
||||
foreach ($allContacts as $contact) {
|
||||
$type = trim((string) ($contact['Type'] ?? ''));
|
||||
if ($type !== '' && !isset($typeSet[$type])) {
|
||||
$typeSet[$type] = true;
|
||||
}
|
||||
}
|
||||
ksort($typeSet);
|
||||
$typeOptions = [];
|
||||
foreach (array_keys($typeSet) as $type) {
|
||||
$typeOptions[] = ['id' => $type, 'description' => $type];
|
||||
}
|
||||
|
||||
// --- PHP-side filtering ---
|
||||
|
||||
$search = trim((string) ($filters['search'] ?? ''));
|
||||
$typeFilter = trim((string) ($filters['type'] ?? ''));
|
||||
$order = (string) ($filters['order'] ?? 'Name');
|
||||
$dir = (string) ($filters['dir'] ?? 'asc');
|
||||
$limit = (int) ($filters['limit'] ?? 10);
|
||||
$offset = (int) ($filters['offset'] ?? 0);
|
||||
|
||||
$filtered = $allContacts;
|
||||
|
||||
// Search: stripos on Name + Job_Title + E_Mail + Phone_No + Mobile_Phone_No
|
||||
if ($search !== '') {
|
||||
$searchLower = mb_strtolower($search);
|
||||
$filtered = array_values(array_filter($filtered, static function (array $contact) use ($searchLower): bool {
|
||||
$haystack = mb_strtolower(
|
||||
($contact['Name'] ?? '') . ' '
|
||||
. ($contact['Job_Title'] ?? '') . ' '
|
||||
. ($contact['E_Mail'] ?? '') . ' '
|
||||
. ($contact['Phone_No'] ?? '') . ' '
|
||||
. ($contact['Mobile_Phone_No'] ?? '')
|
||||
);
|
||||
|
||||
return str_contains($haystack, $searchLower);
|
||||
}));
|
||||
}
|
||||
|
||||
// Type filter
|
||||
if ($typeFilter !== '') {
|
||||
$filtered = array_values(array_filter($filtered, static function (array $contact) use ($typeFilter): bool {
|
||||
return trim((string) ($contact['Type'] ?? '')) === $typeFilter;
|
||||
}));
|
||||
}
|
||||
|
||||
// --- Sorting ---
|
||||
|
||||
$total = count($filtered);
|
||||
|
||||
usort($filtered, static function (array $a, array $b) use ($order, $dir): int {
|
||||
$va = (string) ($a[$order] ?? '');
|
||||
$vb = (string) ($b[$order] ?? '');
|
||||
$cmp = strnatcasecmp($va, $vb);
|
||||
|
||||
return $dir === 'desc' ? -$cmp : $cmp;
|
||||
});
|
||||
|
||||
// --- Pagination ---
|
||||
|
||||
$rows = array_slice($filtered, $offset, $limit);
|
||||
|
||||
// --- Row preparation ---
|
||||
|
||||
$preparedRows = [];
|
||||
foreach ($rows as $contact) {
|
||||
$preparedRows[] = [
|
||||
'Name' => (string) ($contact['Name'] ?? ''),
|
||||
'Type' => (string) ($contact['Type'] ?? ''),
|
||||
'Job_Title' => (string) ($contact['Job_Title'] ?? ''),
|
||||
'E_Mail' => (string) ($contact['E_Mail'] ?? ''),
|
||||
'Phone_No' => (string) ($contact['Phone_No'] ?? ''),
|
||||
'Mobile_Phone_No' => (string) ($contact['Mobile_Phone_No'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
Router::json([
|
||||
'data' => array_values($preparedRows),
|
||||
'total' => max(0, $total),
|
||||
'type_options' => $typeOptions,
|
||||
'meta' => [
|
||||
'cache_used' => $cacheUsed,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => $refreshRequested,
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
return gridFilterSchema([
|
||||
'query' => [
|
||||
'customerNo' => ['type' => 'string'],
|
||||
'customerName' => ['type' => 'string'],
|
||||
'limit' => ['type' => 'int', 'default' => 10, 'min' => 1, 'max' => 100],
|
||||
'offset' => ['type' => 'int', 'default' => 0, 'min' => 0],
|
||||
'search' => ['type' => 'string'],
|
||||
'order' => ['type' => 'order', 'allowed' => ['Name', 'Type', 'Job_Title', 'E_Mail', 'Phone_No'], 'default' => 'Name'],
|
||||
'dir' => ['type' => 'dir', 'default' => 'asc'],
|
||||
'type' => ['type' => 'string', 'default' => ''],
|
||||
],
|
||||
'toolbar' => [
|
||||
[
|
||||
'key' => 'search',
|
||||
'type' => 'text',
|
||||
'label' => 'Search',
|
||||
'input_id' => 'helpdesk-contact-search',
|
||||
'search' => true,
|
||||
'query' => ['type' => 'string'],
|
||||
],
|
||||
[
|
||||
'key' => 'type',
|
||||
'type' => 'select',
|
||||
'label' => 'Type',
|
||||
'input_id' => 'helpdesk-contact-type-filter',
|
||||
'default' => '',
|
||||
'normalize' => 'all_to_empty',
|
||||
'options_key' => 'type_items',
|
||||
'allowed' => [
|
||||
['id' => '', 'description' => 'All'],
|
||||
],
|
||||
'query' => ['type' => 'string'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\BcODataGateway;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorCacheControl;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorDetailService;
|
||||
use MintyPHP\Module\Helpdesk\Service\HelpdeskSettingsGateway;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
||||
|
||||
$request = requestInput();
|
||||
if ($request->method() !== 'GET') {
|
||||
http_response_code(405);
|
||||
Router::json(['ok' => false, 'error' => 'method_not_allowed']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$customerNo = trim((string) $request->query('customerNo', ''));
|
||||
$customerName = trim((string) $request->query('customerName', ''));
|
||||
$periodDays = (int) $request->query('periodDays', '90');
|
||||
$refreshRequested = DebitorCacheControl::parseRefreshFlag($request->query('refresh', ''));
|
||||
|
||||
if ($customerNo === '' || $customerName === '') {
|
||||
http_response_code(400);
|
||||
Router::json(['ok' => false, 'error' => 'missing_parameters']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$allowedPeriods = [30, 90, 180, 365];
|
||||
if (!in_array($periodDays, $allowedPeriods, true)) {
|
||||
$periodDays = 90;
|
||||
}
|
||||
|
||||
$sessionStore = app(SessionStoreInterface::class);
|
||||
$session = $sessionStore->all();
|
||||
$tenantScope = DebitorCacheControl::resolveTenantScope($session);
|
||||
if ($refreshRequested) {
|
||||
DebitorCacheControl::invalidateDebitorCaches($sessionStore, $tenantScope, $customerNo, true);
|
||||
}
|
||||
|
||||
// --- Load controlling tickets (period-independent cache: full 500 tickets) ---
|
||||
|
||||
$ticketsCacheKey = DebitorCacheControl::controllingTicketsKey($tenantScope, $customerNo);
|
||||
$cacheTtl = DebitorCacheControl::TTL_STANDARD_SECONDS;
|
||||
$cached = $sessionStore->get($ticketsCacheKey);
|
||||
$tickets = null;
|
||||
$ticketsCacheUsed = false;
|
||||
|
||||
if (!$refreshRequested && is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
|
||||
$tickets = is_array($cached['tickets'] ?? null) ? $cached['tickets'] : [];
|
||||
$ticketsCacheUsed = true;
|
||||
}
|
||||
|
||||
if ($tickets === null) {
|
||||
$gateway = app(BcODataGateway::class);
|
||||
|
||||
try {
|
||||
$tickets = $gateway->getTicketsForControlling($customerNo);
|
||||
} catch (\Throwable $e) {
|
||||
Router::json([
|
||||
'ok' => false,
|
||||
'error' => 'Failed to load controlling tickets: ' . $e->getMessage(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_array($tickets)) {
|
||||
$tickets = [];
|
||||
}
|
||||
|
||||
$sessionStore->set($ticketsCacheKey, [
|
||||
'tickets' => $tickets,
|
||||
'fetched_at' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
// --- Load contract data (reuse existing contracts cache from Sales tab) ---
|
||||
|
||||
// --- Load risk config ---
|
||||
|
||||
$settingsGateway = app(HelpdeskSettingsGateway::class);
|
||||
$riskConfig = $settingsGateway->getControllingRiskConfig();
|
||||
|
||||
// --- Build dashboard ---
|
||||
|
||||
$dashboard = DebitorDetailService::buildControllingDashboard(
|
||||
$tickets,
|
||||
$periodDays,
|
||||
$riskConfig
|
||||
);
|
||||
|
||||
Router::json([
|
||||
'ok' => true,
|
||||
'kpis' => $dashboard['kpis'],
|
||||
'trend' => $dashboard['trend'],
|
||||
'efficiency' => $dashboard['efficiency'],
|
||||
'risk_indicators' => $dashboard['risk_indicators'],
|
||||
'meta' => array_merge($dashboard['meta'], [
|
||||
'cache_used' => $ticketsCacheUsed,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => $refreshRequested,
|
||||
]),
|
||||
]);
|
||||
108
modules/helpdesk/pages/helpdesk/debitor-overview-data().php
Normal file
108
modules/helpdesk/pages/helpdesk/debitor-overview-data().php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorCacheControl;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorDetailService;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
||||
|
||||
gridRequireGetRequest();
|
||||
|
||||
$request = requestInput();
|
||||
$customerNo = trim((string) $request->query('customerNo', ''));
|
||||
$customerName = trim((string) $request->query('customerName', ''));
|
||||
$refreshRequested = DebitorCacheControl::parseRefreshFlag($request->query('refresh', ''));
|
||||
|
||||
if ($customerNo === '' || $customerName === '') {
|
||||
http_response_code(400);
|
||||
Router::json(['ok' => false, 'error' => 'Missing parameters']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sessionStore = app(SessionStoreInterface::class);
|
||||
$session = $sessionStore->all();
|
||||
$tenantScope = DebitorCacheControl::resolveTenantScope($session);
|
||||
if ($refreshRequested) {
|
||||
DebitorCacheControl::invalidateDebitorCaches($sessionStore, $tenantScope, $customerNo, true);
|
||||
}
|
||||
|
||||
$cacheKey = DebitorCacheControl::ticketsKey($tenantScope, $customerNo);
|
||||
$cacheTtl = DebitorCacheControl::TTL_STANDARD_SECONDS;
|
||||
$cached = $sessionStore->get($cacheKey);
|
||||
$tickets = null;
|
||||
$cacheUsed = false;
|
||||
|
||||
if (!$refreshRequested && is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
|
||||
$tickets = $cached['tickets'] ?? [];
|
||||
$cacheUsed = true;
|
||||
}
|
||||
|
||||
if ($tickets === null) {
|
||||
$service = app(DebitorDetailService::class);
|
||||
$ticketsResult = $service->loadTickets($customerNo, $customerName);
|
||||
|
||||
if (!($ticketsResult['ok'] ?? false)) {
|
||||
Router::json(['ok' => false, 'error' => $ticketsResult['error'] ?? 'Failed to load tickets']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$tickets = $ticketsResult['tickets'] ?? [];
|
||||
if (!is_array($tickets)) {
|
||||
$tickets = [];
|
||||
}
|
||||
|
||||
$sessionStore->set($cacheKey, [
|
||||
'tickets' => $tickets,
|
||||
'fetched_at' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!is_array($tickets)) {
|
||||
$tickets = [];
|
||||
}
|
||||
|
||||
$ticketSortStamp = static function (array $ticket): int {
|
||||
$activity = trim((string) ($ticket['Last_Activity_Date'] ?? ''));
|
||||
if ($activity !== '' && $activity !== '0001-01-01T00:00:00Z') {
|
||||
$activityTs = strtotime($activity);
|
||||
if (is_int($activityTs)) {
|
||||
return $activityTs;
|
||||
}
|
||||
}
|
||||
|
||||
$created = trim((string) ($ticket['Created_On'] ?? ''));
|
||||
if ($created !== '' && $created !== '0001-01-01T00:00:00Z') {
|
||||
$createdTs = strtotime($created);
|
||||
if (is_int($createdTs)) {
|
||||
return $createdTs;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
$recentActivity = array_values($tickets);
|
||||
usort($recentActivity, static function (array $a, array $b) use ($ticketSortStamp): int {
|
||||
$sa = $ticketSortStamp($a);
|
||||
$sb = $ticketSortStamp($b);
|
||||
|
||||
return $sb <=> $sa;
|
||||
});
|
||||
$recentActivity = array_slice($recentActivity, 0, 5);
|
||||
|
||||
Router::json([
|
||||
'ok' => true,
|
||||
'recent_activity' => $recentActivity,
|
||||
'meta' => [
|
||||
'total_tickets' => count($tickets),
|
||||
'cache_used' => $cacheUsed,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => $refreshRequested,
|
||||
],
|
||||
]);
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorCacheControl;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorDetailService;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
||||
|
||||
$request = requestInput();
|
||||
if ($request->method() !== 'GET') {
|
||||
http_response_code(405);
|
||||
Router::json(['ok' => false, 'error' => 'method_not_allowed']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$customerNo = trim((string) $request->query('customerNo', ''));
|
||||
$customerName = trim((string) $request->query('customerName', ''));
|
||||
$refreshRequested = DebitorCacheControl::parseRefreshFlag($request->query('refresh', ''));
|
||||
|
||||
if ($customerNo === '' || $customerName === '') {
|
||||
http_response_code(400);
|
||||
Router::json(['ok' => false, 'error' => 'missing_parameters']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sessionStore = app(SessionStoreInterface::class);
|
||||
$session = $sessionStore->all();
|
||||
$tenantScope = DebitorCacheControl::resolveTenantScope($session);
|
||||
if ($refreshRequested) {
|
||||
DebitorCacheControl::invalidateDebitorCaches($sessionStore, $tenantScope, $customerNo, true);
|
||||
}
|
||||
|
||||
$cacheKey = DebitorCacheControl::contractLinesKey($tenantScope, $customerNo);
|
||||
$cacheTtl = DebitorCacheControl::TTL_STANDARD_SECONDS;
|
||||
$cached = $sessionStore->get($cacheKey);
|
||||
$cacheUsed = false;
|
||||
|
||||
if (!$refreshRequested && is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
|
||||
$cacheUsed = true;
|
||||
$payload = is_array($cached['payload'] ?? null) ? $cached['payload'] : ['ok' => false];
|
||||
$payload['meta'] = [
|
||||
'cache_used' => true,
|
||||
'cache_bypassed' => false,
|
||||
'cache_refreshed' => false,
|
||||
];
|
||||
Router::json($payload);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$service = app(DebitorDetailService::class);
|
||||
$result = $service->loadSalesDashboard($customerNo, $customerName);
|
||||
|
||||
if (($result['ok'] ?? false) === true) {
|
||||
$sessionStore->set($cacheKey, [
|
||||
'payload' => $result,
|
||||
'fetched_at' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
$result['meta'] = [
|
||||
'cache_used' => $cacheUsed,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => $refreshRequested,
|
||||
];
|
||||
|
||||
Router::json($result);
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorCacheControl;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorDetailService;
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Router;
|
||||
@@ -14,6 +15,7 @@ gridRequireGetRequest();
|
||||
$request = requestInput();
|
||||
$customerNo = trim((string) $request->query('customerNo', ''));
|
||||
$customerName = trim((string) $request->query('customerName', ''));
|
||||
$refreshRequested = DebitorCacheControl::parseRefreshFlag($request->query('refresh', ''));
|
||||
|
||||
if ($customerNo === '' || $customerName === '') {
|
||||
http_response_code(400);
|
||||
@@ -25,18 +27,26 @@ if ($customerNo === '' || $customerName === '') {
|
||||
// Reuse session cache from tickets endpoint when available (avoids duplicate OData call)
|
||||
$sessionStore = app(SessionStoreInterface::class);
|
||||
$session = $sessionStore->all();
|
||||
$tenantId = (int) (($session['current_tenant']['id'] ?? 0));
|
||||
$tenantScope = $tenantId > 0 ? (string) $tenantId : 'global';
|
||||
$cacheKey = 'module.helpdesk.tickets_cache.' . $tenantScope . '.' . $customerNo;
|
||||
$cacheTtl = 120;
|
||||
$tenantScope = DebitorCacheControl::resolveTenantScope($session);
|
||||
if ($refreshRequested) {
|
||||
DebitorCacheControl::invalidateDebitorCaches($sessionStore, $tenantScope, $customerNo, true);
|
||||
}
|
||||
|
||||
$cacheKey = DebitorCacheControl::ticketsKey($tenantScope, $customerNo);
|
||||
$cacheTtl = DebitorCacheControl::TTL_STANDARD_SECONDS;
|
||||
$cached = $sessionStore->get($cacheKey);
|
||||
|
||||
if (is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
|
||||
if (!$refreshRequested && is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
|
||||
$cachedTickets = $cached['tickets'] ?? [];
|
||||
if (!is_array($cachedTickets)) {
|
||||
$cachedTickets = [];
|
||||
}
|
||||
$summary = DebitorDetailService::summarizeTickets($cachedTickets);
|
||||
$summary['meta'] = [
|
||||
'cache_used' => true,
|
||||
'cache_bypassed' => false,
|
||||
'cache_refreshed' => false,
|
||||
];
|
||||
Router::json($summary);
|
||||
|
||||
return;
|
||||
@@ -63,5 +73,10 @@ $sessionStore->set($cacheKey, [
|
||||
]);
|
||||
|
||||
$summary = DebitorDetailService::summarizeTickets($tickets);
|
||||
$summary['meta'] = [
|
||||
'cache_used' => false,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => $refreshRequested,
|
||||
];
|
||||
|
||||
Router::json($summary);
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorCacheControl;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorDetailService;
|
||||
use MintyPHP\Module\Helpdesk\Service\HelpdeskSettingsGateway;
|
||||
use MintyPHP\Module\Helpdesk\Service\SystemRecommendationEngine;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
||||
|
||||
gridRequireGetRequest();
|
||||
|
||||
$request = requestInput();
|
||||
$customerNo = trim((string) $request->query('customerNo', ''));
|
||||
$customerName = trim((string) $request->query('customerName', ''));
|
||||
$refreshRequested = DebitorCacheControl::parseRefreshFlag($request->query('refresh', ''));
|
||||
|
||||
if ($customerNo === '' || $customerName === '') {
|
||||
http_response_code(400);
|
||||
Router::json(['ok' => false, 'error' => 'Missing parameters']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sessionStore = app(SessionStoreInterface::class);
|
||||
$session = $sessionStore->all();
|
||||
$tenantScope = DebitorCacheControl::resolveTenantScope($session);
|
||||
if ($refreshRequested) {
|
||||
DebitorCacheControl::invalidateDebitorCaches($sessionStore, $tenantScope, $customerNo, true);
|
||||
}
|
||||
|
||||
$cacheKey = DebitorCacheControl::ticketsKey($tenantScope, $customerNo);
|
||||
$cacheTtl = DebitorCacheControl::TTL_STANDARD_SECONDS;
|
||||
$cached = $sessionStore->get($cacheKey);
|
||||
$tickets = null;
|
||||
$sourceCacheUsed = false;
|
||||
|
||||
if (!$refreshRequested && is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
|
||||
$tickets = $cached['tickets'] ?? [];
|
||||
$sourceCacheUsed = true;
|
||||
}
|
||||
|
||||
if ($tickets === null) {
|
||||
$service = app(DebitorDetailService::class);
|
||||
$ticketsResult = $service->loadTickets($customerNo, $customerName);
|
||||
|
||||
if (!($ticketsResult['ok'] ?? false)) {
|
||||
Router::json([
|
||||
'ok' => false,
|
||||
'error' => (string) ($ticketsResult['error'] ?? 'Failed to load tickets'),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$tickets = $ticketsResult['tickets'] ?? [];
|
||||
if (!is_array($tickets)) {
|
||||
$tickets = [];
|
||||
}
|
||||
|
||||
$sessionStore->set($cacheKey, [
|
||||
'tickets' => $tickets,
|
||||
'fetched_at' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!is_array($tickets)) {
|
||||
$tickets = [];
|
||||
}
|
||||
|
||||
$service = app(DebitorDetailService::class);
|
||||
$escalationCacheKey = DebitorCacheControl::escalationKey($tenantScope, $customerNo);
|
||||
$escalationCached = $sessionStore->get($escalationCacheKey);
|
||||
$escalationSourceCacheUsed = false;
|
||||
$escalationPayload = null;
|
||||
|
||||
if (!$refreshRequested && is_array($escalationCached) && isset($escalationCached['fetched_at']) && (time() - (int) $escalationCached['fetched_at']) < $cacheTtl) {
|
||||
$escalationPayload = [
|
||||
'ok' => (bool) ($escalationCached['ok'] ?? false),
|
||||
'overdue_count' => (int) ($escalationCached['overdue_count'] ?? 0),
|
||||
'entries' => is_array($escalationCached['entries'] ?? null) ? $escalationCached['entries'] : [],
|
||||
'matches' => is_array($escalationCached['matches'] ?? null) ? $escalationCached['matches'] : [],
|
||||
'tickets_considered' => (int) ($escalationCached['tickets_considered'] ?? 0),
|
||||
'tickets_matched' => (int) ($escalationCached['tickets_matched'] ?? 0),
|
||||
'definitions_count' => (int) ($escalationCached['definitions_count'] ?? 0),
|
||||
'error' => (string) ($escalationCached['error'] ?? ''),
|
||||
];
|
||||
$escalationSourceCacheUsed = true;
|
||||
}
|
||||
|
||||
if ($escalationPayload === null) {
|
||||
$escalationResult = $service->loadEscalationHealth($customerNo, $tickets);
|
||||
$escalationPayload = $escalationResult;
|
||||
|
||||
$sessionStore->set($escalationCacheKey, [
|
||||
'ok' => (bool) ($escalationResult['ok'] ?? false),
|
||||
'overdue_count' => (int) ($escalationResult['overdue_count'] ?? 0),
|
||||
'entries' => is_array($escalationResult['entries'] ?? null) ? $escalationResult['entries'] : [],
|
||||
'matches' => is_array($escalationResult['matches'] ?? null) ? $escalationResult['matches'] : [],
|
||||
'tickets_considered' => (int) ($escalationResult['tickets_considered'] ?? 0),
|
||||
'tickets_matched' => (int) ($escalationResult['tickets_matched'] ?? 0),
|
||||
'definitions_count' => (int) ($escalationResult['definitions_count'] ?? 0),
|
||||
'error' => (string) ($escalationResult['error'] ?? ''),
|
||||
'fetched_at' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
$dashboard = DebitorDetailService::buildSupportDashboard(
|
||||
$tickets,
|
||||
48,
|
||||
(($escalationPayload['ok'] ?? false) ? $escalationPayload : null)
|
||||
);
|
||||
$settingsGateway = app(HelpdeskSettingsGateway::class);
|
||||
$recommendationConfigEnvelope = $settingsGateway->getSystemRecommendationsConfigEnvelope();
|
||||
$recommendationConfig = is_array($recommendationConfigEnvelope['config'] ?? null)
|
||||
? $recommendationConfigEnvelope['config']
|
||||
: [];
|
||||
$recommendationEngine = app(SystemRecommendationEngine::class);
|
||||
$recommendationResult = $recommendationEngine->buildRecommendations(
|
||||
$tickets,
|
||||
(($escalationPayload['ok'] ?? false) ? $escalationPayload : null),
|
||||
$recommendationConfig
|
||||
);
|
||||
$recommendations = is_array($recommendationResult['recommendations'] ?? null)
|
||||
? $recommendationResult['recommendations']
|
||||
: [];
|
||||
$recommendationsMeta = is_array($recommendationResult['meta'] ?? null)
|
||||
? $recommendationResult['meta']
|
||||
: [];
|
||||
$meta = $dashboard['meta'];
|
||||
$meta['source_cache_used'] = $sourceCacheUsed;
|
||||
$meta['escalation_source_cache_used'] = $escalationSourceCacheUsed;
|
||||
$meta['cache_used'] = ($sourceCacheUsed || $escalationSourceCacheUsed);
|
||||
$meta['cache_bypassed'] = $refreshRequested;
|
||||
$meta['cache_refreshed'] = $refreshRequested;
|
||||
|
||||
$contractsCacheKey = DebitorCacheControl::contractsKey($tenantScope, $customerNo);
|
||||
$contractsCached = $sessionStore->get($contractsCacheKey);
|
||||
$contractsSourceCacheUsed = false;
|
||||
$contractsPayload = null;
|
||||
|
||||
if (!$refreshRequested && is_array($contractsCached) && isset($contractsCached['fetched_at']) && (time() - (int) $contractsCached['fetched_at']) < $cacheTtl) {
|
||||
$contractsPayload = [
|
||||
'ok' => true,
|
||||
'entries' => is_array($contractsCached['entries'] ?? null) ? $contractsCached['entries'] : [],
|
||||
'summary' => is_array($contractsCached['summary'] ?? null) ? $contractsCached['summary'] : [
|
||||
'total_contracts' => 0,
|
||||
'active_contracts' => 0,
|
||||
'product_types' => [],
|
||||
],
|
||||
];
|
||||
$contractsSourceCacheUsed = true;
|
||||
}
|
||||
|
||||
if ($contractsPayload === null) {
|
||||
$contractsResult = $service->loadContracts($customerNo);
|
||||
if ($contractsResult['ok'] ?? false) {
|
||||
$entries = is_array($contractsResult['entries'] ?? null) ? $contractsResult['entries'] : [];
|
||||
$summary = is_array($contractsResult['summary'] ?? null) ? $contractsResult['summary'] : [
|
||||
'total_contracts' => 0,
|
||||
'active_contracts' => 0,
|
||||
'product_types' => [],
|
||||
];
|
||||
|
||||
$contractsPayload = [
|
||||
'ok' => true,
|
||||
'entries' => $entries,
|
||||
'summary' => $summary,
|
||||
];
|
||||
|
||||
$sessionStore->set($contractsCacheKey, [
|
||||
'entries' => $entries,
|
||||
'summary' => $summary,
|
||||
'fetched_at' => time(),
|
||||
]);
|
||||
} else {
|
||||
$contractsPayload = [
|
||||
'ok' => false,
|
||||
'entries' => [],
|
||||
'summary' => [
|
||||
'total_contracts' => 0,
|
||||
'active_contracts' => 0,
|
||||
'product_types' => [],
|
||||
],
|
||||
'error' => (string) ($contractsResult['error'] ?? 'Failed to load contracts'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$meta['cache_used'] = ($sourceCacheUsed || $escalationSourceCacheUsed || $contractsSourceCacheUsed);
|
||||
|
||||
Router::json([
|
||||
'ok' => true,
|
||||
'health' => $dashboard['health'],
|
||||
'recommendations' => $recommendations,
|
||||
'recommendations_meta' => [
|
||||
'config_source' => (string) ($recommendationConfigEnvelope['source'] ?? 'default'),
|
||||
'applied_rules' => is_array($recommendationsMeta['applied_rules'] ?? null) ? $recommendationsMeta['applied_rules'] : [],
|
||||
'max_items' => (int) ($recommendationsMeta['max_items'] ?? (($recommendationConfig['max_items'] ?? 5))),
|
||||
'escalation_data_available' => (bool) ($recommendationsMeta['escalation_data_available'] ?? false),
|
||||
'cache_used' => ($sourceCacheUsed || $escalationSourceCacheUsed || $contractsSourceCacheUsed),
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => $refreshRequested,
|
||||
],
|
||||
'actions' => $recommendations,
|
||||
'meta' => $meta,
|
||||
'contracts' => $contractsPayload['entries'],
|
||||
'contracts_summary' => $contractsPayload['summary'],
|
||||
'contracts_meta' => [
|
||||
'available' => (bool) ($contractsPayload['ok'] ?? false),
|
||||
'source_cache_used' => $contractsSourceCacheUsed,
|
||||
'cache_used' => $contractsSourceCacheUsed,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => $refreshRequested,
|
||||
'error' => (string) ($contractsPayload['error'] ?? ''),
|
||||
],
|
||||
'escalation_entries' => is_array($escalationPayload['entries'] ?? null) ? $escalationPayload['entries'] : [],
|
||||
'escalation_meta' => [
|
||||
'available' => (bool) ($escalationPayload['ok'] ?? false),
|
||||
'source_cache_used' => $escalationSourceCacheUsed,
|
||||
'cache_used' => $escalationSourceCacheUsed,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => $refreshRequested,
|
||||
'error' => (string) ($escalationPayload['error'] ?? ''),
|
||||
'tickets_considered' => (int) ($escalationPayload['tickets_considered'] ?? 0),
|
||||
'tickets_matched' => (int) ($escalationPayload['tickets_matched'] ?? 0),
|
||||
'definitions_count' => (int) ($escalationPayload['definitions_count'] ?? 0),
|
||||
],
|
||||
]);
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorCacheControl;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorDetailService;
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Router;
|
||||
@@ -18,11 +19,17 @@ $filters = gridParseFilters(requestInput()->queryAll(), [
|
||||
|
||||
$customerNo = (string) ($filters['customerNo'] ?? '');
|
||||
$customerName = (string) ($filters['customerName'] ?? '');
|
||||
$refreshRequested = DebitorCacheControl::parseRefreshFlag(requestInput()->query('refresh', ''));
|
||||
|
||||
if ($customerNo === '' || $customerName === '') {
|
||||
Router::json([
|
||||
'ok' => false,
|
||||
'categories' => [],
|
||||
'meta' => [
|
||||
'cache_used' => false,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
return;
|
||||
@@ -30,15 +37,20 @@ if ($customerNo === '' || $customerName === '') {
|
||||
|
||||
$sessionStore = app(SessionStoreInterface::class);
|
||||
$session = $sessionStore->all();
|
||||
$tenantId = (int) (($session['current_tenant']['id'] ?? 0));
|
||||
$tenantScope = $tenantId > 0 ? (string) $tenantId : 'global';
|
||||
$cacheKey = 'module.helpdesk.tickets_cache.' . $tenantScope . '.' . $customerNo;
|
||||
$cacheTtl = 120;
|
||||
$tenantScope = DebitorCacheControl::resolveTenantScope($session);
|
||||
if ($refreshRequested) {
|
||||
DebitorCacheControl::invalidateDebitorCaches($sessionStore, $tenantScope, $customerNo, true);
|
||||
}
|
||||
|
||||
$cacheKey = DebitorCacheControl::ticketsKey($tenantScope, $customerNo);
|
||||
$cacheTtl = DebitorCacheControl::TTL_STANDARD_SECONDS;
|
||||
$cached = $sessionStore->get($cacheKey);
|
||||
$allTickets = null;
|
||||
$cacheUsed = false;
|
||||
|
||||
if (is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
|
||||
if (!$refreshRequested && is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
|
||||
$allTickets = $cached['tickets'] ?? [];
|
||||
$cacheUsed = true;
|
||||
}
|
||||
|
||||
if ($allTickets === null) {
|
||||
@@ -49,6 +61,11 @@ if ($allTickets === null) {
|
||||
Router::json([
|
||||
'ok' => false,
|
||||
'categories' => [],
|
||||
'meta' => [
|
||||
'cache_used' => $cacheUsed,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => $refreshRequested,
|
||||
],
|
||||
]);
|
||||
|
||||
return;
|
||||
@@ -80,4 +97,9 @@ usort($categoryValues, 'strnatcasecmp');
|
||||
Router::json([
|
||||
'ok' => true,
|
||||
'categories' => array_values($categoryValues),
|
||||
'meta' => [
|
||||
'cache_used' => $cacheUsed,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => $refreshRequested,
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorCacheControl;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorDetailService;
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
@@ -11,12 +13,22 @@ Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
||||
gridRequireGetRequest();
|
||||
|
||||
$filters = gridParseFiltersFromSchemaFile(__DIR__ . '/debitor-tickets-filter-schema.php');
|
||||
$request = requestInput();
|
||||
|
||||
$customerNo = (string) ($filters['customerNo'] ?? '');
|
||||
$customerName = (string) ($filters['customerName'] ?? '');
|
||||
$refreshRequested = DebitorCacheControl::parseRefreshFlag($request->query('refresh', ''));
|
||||
|
||||
if ($customerNo === '' || $customerName === '') {
|
||||
gridJsonDataResult([], 0);
|
||||
Router::json([
|
||||
'data' => [],
|
||||
'total' => 0,
|
||||
'meta' => [
|
||||
'cache_used' => false,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -25,15 +37,20 @@ if ($customerNo === '' || $customerName === '') {
|
||||
|
||||
$sessionStore = app(SessionStoreInterface::class);
|
||||
$session = $sessionStore->all();
|
||||
$tenantId = (int) (($session['current_tenant']['id'] ?? 0));
|
||||
$tenantScope = $tenantId > 0 ? (string) $tenantId : 'global';
|
||||
$cacheKey = 'module.helpdesk.tickets_cache.' . $tenantScope . '.' . $customerNo;
|
||||
$cacheTtl = 120;
|
||||
$tenantScope = DebitorCacheControl::resolveTenantScope($session);
|
||||
if ($refreshRequested) {
|
||||
DebitorCacheControl::invalidateDebitorCaches($sessionStore, $tenantScope, $customerNo, true);
|
||||
}
|
||||
|
||||
$cacheKey = DebitorCacheControl::ticketsKey($tenantScope, $customerNo);
|
||||
$cacheTtl = DebitorCacheControl::TTL_STANDARD_SECONDS;
|
||||
$cached = $sessionStore->get($cacheKey);
|
||||
$allTickets = null;
|
||||
$cacheUsed = false;
|
||||
|
||||
if (is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
|
||||
if (!$refreshRequested && is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) {
|
||||
$allTickets = $cached['tickets'] ?? [];
|
||||
$cacheUsed = true;
|
||||
}
|
||||
|
||||
if ($allTickets === null) {
|
||||
@@ -41,7 +58,15 @@ if ($allTickets === null) {
|
||||
$result = $service->loadTickets($customerNo, $customerName);
|
||||
|
||||
if (!($result['ok'] ?? false)) {
|
||||
gridJsonDataResult([], 0);
|
||||
Router::json([
|
||||
'data' => [],
|
||||
'total' => 0,
|
||||
'meta' => [
|
||||
'cache_used' => $cacheUsed,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => $refreshRequested,
|
||||
],
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -176,4 +201,12 @@ foreach ($rows as $ticket) {
|
||||
];
|
||||
}
|
||||
|
||||
gridJsonDataResult($preparedRows, $total);
|
||||
Router::json([
|
||||
'data' => array_values($preparedRows),
|
||||
'total' => max(0, $total),
|
||||
'meta' => [
|
||||
'cache_used' => $cacheUsed,
|
||||
'cache_bypassed' => $refreshRequested,
|
||||
'cache_refreshed' => $refreshRequested,
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -33,6 +33,7 @@ if ($request->isMethod('POST')) {
|
||||
$oauthClientId = trim((string) ($post['oauth_client_id'] ?? ''));
|
||||
$oauthClientSecret = trim((string) ($post['oauth_client_secret'] ?? ''));
|
||||
$oauthTokenEndpoint = trim((string) ($post['oauth_token_endpoint'] ?? ''));
|
||||
$recommendationsMaxItems = (int) ($post['recommendations_max_items'] ?? 5);
|
||||
|
||||
$errorBag = formErrors();
|
||||
|
||||
@@ -65,11 +66,66 @@ if ($request->isMethod('POST')) {
|
||||
$settingsGateway->setOAuthTokenEndpoint($oauthTokenEndpoint);
|
||||
}
|
||||
|
||||
$settingsGateway->setSystemRecommendationsConfig([
|
||||
'version' => 1,
|
||||
'max_items' => $recommendationsMaxItems,
|
||||
'rules' => [
|
||||
'escalation_overdue' => [
|
||||
'enabled' => isset($post['recommendations_escalation_overdue_enabled']),
|
||||
'priority' => (int) ($post['recommendations_escalation_overdue_priority'] ?? 100),
|
||||
'min_overdue_minutes' => (int) ($post['recommendations_escalation_overdue_min_overdue_minutes'] ?? 0),
|
||||
],
|
||||
'high_risk_aging' => [
|
||||
'enabled' => isset($post['recommendations_high_risk_aging_enabled']),
|
||||
'priority' => (int) ($post['recommendations_high_risk_aging_priority'] ?? 90),
|
||||
'codes' => trim((string) ($post['recommendations_high_risk_aging_codes'] ?? 'MAX,HOCH')),
|
||||
'min_age_hours' => (int) ($post['recommendations_high_risk_aging_min_age_hours'] ?? 8),
|
||||
],
|
||||
'unassigned_open' => [
|
||||
'enabled' => isset($post['recommendations_unassigned_open_enabled']),
|
||||
'priority' => (int) ($post['recommendations_unassigned_open_priority'] ?? 80),
|
||||
'min_age_hours' => (int) ($post['recommendations_unassigned_open_min_age_hours'] ?? 2),
|
||||
],
|
||||
'stale_open' => [
|
||||
'enabled' => isset($post['recommendations_stale_open_enabled']),
|
||||
'priority' => (int) ($post['recommendations_stale_open_priority'] ?? 70),
|
||||
'min_age_hours' => (int) ($post['recommendations_stale_open_min_age_hours'] ?? 48),
|
||||
],
|
||||
'customer_backlog' => [
|
||||
'enabled' => isset($post['recommendations_customer_backlog_enabled']),
|
||||
'priority' => (int) ($post['recommendations_customer_backlog_priority'] ?? 60),
|
||||
'min_open_tickets' => (int) ($post['recommendations_customer_backlog_min_open_tickets'] ?? 6),
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$settingsGateway->setControllingRiskConfig([
|
||||
'version' => 1,
|
||||
'rules' => [
|
||||
'ticket_volume_increase' => [
|
||||
'enabled' => isset($post['controlling_ticket_volume_increase_enabled']),
|
||||
'threshold_percent' => (int) ($post['controlling_ticket_volume_increase_threshold'] ?? 30),
|
||||
],
|
||||
'avg_resolution_high' => [
|
||||
'enabled' => isset($post['controlling_avg_resolution_high_enabled']),
|
||||
'threshold_hours' => (int) ($post['controlling_avg_resolution_high_threshold'] ?? 72),
|
||||
],
|
||||
'open_aging' => [
|
||||
'enabled' => isset($post['controlling_open_aging_enabled']),
|
||||
'threshold_hours' => (int) ($post['controlling_open_aging_threshold'] ?? 168),
|
||||
],
|
||||
'unassigned_ratio' => [
|
||||
'enabled' => isset($post['controlling_unassigned_ratio_enabled']),
|
||||
'threshold_percent' => (int) ($post['controlling_unassigned_ratio_threshold'] ?? 20),
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$action = trim((string) ($post['action'] ?? 'save'));
|
||||
Flash::success(t('Settings saved'), 'helpdesk/settings', 'settings_saved');
|
||||
|
||||
if ($action === 'save_close') {
|
||||
Router::redirect('helpdesk/debitor');
|
||||
Router::redirect('helpdesk');
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -93,6 +149,17 @@ $oauthClientId = $settingsGateway->getOAuthClientId() ?? '';
|
||||
$hasOAuthClientSecret = $settingsGateway->getOAuthClientSecret() !== null;
|
||||
$oauthTokenEndpoint = $settingsGateway->getOAuthTokenEndpoint() ?? '';
|
||||
$configErrors = $settingsGateway->validateConfiguration();
|
||||
$recommendationsConfigEnvelope = $settingsGateway->getSystemRecommendationsConfigEnvelope();
|
||||
$recommendationsConfig = is_array($recommendationsConfigEnvelope['config'] ?? null)
|
||||
? $recommendationsConfigEnvelope['config']
|
||||
: [];
|
||||
$recommendationsConfigSource = (string) ($recommendationsConfigEnvelope['source'] ?? 'default');
|
||||
|
||||
$controllingConfigEnvelope = $settingsGateway->getControllingRiskConfigEnvelope();
|
||||
$controllingConfig = is_array($controllingConfigEnvelope['config'] ?? null)
|
||||
? $controllingConfigEnvelope['config']
|
||||
: [];
|
||||
$controllingConfigSource = (string) ($controllingConfigEnvelope['source'] ?? 'default');
|
||||
|
||||
Buffer::set('title', t('Helpdesk settings'));
|
||||
Buffer::set('style_groups', json_encode(['helpdesk']));
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
* @var bool $hasOAuthClientSecret
|
||||
* @var string $oauthTokenEndpoint
|
||||
* @var array $configErrors
|
||||
* @var array $recommendationsConfig
|
||||
* @var string $recommendationsConfigSource
|
||||
* @var array $controllingConfig
|
||||
* @var string $controllingConfigSource
|
||||
*/
|
||||
|
||||
use MintyPHP\Module\Helpdesk\Service\HelpdeskSettingsGateway;
|
||||
@@ -26,23 +30,38 @@ $oauthClientId = $oauthClientId ?? '';
|
||||
$hasOAuthClientSecret = $hasOAuthClientSecret ?? false;
|
||||
$oauthTokenEndpoint = $oauthTokenEndpoint ?? '';
|
||||
$configErrors = is_array($configErrors ?? null) ? $configErrors : [];
|
||||
|
||||
$recommendationsConfig = is_array($recommendationsConfig ?? null) ? $recommendationsConfig : [];
|
||||
$recommendationsConfigSource = (string) ($recommendationsConfigSource ?? 'default');
|
||||
$recommendationsMaxItems = (int) ($recommendationsConfig['max_items'] ?? 5);
|
||||
$recommendationRules = is_array($recommendationsConfig['rules'] ?? null) ? $recommendationsConfig['rules'] : [];
|
||||
$ruleEscalationOverdue = is_array($recommendationRules['escalation_overdue'] ?? null) ? $recommendationRules['escalation_overdue'] : [];
|
||||
$ruleHighRiskAging = is_array($recommendationRules['high_risk_aging'] ?? null) ? $recommendationRules['high_risk_aging'] : [];
|
||||
$ruleUnassignedOpen = is_array($recommendationRules['unassigned_open'] ?? null) ? $recommendationRules['unassigned_open'] : [];
|
||||
$ruleStaleOpen = is_array($recommendationRules['stale_open'] ?? null) ? $recommendationRules['stale_open'] : [];
|
||||
$ruleCustomerBacklog = is_array($recommendationRules['customer_backlog'] ?? null) ? $recommendationRules['customer_backlog'] : [];
|
||||
$highRiskCodes = is_array($ruleHighRiskAging['codes'] ?? null) ? implode(',', $ruleHighRiskAging['codes']) : 'MAX,HOCH';
|
||||
$controllingConfig = is_array($controllingConfig ?? null) ? $controllingConfig : [];
|
||||
$controllingConfigSource = (string) ($controllingConfigSource ?? 'default');
|
||||
$controllingRules = is_array($controllingConfig['rules'] ?? null) ? $controllingConfig['rules'] : [];
|
||||
$ctrlTicketVolume = is_array($controllingRules['ticket_volume_increase'] ?? null) ? $controllingRules['ticket_volume_increase'] : [];
|
||||
$ctrlAvgResolution = is_array($controllingRules['avg_resolution_high'] ?? null) ? $controllingRules['avg_resolution_high'] : [];
|
||||
$ctrlOpenAging = is_array($controllingRules['open_aging'] ?? null) ? $controllingRules['open_aging'] : [];
|
||||
$ctrlUnassigned = is_array($controllingRules['unassigned_ratio'] ?? null) ? $controllingRules['unassigned_ratio'] : [];
|
||||
$isOAuth2 = $authMode === HelpdeskSettingsGateway::AUTH_MODE_OAUTH2;
|
||||
?>
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
<?php
|
||||
$breadcrumbs = [
|
||||
['label' => t('Home'), 'path' => 'admin'],
|
||||
['label' => t('Helpdesk'), 'path' => 'helpdesk/debitor'],
|
||||
['label' => t('Helpdesk'), 'path' => 'helpdesk'],
|
||||
['label' => t('Settings')],
|
||||
];
|
||||
require templatePath('partials/app-breadcrumb.phtml');
|
||||
?>
|
||||
|
||||
<?php
|
||||
$titlebar = [
|
||||
'title' => t('Helpdesk settings'),
|
||||
'backHref' => lurl('helpdesk/debitor'),
|
||||
'backHref' => lurl('helpdesk'),
|
||||
'actions' => [
|
||||
[
|
||||
'label' => t('Save'),
|
||||
@@ -50,7 +69,9 @@ $configErrors = is_array($configErrors ?? null) ? $configErrors : [];
|
||||
'form' => 'helpdesk-settings-form',
|
||||
'name' => 'action',
|
||||
'value' => 'save',
|
||||
'class' => 'secondary outline',
|
||||
'class' => 'primary',
|
||||
'detailSavePrimary' => true,
|
||||
'detailActionKind' => 'save',
|
||||
],
|
||||
[
|
||||
'label' => t('Save & close'),
|
||||
@@ -58,15 +79,14 @@ $configErrors = is_array($configErrors ?? null) ? $configErrors : [];
|
||||
'form' => 'helpdesk-settings-form',
|
||||
'name' => 'action',
|
||||
'value' => 'save_close',
|
||||
'class' => 'primary',
|
||||
'class' => 'secondary outline',
|
||||
],
|
||||
],
|
||||
];
|
||||
require templatePath('partials/app-details-titlebar.phtml');
|
||||
require templatePath('partials/app-flash.phtml');
|
||||
?>
|
||||
|
||||
<?php require templatePath('partials/app-flash.phtml'); ?>
|
||||
|
||||
<?php if ($configErrors !== []): ?>
|
||||
<div class="notice" data-variant="warning" role="alert">
|
||||
<strong><?php e(t('Configuration incomplete')); ?>:</strong>
|
||||
@@ -78,118 +98,388 @@ $configErrors = is_array($configErrors ?? null) ? $configErrors : [];
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form id="helpdesk-settings-form" method="post" action="<?php e(lurl('helpdesk/settings')); ?>">
|
||||
<?php if ($recommendationsConfigSource === 'fallback_invalid_json'): ?>
|
||||
<div class="notice" data-variant="warning" role="alert">
|
||||
<p><?php e(t('System recommendation configuration was invalid. Defaults were applied.')); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($controllingConfigSource === 'fallback_invalid_json'): ?>
|
||||
<div class="notice" data-variant="warning" role="alert">
|
||||
<p><?php e(t('Controlling risk configuration was invalid. Defaults were applied.')); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form
|
||||
id="helpdesk-settings-form"
|
||||
method="post"
|
||||
action="<?php e(lurl('helpdesk/settings')); ?>"
|
||||
data-standard-detail-form="1"
|
||||
data-details-storage="helpdesk-settings-main-details-v1"
|
||||
data-test-success-message="<?php e(t('Connection successful')); ?>"
|
||||
data-test-error-message="<?php e(t('Connection failed')); ?>"
|
||||
data-test-loading-label="<?php e(t('Testing...')); ?>"
|
||||
data-test-default-label="<?php e(t('Test connection')); ?>"
|
||||
>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
|
||||
<fieldset>
|
||||
<legend><?php e(t('OData connection')); ?></legend>
|
||||
<div class="app-tabs" data-tabs data-app-component="tabs" data-tabs-param="tab" data-tabs-storage-key="helpdesk-settings-tabs-v1">
|
||||
<div class="app-tabs-nav">
|
||||
<button type="button" data-tab="connection" data-tab-default><?php e(t('Connection')); ?></button>
|
||||
<button type="button" data-tab="auth"><?php e(t('Authentication')); ?></button>
|
||||
<button type="button" data-tab="recommendations"><?php e(t('System recommendations')); ?></button>
|
||||
<button type="button" data-tab="controlling"><?php e(t('Controlling')); ?></button>
|
||||
</div>
|
||||
|
||||
<label for="odata_base_url"><?php e(t('OData Base URL')); ?></label>
|
||||
<input
|
||||
type="url"
|
||||
id="odata_base_url"
|
||||
name="odata_base_url"
|
||||
value="<?php e($odataBaseUrl); ?>"
|
||||
placeholder="https://bc.example.com:7048/BusinessCentral/ODataV4"
|
||||
>
|
||||
<small><?php e(t('Base URL without Company path segment')); ?></small>
|
||||
<div data-tab-panel="connection" class="helpdesk-settings-panel">
|
||||
<details class="app-details-card" name="helpdesk-odata" data-details-key="helpdesk-settings-odata" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('OData connection')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<label class="app-field" for="odata_base_url">
|
||||
<span><?php e(t('OData Base URL')); ?></span>
|
||||
<input
|
||||
type="url"
|
||||
id="odata_base_url"
|
||||
name="odata_base_url"
|
||||
value="<?php e($odataBaseUrl); ?>"
|
||||
placeholder="https://bc.example.com:7048/BusinessCentral/ODataV4"
|
||||
>
|
||||
<small><?php e(t('Base URL without Company path segment')); ?></small>
|
||||
</label>
|
||||
|
||||
<label for="company_name"><?php e(t('Company name')); ?></label>
|
||||
<input
|
||||
type="text"
|
||||
id="company_name"
|
||||
name="company_name"
|
||||
value="<?php e($companyName); ?>"
|
||||
placeholder="<?php e(HelpdeskSettingsGateway::DEFAULT_COMPANY_NAME); ?>"
|
||||
>
|
||||
<small><?php e(t('BC company name (URL-encoded automatically)')); ?></small>
|
||||
</fieldset>
|
||||
<label class="app-field" for="company_name">
|
||||
<span><?php e(t('Company name')); ?></span>
|
||||
<input
|
||||
type="text"
|
||||
id="company_name"
|
||||
name="company_name"
|
||||
value="<?php e($companyName); ?>"
|
||||
placeholder="<?php e(HelpdeskSettingsGateway::DEFAULT_COMPANY_NAME); ?>"
|
||||
>
|
||||
<small><?php e(t('BC company name (URL-encoded automatically)')); ?></small>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<fieldset>
|
||||
<legend><?php e(t('Authentication')); ?></legend>
|
||||
<details class="app-details-card" name="helpdesk-connection-test" data-details-key="helpdesk-settings-connection-test" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Connection test')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<button
|
||||
type="button"
|
||||
id="helpdesk-test-connection-button"
|
||||
class="secondary outline"
|
||||
data-label-default="<?php e(t('Test connection')); ?>"
|
||||
data-label-loading="<?php e(t('Testing...')); ?>"
|
||||
>
|
||||
<?php e(t('Test connection')); ?>
|
||||
</button>
|
||||
<div id="helpdesk-connection-test-result" class="notice helpdesk-settings-test-result" data-variant="info" role="status" aria-live="polite" hidden></div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<label for="auth_mode"><?php e(t('Auth mode')); ?></label>
|
||||
<select id="auth_mode" name="auth_mode">
|
||||
<option value="<?php e(HelpdeskSettingsGateway::AUTH_MODE_BASIC); ?>" <?php if ($authMode === HelpdeskSettingsGateway::AUTH_MODE_BASIC): ?>selected<?php endif; ?>>
|
||||
<?php e(t('Basic Auth')); ?>
|
||||
</option>
|
||||
<option value="<?php e(HelpdeskSettingsGateway::AUTH_MODE_OAUTH2); ?>" <?php if ($authMode === HelpdeskSettingsGateway::AUTH_MODE_OAUTH2): ?>selected<?php endif; ?>>
|
||||
<?php e(t('OAuth2 (client credentials)')); ?>
|
||||
</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
<div data-tab-panel="auth" class="helpdesk-settings-panel">
|
||||
<details class="app-details-card" name="helpdesk-auth-mode" data-details-key="helpdesk-settings-auth-mode" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Authentication')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<label class="app-field" for="auth_mode">
|
||||
<span><?php e(t('Auth mode')); ?></span>
|
||||
<select id="auth_mode" name="auth_mode">
|
||||
<option value="<?php e(HelpdeskSettingsGateway::AUTH_MODE_BASIC); ?>" <?php if ($authMode === HelpdeskSettingsGateway::AUTH_MODE_BASIC): ?>selected<?php endif; ?>>
|
||||
<?php e(t('Basic Auth')); ?>
|
||||
</option>
|
||||
<option value="<?php e(HelpdeskSettingsGateway::AUTH_MODE_OAUTH2); ?>" <?php if ($authMode === HelpdeskSettingsGateway::AUTH_MODE_OAUTH2): ?>selected<?php endif; ?>>
|
||||
<?php e(t('OAuth2 (client credentials)')); ?>
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<fieldset id="helpdesk-basic-auth-fields">
|
||||
<legend><?php e(t('Basic Auth credentials')); ?></legend>
|
||||
<details id="helpdesk-basic-auth-fields" class="app-details-card" name="helpdesk-basic-auth" data-details-key="helpdesk-settings-basic-auth" <?php if ($isOAuth2): ?>hidden<?php else: ?>open<?php endif; ?>>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Basic Auth credentials')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container helpdesk-settings-rule-grid">
|
||||
<label class="app-field" for="basic_user">
|
||||
<span><?php e(t('Username')); ?></span>
|
||||
<input
|
||||
type="text"
|
||||
id="basic_user"
|
||||
name="basic_user"
|
||||
value="<?php e($basicUser); ?>"
|
||||
autocomplete="off"
|
||||
>
|
||||
</label>
|
||||
|
||||
<label for="basic_user"><?php e(t('Username')); ?></label>
|
||||
<input
|
||||
type="text"
|
||||
id="basic_user"
|
||||
name="basic_user"
|
||||
value="<?php e($basicUser); ?>"
|
||||
autocomplete="off"
|
||||
>
|
||||
<label class="app-field" for="basic_password">
|
||||
<span><?php e(t('Password')); ?></span>
|
||||
<input
|
||||
type="password"
|
||||
id="basic_password"
|
||||
name="basic_password"
|
||||
value="<?php if ($hasBasicPassword): ?>********<?php endif; ?>"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<?php if ($hasBasicPassword): ?>
|
||||
<small><?php e(t('Leave unchanged to keep current password')); ?></small>
|
||||
<?php endif; ?>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<label for="basic_password"><?php e(t('Password')); ?></label>
|
||||
<input
|
||||
type="password"
|
||||
id="basic_password"
|
||||
name="basic_password"
|
||||
value="<?php if ($hasBasicPassword): ?>********<?php endif; ?>"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<?php if ($hasBasicPassword): ?>
|
||||
<small><?php e(t('Leave unchanged to keep current password')); ?></small>
|
||||
<?php endif; ?>
|
||||
</fieldset>
|
||||
<details id="helpdesk-oauth2-fields" class="app-details-card" name="helpdesk-oauth2-auth" data-details-key="helpdesk-settings-oauth2-auth" <?php if (!$isOAuth2): ?>hidden<?php else: ?>open<?php endif; ?>>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('OAuth2 credentials')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container helpdesk-settings-rule-grid">
|
||||
<label class="app-field" for="oauth_tenant_id">
|
||||
<span><?php e(t('Azure Tenant ID')); ?></span>
|
||||
<input
|
||||
type="text"
|
||||
id="oauth_tenant_id"
|
||||
name="oauth_tenant_id"
|
||||
value="<?php e($oauthTenantId); ?>"
|
||||
autocomplete="off"
|
||||
>
|
||||
</label>
|
||||
|
||||
<fieldset id="helpdesk-oauth2-fields">
|
||||
<legend><?php e(t('OAuth2 credentials')); ?></legend>
|
||||
<label class="app-field" for="oauth_client_id">
|
||||
<span><?php e(t('Client ID')); ?></span>
|
||||
<input
|
||||
type="text"
|
||||
id="oauth_client_id"
|
||||
name="oauth_client_id"
|
||||
value="<?php e($oauthClientId); ?>"
|
||||
autocomplete="off"
|
||||
>
|
||||
</label>
|
||||
|
||||
<label for="oauth_tenant_id"><?php e(t('Azure Tenant ID')); ?></label>
|
||||
<input
|
||||
type="text"
|
||||
id="oauth_tenant_id"
|
||||
name="oauth_tenant_id"
|
||||
value="<?php e($oauthTenantId); ?>"
|
||||
autocomplete="off"
|
||||
>
|
||||
<label class="app-field" for="oauth_client_secret">
|
||||
<span><?php e(t('Client Secret')); ?></span>
|
||||
<input
|
||||
type="password"
|
||||
id="oauth_client_secret"
|
||||
name="oauth_client_secret"
|
||||
value="<?php if ($hasOAuthClientSecret): ?>********<?php endif; ?>"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<?php if ($hasOAuthClientSecret): ?>
|
||||
<small><?php e(t('Leave unchanged to keep current password')); ?></small>
|
||||
<?php endif; ?>
|
||||
</label>
|
||||
|
||||
<label for="oauth_client_id"><?php e(t('Client ID')); ?></label>
|
||||
<input
|
||||
type="text"
|
||||
id="oauth_client_id"
|
||||
name="oauth_client_id"
|
||||
value="<?php e($oauthClientId); ?>"
|
||||
autocomplete="off"
|
||||
>
|
||||
<label class="app-field" for="oauth_token_endpoint">
|
||||
<span><?php e(t('Token endpoint URL')); ?></span>
|
||||
<input
|
||||
type="url"
|
||||
id="oauth_token_endpoint"
|
||||
name="oauth_token_endpoint"
|
||||
value="<?php e($oauthTokenEndpoint); ?>"
|
||||
placeholder="https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<label for="oauth_client_secret"><?php e(t('Client Secret')); ?></label>
|
||||
<input
|
||||
type="password"
|
||||
id="oauth_client_secret"
|
||||
name="oauth_client_secret"
|
||||
value="<?php if ($hasOAuthClientSecret): ?>********<?php endif; ?>"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<div data-tab-panel="recommendations" class="helpdesk-settings-panel">
|
||||
<details class="app-details-card" name="helpdesk-recommendations-limits" data-details-key="helpdesk-settings-recommendations-limits" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Maximum recommendations')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<label class="app-field" for="recommendations_max_items">
|
||||
<span><?php e(t('Maximum recommendations')); ?></span>
|
||||
<input
|
||||
type="number"
|
||||
id="recommendations_max_items"
|
||||
name="recommendations_max_items"
|
||||
min="1"
|
||||
max="20"
|
||||
value="<?php e($recommendationsMaxItems); ?>"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<label for="oauth_token_endpoint"><?php e(t('Token endpoint URL')); ?></label>
|
||||
<input
|
||||
type="url"
|
||||
id="oauth_token_endpoint"
|
||||
name="oauth_token_endpoint"
|
||||
value="<?php e($oauthTokenEndpoint); ?>"
|
||||
placeholder="https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"
|
||||
>
|
||||
</fieldset>
|
||||
<details class="app-details-card" name="helpdesk-rule-escalation-overdue" data-details-key="helpdesk-settings-rule-escalation-overdue" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Escalation overdue')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container helpdesk-settings-rule-grid">
|
||||
<label class="app-field">
|
||||
<input type="checkbox" role="switch" name="recommendations_escalation_overdue_enabled" <?php if (($ruleEscalationOverdue['enabled'] ?? true) === true): ?>checked<?php endif; ?>>
|
||||
<span><?php e(t('Enabled')); ?></span>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Priority')); ?></span>
|
||||
<input type="number" name="recommendations_escalation_overdue_priority" min="1" max="999" value="<?php e((int) ($ruleEscalationOverdue['priority'] ?? 100)); ?>">
|
||||
</label>
|
||||
<label class="app-field helpdesk-settings-rule-grid-full">
|
||||
<span><?php e(t('Minimum overdue (minutes)')); ?></span>
|
||||
<input type="number" name="recommendations_escalation_overdue_min_overdue_minutes" min="0" max="43200" value="<?php e((int) ($ruleEscalationOverdue['min_overdue_minutes'] ?? 0)); ?>">
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<fieldset>
|
||||
<legend><?php e(t('Connection test')); ?></legend>
|
||||
<button type="button" id="helpdesk-test-connection-button" class="secondary outline">
|
||||
<?php e(t('Test connection')); ?>
|
||||
</button>
|
||||
</fieldset>
|
||||
<details class="app-details-card" name="helpdesk-rule-high-risk" data-details-key="helpdesk-settings-rule-high-risk" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('High-risk aging')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container helpdesk-settings-rule-grid">
|
||||
<label class="app-field">
|
||||
<input type="checkbox" role="switch" name="recommendations_high_risk_aging_enabled" <?php if (($ruleHighRiskAging['enabled'] ?? true) === true): ?>checked<?php endif; ?>>
|
||||
<span><?php e(t('Enabled')); ?></span>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Priority')); ?></span>
|
||||
<input type="number" name="recommendations_high_risk_aging_priority" min="1" max="999" value="<?php e((int) ($ruleHighRiskAging['priority'] ?? 90)); ?>">
|
||||
</label>
|
||||
<label class="app-field helpdesk-settings-rule-grid-full">
|
||||
<span><?php e(t('High-risk codes (comma separated)')); ?></span>
|
||||
<input type="text" name="recommendations_high_risk_aging_codes" value="<?php e($highRiskCodes); ?>">
|
||||
</label>
|
||||
<label class="app-field helpdesk-settings-rule-grid-full">
|
||||
<span><?php e(t('Minimum age (hours)')); ?></span>
|
||||
<input type="number" name="recommendations_high_risk_aging_min_age_hours" min="0" max="720" value="<?php e((int) ($ruleHighRiskAging['min_age_hours'] ?? 8)); ?>">
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="app-details-card" name="helpdesk-rule-unassigned-open" data-details-key="helpdesk-settings-rule-unassigned-open" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Unassigned open')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container helpdesk-settings-rule-grid">
|
||||
<label class="app-field">
|
||||
<input type="checkbox" role="switch" name="recommendations_unassigned_open_enabled" <?php if (($ruleUnassignedOpen['enabled'] ?? true) === true): ?>checked<?php endif; ?>>
|
||||
<span><?php e(t('Enabled')); ?></span>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Priority')); ?></span>
|
||||
<input type="number" name="recommendations_unassigned_open_priority" min="1" max="999" value="<?php e((int) ($ruleUnassignedOpen['priority'] ?? 80)); ?>">
|
||||
</label>
|
||||
<label class="app-field helpdesk-settings-rule-grid-full">
|
||||
<span><?php e(t('Minimum age (hours)')); ?></span>
|
||||
<input type="number" name="recommendations_unassigned_open_min_age_hours" min="0" max="720" value="<?php e((int) ($ruleUnassignedOpen['min_age_hours'] ?? 2)); ?>">
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="app-details-card" name="helpdesk-rule-stale-open" data-details-key="helpdesk-settings-rule-stale-open" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Stale open')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container helpdesk-settings-rule-grid">
|
||||
<label class="app-field">
|
||||
<input type="checkbox" role="switch" name="recommendations_stale_open_enabled" <?php if (($ruleStaleOpen['enabled'] ?? true) === true): ?>checked<?php endif; ?>>
|
||||
<span><?php e(t('Enabled')); ?></span>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Priority')); ?></span>
|
||||
<input type="number" name="recommendations_stale_open_priority" min="1" max="999" value="<?php e((int) ($ruleStaleOpen['priority'] ?? 70)); ?>">
|
||||
</label>
|
||||
<label class="app-field helpdesk-settings-rule-grid-full">
|
||||
<span><?php e(t('Minimum age (hours)')); ?></span>
|
||||
<input type="number" name="recommendations_stale_open_min_age_hours" min="0" max="1440" value="<?php e((int) ($ruleStaleOpen['min_age_hours'] ?? 48)); ?>">
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="app-details-card" name="helpdesk-rule-customer-backlog" data-details-key="helpdesk-settings-rule-customer-backlog" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Customer backlog')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container helpdesk-settings-rule-grid">
|
||||
<label class="app-field">
|
||||
<input type="checkbox" role="switch" name="recommendations_customer_backlog_enabled" <?php if (($ruleCustomerBacklog['enabled'] ?? true) === true): ?>checked<?php endif; ?>>
|
||||
<span><?php e(t('Enabled')); ?></span>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Priority')); ?></span>
|
||||
<input type="number" name="recommendations_customer_backlog_priority" min="1" max="999" value="<?php e((int) ($ruleCustomerBacklog['priority'] ?? 60)); ?>">
|
||||
</label>
|
||||
<label class="app-field helpdesk-settings-rule-grid-full">
|
||||
<span><?php e(t('Minimum open tickets')); ?></span>
|
||||
<input type="number" name="recommendations_customer_backlog_min_open_tickets" min="1" max="500" value="<?php e((int) ($ruleCustomerBacklog['min_open_tickets'] ?? 6)); ?>">
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="controlling" class="helpdesk-settings-panel">
|
||||
<details class="app-details-card" name="helpdesk-ctrl-ticket-volume" data-details-key="helpdesk-settings-ctrl-ticket-volume" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Ticket volume increase')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container helpdesk-settings-rule-grid">
|
||||
<label class="app-field">
|
||||
<input type="checkbox" role="switch" name="controlling_ticket_volume_increase_enabled" <?php if (($ctrlTicketVolume['enabled'] ?? true) === true): ?>checked<?php endif; ?>>
|
||||
<span><?php e(t('Enabled')); ?></span>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Threshold (%)')); ?></span>
|
||||
<input type="number" name="controlling_ticket_volume_increase_threshold" min="5" max="200" value="<?php e((int) ($ctrlTicketVolume['threshold_percent'] ?? 30)); ?>">
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="app-details-card" name="helpdesk-ctrl-avg-resolution" data-details-key="helpdesk-settings-ctrl-avg-resolution" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('High avg. resolution time')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container helpdesk-settings-rule-grid">
|
||||
<label class="app-field">
|
||||
<input type="checkbox" role="switch" name="controlling_avg_resolution_high_enabled" <?php if (($ctrlAvgResolution['enabled'] ?? true) === true): ?>checked<?php endif; ?>>
|
||||
<span><?php e(t('Enabled')); ?></span>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Threshold (hours)')); ?></span>
|
||||
<input type="number" name="controlling_avg_resolution_high_threshold" min="1" max="720" value="<?php e((int) ($ctrlAvgResolution['threshold_hours'] ?? 72)); ?>">
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="app-details-card" name="helpdesk-ctrl-open-aging" data-details-key="helpdesk-settings-ctrl-open-aging" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Open ticket aging')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container helpdesk-settings-rule-grid">
|
||||
<label class="app-field">
|
||||
<input type="checkbox" role="switch" name="controlling_open_aging_enabled" <?php if (($ctrlOpenAging['enabled'] ?? true) === true): ?>checked<?php endif; ?>>
|
||||
<span><?php e(t('Enabled')); ?></span>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Threshold (hours)')); ?></span>
|
||||
<input type="number" name="controlling_open_aging_threshold" min="1" max="2160" value="<?php e((int) ($ctrlOpenAging['threshold_hours'] ?? 168)); ?>">
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="app-details-card" name="helpdesk-ctrl-unassigned" data-details-key="helpdesk-settings-ctrl-unassigned" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Unassigned ticket ratio')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container helpdesk-settings-rule-grid">
|
||||
<label class="app-field">
|
||||
<input type="checkbox" role="switch" name="controlling_unassigned_ratio_enabled" <?php if (($ctrlUnassigned['enabled'] ?? true) === true): ?>checked<?php endif; ?>>
|
||||
<span><?php e(t('Enabled')); ?></span>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Threshold (%)')); ?></span>
|
||||
<input type="number" name="controlling_unassigned_ratio_threshold" min="1" max="100" value="<?php e((int) ($ctrlUnassigned['threshold_percent'] ?? 20)); ?>">
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ $ticketNo = trim((string) ($id ?? ''));
|
||||
$fromCustomerNo = trim((string) $request->query('from', ''));
|
||||
|
||||
if ($ticketNo === '') {
|
||||
Router::redirect('helpdesk/debitor');
|
||||
Router::redirect('helpdesk');
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -32,8 +32,8 @@ try {
|
||||
$connectionError = $connectionError ?? false;
|
||||
|
||||
if ($ticket === null && !$connectionError) {
|
||||
Flash::error(t('Ticket not found'), 'helpdesk/debitor', 'ticket_not_found');
|
||||
Router::redirect('helpdesk/debitor');
|
||||
Flash::error(t('Ticket not found'), 'helpdesk', 'ticket_not_found');
|
||||
Router::redirect('helpdesk');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ $ticketDescription = $ticketDescription ?? '';
|
||||
$ticketLogUrl = $ticketLogUrl ?? '';
|
||||
$ticketCommunicationUrl = $ticketCommunicationUrl ?? '';
|
||||
|
||||
$backUrl = $ticketCustomerNo !== '' ? lurl('helpdesk/debitor/' . rawurlencode($ticketCustomerNo)) : lurl('helpdesk/debitor');
|
||||
$backUrl = $ticketCustomerNo !== '' ? lurl('helpdesk/debitor/' . rawurlencode($ticketCustomerNo)) : lurl('helpdesk');
|
||||
|
||||
?>
|
||||
<div class="app-details-container"
|
||||
@@ -32,7 +32,7 @@ $backUrl = $ticketCustomerNo !== '' ? lurl('helpdesk/debitor/' . rawurlencode($t
|
||||
<?php
|
||||
$breadcrumbs = [
|
||||
['label' => t('Home'), 'path' => 'admin'],
|
||||
['label' => t('Helpdesk'), 'path' => 'helpdesk/debitor'],
|
||||
['label' => t('Helpdesk'), 'path' => 'helpdesk'],
|
||||
];
|
||||
if ($ticketCustomerNo !== '') {
|
||||
$breadcrumbs[] = ['label' => (string) ($ticket['Company_Contact_Name'] ?? $ticketCustomerNo), 'path' => 'helpdesk/debitor/' . rawurlencode($ticketCustomerNo)];
|
||||
|
||||
Reference in New Issue
Block a user