1302 lines
51 KiB
PHTML
1302 lines
51 KiB
PHTML
<?php
|
|
|
|
/**
|
|
* @var int $activeUserCount
|
|
* @var int $inactiveUserCount
|
|
* @var int $activeTenantCount
|
|
* @var int $inactiveTenantCount
|
|
* @var int $activeDepartmentCount
|
|
* @var int $inactiveDepartmentCount
|
|
* @var int $activeRoleCount
|
|
* @var int $inactiveRoleCount
|
|
* @var int $rolesWithoutPermissionsCount
|
|
* @var int $permissionsWithoutRolesCount
|
|
* @var int $usersWithoutRolesCount
|
|
* @var int $usersWithoutTenantsCount
|
|
* @var bool $ssoStatsAvailable
|
|
* @var int $ssoEnabledTenantCount
|
|
* @var int $ssoEnforcedTenantCount
|
|
* @var float $ssoEnabledTenantPercent
|
|
* @var int $usersMicrosoftOnlyCount
|
|
* @var int $usersMicrosoftSyncGapsCount
|
|
* @var array<int, array<string, mixed>> $ssoTenantRows
|
|
* @var array<int, array<string, mixed>> $usersMicrosoftOnlyRows
|
|
* @var array<int, array<string, mixed>> $usersMicrosoftSyncGapRows
|
|
* @var bool $customFieldStatsAvailable
|
|
* @var int $customFieldsSelectionWithoutOptionsCount
|
|
* @var int $customFieldTenantsWithSelectionWithoutOptionsCount
|
|
* @var int $customFieldsInactiveWithValuesCount
|
|
* @var int $customFieldsUnusedActiveCount
|
|
* @var string $customFieldsSelectionWithoutOptionsHref
|
|
* @var string $customFieldTenantsWithSelectionWithoutOptionsHref
|
|
* @var string $customFieldsInactiveWithValuesHref
|
|
* @var string $customFieldsUnusedActiveHref
|
|
* @var array<int, array<string, mixed>> $customFieldIssueTenantRows
|
|
* @var bool $apiStatsAvailable
|
|
* @var int $apiRequests24hCount
|
|
* @var int $apiRequestsPrev24hCount
|
|
* @var float $apiRequestTrendPercent
|
|
* @var int $apiErrors4xx24hCount
|
|
* @var int $apiErrors5xx24hCount
|
|
* @var int $apiP95DurationMs
|
|
* @var int $apiTokensExpiring7dCount
|
|
* @var array<int, array<string, mixed>> $apiTopErrorRows
|
|
* @var array<int, array<string, mixed>> $apiSlowEndpointRows
|
|
* @var bool $importStatsAvailable
|
|
* @var int $importRuns7dCount
|
|
* @var int $importRunsPrev7dCount
|
|
* @var float $importRunTrendPercent
|
|
* @var int $importRowsCreated7dCount
|
|
* @var int $importRowsFailed7dCount
|
|
* @var int $importPartialOrFailedRuns7dCount
|
|
* @var float $importSuccessRate7dPercent
|
|
* @var int $importAverageDurationMs7d
|
|
* @var array<int, array<string, mixed>> $importRecentRunRows
|
|
* @var array<int, array<string, mixed>> $importProfileRows
|
|
* @var array<int, array<string, mixed>> $importTopErrorCodeRows
|
|
* @var bool $auditStatsAvailable
|
|
* @var bool $systemAuditStatsAvailable
|
|
* @var bool $userLifecycleAuditStatsAvailable
|
|
* @var int $systemAuditEvents24hCount
|
|
* @var int $systemAuditFailed24hCount
|
|
* @var int $systemAuditDenied24hCount
|
|
* @var int $systemAuditEventsPrev24hCount
|
|
* @var float $systemAuditTrendPercent
|
|
* @var array<int, array<string, mixed>> $systemAuditTopEventTypeRows
|
|
* @var array<int, array<string, mixed>> $systemAuditRecentRiskRows
|
|
* @var string $systemAuditAll24hHref
|
|
* @var string $systemAuditFailed24hHref
|
|
* @var string $systemAuditDenied24hHref
|
|
* @var int $lifecycleRuns7dCount
|
|
* @var int $lifecycleRunsPrev7dCount
|
|
* @var float $lifecycleRunTrendPercent
|
|
* @var int $lifecycleFailed7dCount
|
|
* @var int $lifecycleSkipped7dCount
|
|
* @var int $lifecycleRestore7dCount
|
|
* @var array<int, array<string, mixed>> $lifecycleTopReasonRows
|
|
* @var array<int, array<string, mixed>> $lifecycleRecentRiskRows
|
|
* @var string $lifecycleAll7dHref
|
|
* @var string $lifecycleFailed7dHref
|
|
* @var string $lifecycleRestore7dHref
|
|
* @var bool $scheduledStatsAvailable
|
|
* @var int $scheduledEnabledJobsCount
|
|
* @var int $scheduledOverdueJobsCount
|
|
* @var int $scheduledRuns24hCount
|
|
* @var int $scheduledFailedRuns24hCount
|
|
* @var bool $schedulerRunnerIsActive
|
|
* @var string $schedulerRunnerLastHeartbeatAt
|
|
* @var string $schedulerRunnerLastResult
|
|
* @var string $schedulerRunnerLastErrorCode
|
|
* @var int $usersNeverLoggedCount
|
|
* @var int $usersUnverifiedCount
|
|
* @var int $mailLogSentCount
|
|
* @var int $mailLogFailedCount
|
|
* @var string $mailLogLastSentAt
|
|
* @var array $mailLogRecentFailed
|
|
*/
|
|
|
|
$pageAuth = is_array($viewAuth['page'] ?? null) ? $viewAuth['page'] : [];
|
|
$canViewMailLog = (bool) ($pageAuth['can_view_mail_log'] ?? false);
|
|
$canViewUsers = (bool) ($pageAuth['can_view_users'] ?? false);
|
|
$canViewTenants = (bool) ($pageAuth['can_view_tenants'] ?? false);
|
|
$canViewImportAudit = (bool) ($pageAuth['can_view_imports_audit'] ?? false);
|
|
$canViewSystemAudit = (bool) ($pageAuth['can_view_system_audit'] ?? false);
|
|
$canViewUserLifecycleAudit = (bool) ($pageAuth['can_view_user_lifecycle_audit'] ?? false);
|
|
$canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && (
|
|
($canViewSystemAudit && (bool) ($systemAuditStatsAvailable ?? false))
|
|
|| ($canViewUserLifecycleAudit && (bool) ($userLifecycleAuditStatsAvailable ?? false))
|
|
);
|
|
|
|
?>
|
|
<?php
|
|
$breadcrumbs = [
|
|
['label' => t('Home'), 'path' => 'admin'],
|
|
['label' => t('Statistics')],
|
|
];
|
|
require templatePath('partials/app-breadcrumb.phtml');
|
|
?>
|
|
<div class="app-dashboard-titlebar">
|
|
<h1><?php e(t('Statistics')); ?></h1>
|
|
<div class="app-dashboard-titlebar-actions">
|
|
<!-- Optional actions -->
|
|
</div>
|
|
</div>
|
|
<hr>
|
|
<div class="app-dashboard">
|
|
<div class="app-tabs" data-tabs id="stats-tabs">
|
|
<!-- Tab Navigation -->
|
|
<div class="app-tabs-nav">
|
|
<button data-tab="users" data-tab-default class="transparent tab-button">
|
|
<?php e(t('Users')); ?>
|
|
</button>
|
|
<button data-tab="tenants" class="transparent tab-button">
|
|
<?php e(t('Tenants')); ?>
|
|
</button>
|
|
<button data-tab="departments" class="transparent tab-button">
|
|
<?php e(t('Departments')); ?>
|
|
</button>
|
|
<button data-tab="roles-permissions" class="transparent tab-button">
|
|
<?php e(t('Roles & permissions')); ?>
|
|
</button>
|
|
<button data-tab="security" class="transparent tab-button">
|
|
<?php e(t('Security')); ?>
|
|
</button>
|
|
<?php if ($canViewAuditTab): ?>
|
|
<button data-tab="audit" class="transparent tab-button">
|
|
<?php e(t('Audit')); ?>
|
|
</button>
|
|
<?php endif; ?>
|
|
<?php if ($ssoStatsAvailable): ?>
|
|
<button data-tab="sso" class="transparent tab-button">
|
|
<?php e(t('SSO')); ?>
|
|
</button>
|
|
<?php endif; ?>
|
|
<?php if ($customFieldStatsAvailable): ?>
|
|
<button data-tab="custom-fields" class="transparent tab-button">
|
|
<?php e(t('Custom fields')); ?>
|
|
</button>
|
|
<?php endif; ?>
|
|
<?php if ($apiStatsAvailable): ?>
|
|
<button data-tab="api" class="transparent tab-button">
|
|
<?php e(t('API')); ?>
|
|
</button>
|
|
<?php endif; ?>
|
|
<?php if ($importStatsAvailable): ?>
|
|
<button data-tab="imports" class="transparent tab-button">
|
|
<?php e(t('Imports')); ?>
|
|
</button>
|
|
<?php endif; ?>
|
|
<?php if ($scheduledStatsAvailable): ?>
|
|
<button data-tab="scheduled-jobs" class="transparent tab-button">
|
|
<?php e(t('Scheduled jobs')); ?>
|
|
</button>
|
|
<?php endif; ?>
|
|
<?php if ($canViewMailLog): ?>
|
|
<button data-tab="email-security" class="transparent tab-button">
|
|
<?php e(t('Email security')); ?>
|
|
</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Tab Panel: Users -->
|
|
<div data-tab-panel="users">
|
|
<div class="app-tiles">
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/users?active=active',
|
|
'label' => t('Active users'),
|
|
'count' => (string) $activeUserCount,
|
|
'icon' => 'bi bi-person-check-fill',
|
|
'iconBg' => '#d9f2e6',
|
|
'iconColor' => '#1f6a3a',
|
|
'tooltip' => t('Active users'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/users?active=inactive',
|
|
'label' => t('Inactive users'),
|
|
'count' => (string) $inactiveUserCount,
|
|
'icon' => 'bi bi-person-x-fill',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('Inactive users'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/users?login_status=never',
|
|
'label' => t('Never logged in'),
|
|
'count' => (string) $usersNeverLoggedCount,
|
|
'icon' => 'bi bi-person-slash',
|
|
'iconBg' => '#fff2d9',
|
|
'iconColor' => '#9a5a00',
|
|
'tooltip' => t('Never logged in'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/users?email_verified=unverified',
|
|
'label' => t('Email unverified'),
|
|
'count' => (string) $usersUnverifiedCount,
|
|
'icon' => 'bi bi-shield-exclamation',
|
|
'iconBg' => '#fff2d9',
|
|
'iconColor' => '#9a5a00',
|
|
'tooltip' => t('Email unverified'),
|
|
]);
|
|
?>
|
|
</div>
|
|
<div class="app-stats-table">
|
|
<div class="app-stats-table-header">
|
|
<small><?php e(t('Recent logins')); ?></small>
|
|
</div>
|
|
<?php if (!empty($recentLogins)): ?>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><?php e(t('User')); ?></th>
|
|
<th><?php e(t('Email')); ?></th>
|
|
<th><?php e(t('Last login')); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($recentLogins as $row): ?>
|
|
<tr>
|
|
<td>
|
|
<?php $name = trim(($row['first_name'] ?? '') . ' ' . ($row['last_name'] ?? '')); ?>
|
|
<?php if ($canViewUsers && !empty($row['uuid'])): ?>
|
|
<a href="admin/users/edit/<?php e($row['uuid']); ?>"><?php e($name); ?></a>
|
|
<?php else: ?>
|
|
<?php e($name); ?>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?php e($row['email'] ?? ''); ?></td>
|
|
<td><?php e(dt($row['last_login_at'] ?? '')); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="app-stats-table">
|
|
<div class="app-stats-table-header">
|
|
<small><?php e(t('Never logged in')); ?></small>
|
|
</div>
|
|
<?php if (!empty($neverLoggedUsers)): ?>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><?php e(t('User')); ?></th>
|
|
<th><?php e(t('Email')); ?></th>
|
|
<th><?php e(t('Created')); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($neverLoggedUsers as $row): ?>
|
|
<tr>
|
|
<td>
|
|
<?php $name = trim(($row['first_name'] ?? '') . ' ' . ($row['last_name'] ?? '')); ?>
|
|
<?php if ($canViewUsers && !empty($row['uuid'])): ?>
|
|
<a href="admin/users/edit/<?php e($row['uuid']); ?>"><?php e($name); ?></a>
|
|
<?php else: ?>
|
|
<?php e($name); ?>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?php e($row['email'] ?? ''); ?></td>
|
|
<td><?php e(dt($row['created'] ?? '')); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab Panel: Tenants -->
|
|
<div data-tab-panel="tenants">
|
|
<div class="app-tiles">
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/tenants',
|
|
'label' => t('Active tenants'),
|
|
'count' => (string) $activeTenantCount,
|
|
'icon' => 'bi bi-buildings',
|
|
'iconBg' => '#e9f0ff',
|
|
'iconColor' => '#264db3',
|
|
'tooltip' => t('Active tenants'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/tenants',
|
|
'label' => t('Inactive tenants'),
|
|
'count' => (string) $inactiveTenantCount,
|
|
'icon' => 'bi bi-building-x',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('Inactive tenants'),
|
|
]);
|
|
?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab Panel: Departments -->
|
|
<div data-tab-panel="departments">
|
|
<div class="app-tiles">
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/departments?active=active',
|
|
'label' => t('Active departments'),
|
|
'count' => (string) $activeDepartmentCount,
|
|
'icon' => 'bi bi-diagram-3-fill',
|
|
'iconBg' => '#fff2d9',
|
|
'iconColor' => '#9a5a00',
|
|
'tooltip' => t('Active departments'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/departments?active=inactive',
|
|
'label' => t('Inactive departments'),
|
|
'count' => (string) $inactiveDepartmentCount,
|
|
'icon' => 'bi bi-diagram-3',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('Inactive departments'),
|
|
]);
|
|
?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab Panel: Roles & permissions -->
|
|
<div data-tab-panel="roles-permissions">
|
|
<div class="app-tiles">
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/roles?active=active',
|
|
'label' => t('Active roles'),
|
|
'count' => (string) $activeRoleCount,
|
|
'icon' => 'bi bi-shield-check',
|
|
'iconBg' => '#d9f2e6',
|
|
'iconColor' => '#1f6a3a',
|
|
'tooltip' => t('Active roles'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/roles?active=inactive',
|
|
'label' => t('Inactive roles'),
|
|
'count' => (string) $inactiveRoleCount,
|
|
'icon' => 'bi bi-shield-x',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('Inactive roles'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/roles',
|
|
'label' => t('Roles without permissions'),
|
|
'count' => (string) $rolesWithoutPermissionsCount,
|
|
'icon' => 'bi bi-shield-exclamation',
|
|
'iconBg' => '#fff2d9',
|
|
'iconColor' => '#9a5a00',
|
|
'tooltip' => t('Roles without permissions'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/permissions',
|
|
'label' => t('Permissions without roles'),
|
|
'count' => (string) $permissionsWithoutRolesCount,
|
|
'icon' => 'bi bi-key-fill',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('Permissions without roles'),
|
|
]);
|
|
?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab Panel: Security -->
|
|
<div data-tab-panel="security">
|
|
<div class="app-tiles">
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/users?active=inactive',
|
|
'label' => t('Inactive users'),
|
|
'count' => (string) $inactiveUserCount,
|
|
'icon' => 'bi bi-person-x-fill',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('Inactive users'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/users',
|
|
'label' => t('Users without roles'),
|
|
'count' => (string) $usersWithoutRolesCount,
|
|
'icon' => 'bi bi-person-dash',
|
|
'iconBg' => '#fff2d9',
|
|
'iconColor' => '#9a5a00',
|
|
'tooltip' => t('Users without roles'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/users',
|
|
'label' => t('Users without tenants'),
|
|
'count' => (string) $usersWithoutTenantsCount,
|
|
'icon' => 'bi bi-person-slash',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('Users without tenants'),
|
|
]);
|
|
?>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($canViewAuditTab): ?>
|
|
<?php
|
|
$systemAuditRisk24hCount = (int) $systemAuditFailed24hCount + (int) $systemAuditDenied24hCount;
|
|
$lifecycleRisk7dCount = (int) $lifecycleFailed7dCount + (int) $lifecycleSkipped7dCount;
|
|
?>
|
|
<div data-tab-panel="audit">
|
|
<div class="app-tiles">
|
|
<?php if ($canViewSystemAudit && $systemAuditStatsAvailable): ?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => $systemAuditAll24hHref,
|
|
'label' => t('System audit events (24h)'),
|
|
'count' => (string) $systemAuditEvents24hCount,
|
|
'icon' => 'bi bi-journal-text',
|
|
'iconBg' => '#e9f0ff',
|
|
'iconColor' => '#264db3',
|
|
'tooltip' => t('System audit events (24h)'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => $systemAuditFailed24hHref,
|
|
'label' => t('System audit risks (24h)'),
|
|
'count' => (string) $systemAuditRisk24hCount,
|
|
'icon' => 'bi bi-exclamation-triangle-fill',
|
|
'iconBg' => '#fff2d9',
|
|
'iconColor' => '#9a5a00',
|
|
'tooltip' => t('System audit risks (24h)'),
|
|
]);
|
|
?>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($canViewUserLifecycleAudit && $userLifecycleAuditStatsAvailable): ?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => $lifecycleAll7dHref,
|
|
'label' => t('Lifecycle runs (7d)'),
|
|
'count' => (string) $lifecycleRuns7dCount,
|
|
'icon' => 'bi bi-person-gear',
|
|
'iconBg' => '#e9f0ff',
|
|
'iconColor' => '#264db3',
|
|
'tooltip' => t('Lifecycle runs (7d)'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => $lifecycleFailed7dHref,
|
|
'label' => t('Lifecycle risks (7d)'),
|
|
'count' => (string) $lifecycleRisk7dCount,
|
|
'icon' => 'bi bi-exclamation-octagon-fill',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('Lifecycle risks (7d)'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => $lifecycleRestore7dHref,
|
|
'label' => t('Restore actions (7d)'),
|
|
'count' => (string) $lifecycleRestore7dCount,
|
|
'icon' => 'bi bi-arrow-counterclockwise',
|
|
'iconBg' => '#d9f2e6',
|
|
'iconColor' => '#1f6a3a',
|
|
'tooltip' => t('Restore actions (7d)'),
|
|
]);
|
|
?>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="grid">
|
|
<div class="app-stats-table">
|
|
<div class="app-stats-table-header">
|
|
<small><?php e(t('Recent system audit risks (24h)')); ?></small>
|
|
</div>
|
|
<?php if ($canViewSystemAudit && $systemAuditStatsAvailable): ?>
|
|
<?php if (!empty($systemAuditRecentRiskRows)): ?>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><?php e(t('Event type')); ?></th>
|
|
<th><?php e(t('Status')); ?></th>
|
|
<th><?php e(t('Channel')); ?></th>
|
|
<th><?php e(t('Created')); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($systemAuditRecentRiskRows as $row): ?>
|
|
<?php $auditId = (int) ($row['id'] ?? 0); ?>
|
|
<tr>
|
|
<td>
|
|
<?php if ($auditId > 0): ?>
|
|
<a href="admin/system-audit/view/<?php e((string) $auditId); ?>"><?php e((string) ($row['event_type'] ?? '')); ?></a>
|
|
<?php else: ?>
|
|
<?php e((string) ($row['event_type'] ?? '')); ?>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?php e((string) ($row['outcome'] ?? '')); ?></td>
|
|
<td><?php e((string) ($row['channel'] ?? '')); ?></td>
|
|
<td><?php e(dt((string) ($row['created_at'] ?? ''))); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<div class="app-stats-table-empty">
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php elseif ($canViewUserLifecycleAudit && $userLifecycleAuditStatsAvailable): ?>
|
|
<?php if (!empty($lifecycleTopReasonRows)): ?>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><?php e(t('Reason code')); ?></th>
|
|
<th><?php e(t('Count')); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($lifecycleTopReasonRows as $row): ?>
|
|
<tr>
|
|
<td><?php e((string) ($row['reason_code'] ?? '-')); ?></td>
|
|
<td><?php e((string) ((int) ($row['hit_count'] ?? 0))); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<div class="app-stats-table-empty">
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php else: ?>
|
|
<div class="app-stats-table-empty">
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="app-stats-table">
|
|
<div class="app-stats-table-header">
|
|
<small><?php e(t('Recent lifecycle risks (7d)')); ?></small>
|
|
</div>
|
|
<?php if ($canViewUserLifecycleAudit && $userLifecycleAuditStatsAvailable): ?>
|
|
<?php if (!empty($lifecycleRecentRiskRows)): ?>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><?php e(t('Action')); ?></th>
|
|
<th><?php e(t('Status')); ?></th>
|
|
<th><?php e(t('Trigger')); ?></th>
|
|
<th><?php e(t('Target')); ?></th>
|
|
<th><?php e(t('Created')); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($lifecycleRecentRiskRows as $row): ?>
|
|
<?php
|
|
$lifecycleId = (int) ($row['id'] ?? 0);
|
|
$targetLabel = trim((string) ($row['target_user_uuid'] ?? ''));
|
|
if ($targetLabel === '') {
|
|
$targetLabel = trim((string) ($row['target_user_email'] ?? ''));
|
|
}
|
|
if ($targetLabel === '') {
|
|
$targetLabel = '-';
|
|
}
|
|
?>
|
|
<tr>
|
|
<td>
|
|
<?php if ($lifecycleId > 0): ?>
|
|
<a href="admin/user-lifecycle-audit/view/<?php e((string) $lifecycleId); ?>"><?php e((string) ($row['action'] ?? '')); ?></a>
|
|
<?php else: ?>
|
|
<?php e((string) ($row['action'] ?? '')); ?>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?php e((string) ($row['status'] ?? '')); ?></td>
|
|
<td><?php e((string) ($row['trigger_type'] ?? '')); ?></td>
|
|
<td><?php e($targetLabel); ?></td>
|
|
<td><?php e(dt((string) ($row['created_at'] ?? ''))); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<div class="app-stats-table-empty">
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php elseif ($canViewSystemAudit && $systemAuditStatsAvailable): ?>
|
|
<?php if (!empty($systemAuditTopEventTypeRows)): ?>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><?php e(t('Event type')); ?></th>
|
|
<th><?php e(t('Count')); ?></th>
|
|
<th><?php e(t('Failed or denied')); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($systemAuditTopEventTypeRows as $row): ?>
|
|
<tr>
|
|
<td><?php e((string) ($row['event_type'] ?? '')); ?></td>
|
|
<td><?php e((string) ((int) ($row['hit_count'] ?? 0))); ?></td>
|
|
<td><?php e((string) ((int) ($row['failed_or_denied_count'] ?? 0))); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<div class="app-stats-table-empty">
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php else: ?>
|
|
<div class="app-stats-table-empty">
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($ssoStatsAvailable): ?>
|
|
<div data-tab-panel="sso">
|
|
<div class="app-tiles">
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/tenants',
|
|
'label' => t('SSO-enabled tenants'),
|
|
'count' => (string) $ssoEnabledTenantCount,
|
|
'icon' => 'bi bi-microsoft',
|
|
'iconBg' => '#e9f0ff',
|
|
'iconColor' => '#264db3',
|
|
'tooltip' => t('SSO-enabled tenants'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/tenants',
|
|
'label' => t('SSO-enforced tenants'),
|
|
'count' => (string) $ssoEnforcedTenantCount,
|
|
'icon' => 'bi bi-shield-lock-fill',
|
|
'iconBg' => '#d9f2e6',
|
|
'iconColor' => '#1f6a3a',
|
|
'tooltip' => t('SSO-enforced tenants'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/tenants',
|
|
'label' => t('SSO rollout'),
|
|
'count' => number_format($ssoEnabledTenantPercent, 1, '.', '') . '%',
|
|
'icon' => 'bi bi-speedometer2',
|
|
'iconBg' => '#fff2d9',
|
|
'iconColor' => '#9a5a00',
|
|
'tooltip' => t('SSO rollout'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/users',
|
|
'label' => t('Users with Microsoft-only login'),
|
|
'count' => (string) $usersMicrosoftOnlyCount,
|
|
'icon' => 'bi bi-person-lock',
|
|
'iconBg' => '#f2f2f2',
|
|
'iconColor' => '#555555',
|
|
'tooltip' => t('Users with Microsoft-only login'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/users',
|
|
'label' => t('Microsoft profile sync gaps'),
|
|
'count' => (string) $usersMicrosoftSyncGapsCount,
|
|
'icon' => 'bi bi-person-exclamation',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('Microsoft profile sync gaps'),
|
|
]);
|
|
?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($customFieldStatsAvailable): ?>
|
|
<div data-tab-panel="custom-fields">
|
|
<div class="app-tiles">
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => $customFieldsSelectionWithoutOptionsHref,
|
|
'label' => t('Selection fields without options'),
|
|
'count' => (string) $customFieldsSelectionWithoutOptionsCount,
|
|
'icon' => 'bi bi-list-check',
|
|
'iconBg' => '#fff2d9',
|
|
'iconColor' => '#9a5a00',
|
|
'tooltip' => t('Selection fields without options'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => $customFieldTenantsWithSelectionWithoutOptionsHref,
|
|
'label' => t('Tenants with broken selection fields'),
|
|
'count' => (string) $customFieldTenantsWithSelectionWithoutOptionsCount,
|
|
'icon' => 'bi bi-exclamation-triangle',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('Tenants with broken selection fields'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => $customFieldsInactiveWithValuesHref,
|
|
'label' => t('Inactive custom fields with values'),
|
|
'count' => (string) $customFieldsInactiveWithValuesCount,
|
|
'icon' => 'bi bi-archive-fill',
|
|
'iconBg' => '#e9f0ff',
|
|
'iconColor' => '#264db3',
|
|
'tooltip' => t('Inactive custom fields with values'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => $customFieldsUnusedActiveHref,
|
|
'label' => t('Unused active custom fields'),
|
|
'count' => (string) $customFieldsUnusedActiveCount,
|
|
'icon' => 'bi bi-slash-circle',
|
|
'iconBg' => '#f2f2f2',
|
|
'iconColor' => '#555555',
|
|
'tooltip' => t('Unused active custom fields'),
|
|
]);
|
|
?>
|
|
</div>
|
|
<div class="app-stats-table">
|
|
<div class="app-stats-table-header">
|
|
<small><?php e(t('Affected tenants')); ?></small>
|
|
</div>
|
|
<?php if (!empty($customFieldIssueTenantRows)): ?>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><?php e(t('Issue')); ?></th>
|
|
<th><?php e(t('Tenant')); ?></th>
|
|
<th><?php e(t('Count')); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($customFieldIssueTenantRows as $row): ?>
|
|
<tr>
|
|
<td><?php e($row['issue'] ?? ''); ?></td>
|
|
<td>
|
|
<?php $tenantUuid = (string) ($row['tenant_uuid'] ?? ''); ?>
|
|
<?php $tenantDescription = (string) ($row['tenant_description'] ?? ''); ?>
|
|
<?php if ($canViewTenants && $tenantUuid !== ''): ?>
|
|
<a href="admin/tenants/edit/<?php e($tenantUuid); ?>?tab=custom_fields"><?php e($tenantDescription); ?></a>
|
|
<?php else: ?>
|
|
<?php e($tenantDescription); ?>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?php e((string) ((int) ($row['issue_count'] ?? 0))); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<div class="app-stats-table-empty">
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="app-stats-table-explanation">
|
|
<small class="muted">
|
|
<?php e(t('If a tenant appears here, at least one custom field setup needs review (missing options, inactive fields with values, or unused active fields).')); ?>
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($apiStatsAvailable): ?>
|
|
<div data-tab-panel="api">
|
|
<div class="app-tiles">
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/api-audit',
|
|
'label' => t('API requests (24h)'),
|
|
'count' => (string) $apiRequests24hCount,
|
|
'icon' => 'bi bi-bar-chart-line-fill',
|
|
'iconBg' => '#e9f0ff',
|
|
'iconColor' => '#264db3',
|
|
'tooltip' => t('API requests (24h)'),
|
|
]);
|
|
?>
|
|
<?php
|
|
$trendLabel = $apiRequestsPrev24hCount > 0
|
|
? (($apiRequestTrendPercent > 0 ? '+' : '') . number_format($apiRequestTrendPercent, 1, '.', '') . '%')
|
|
: 'n/a';
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/api-audit',
|
|
'label' => t('Request trend vs previous 24h'),
|
|
'count' => $trendLabel,
|
|
'icon' => 'bi bi-graph-up-arrow',
|
|
'iconBg' => '#f2f2f2',
|
|
'iconColor' => '#555555',
|
|
'tooltip' => t('Request trend vs previous 24h'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/api-audit',
|
|
'label' => t('4xx errors (24h)'),
|
|
'count' => (string) $apiErrors4xx24hCount,
|
|
'icon' => 'bi bi-shield-exclamation',
|
|
'iconBg' => '#fff2d9',
|
|
'iconColor' => '#9a5a00',
|
|
'tooltip' => t('4xx errors (24h)'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/api-audit',
|
|
'label' => t('5xx errors (24h)'),
|
|
'count' => (string) $apiErrors5xx24hCount,
|
|
'icon' => 'bi bi-bug-fill',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('5xx errors (24h)'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/api-audit',
|
|
'label' => t('P95 response time (ms)'),
|
|
'count' => (string) $apiP95DurationMs,
|
|
'icon' => 'bi bi-stopwatch-fill',
|
|
'iconBg' => '#d9f2e6',
|
|
'iconColor' => '#1f6a3a',
|
|
'tooltip' => t('P95 response time (ms)'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/settings?tab=api',
|
|
'label' => t('Tokens expiring in 7 days'),
|
|
'count' => (string) $apiTokensExpiring7dCount,
|
|
'icon' => 'bi bi-key-fill',
|
|
'iconBg' => '#fff2d9',
|
|
'iconColor' => '#9a5a00',
|
|
'tooltip' => t('Tokens expiring in 7 days'),
|
|
]);
|
|
?>
|
|
</div>
|
|
|
|
<div class="grid">
|
|
<div class="app-stats-table">
|
|
<div class="app-stats-table-header">
|
|
<small><?php e(t('Top API error codes (24h)')); ?></small>
|
|
</div>
|
|
<?php if (!empty($apiTopErrorRows)): ?>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><?php e(t('Error code')); ?></th>
|
|
<th><?php e(t('Count')); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($apiTopErrorRows as $row): ?>
|
|
<tr>
|
|
<td><?php e((string) ($row['error_label'] ?? '')); ?></td>
|
|
<td><?php e((string) ((int) ($row['hit_count'] ?? 0))); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<div class="app-stats-table-empty">
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="app-stats-table">
|
|
<div class="app-stats-table-header">
|
|
<small><?php e(t('Slowest API endpoints (avg, 24h)')); ?></small>
|
|
</div>
|
|
<?php if (!empty($apiSlowEndpointRows)): ?>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><?php e(t('Path')); ?></th>
|
|
<th><?php e(t('Requests')); ?></th>
|
|
<th><?php e(t('Average duration (ms)')); ?></th>
|
|
<th><?php e(t('Maximum duration (ms)')); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($apiSlowEndpointRows as $row): ?>
|
|
<tr>
|
|
<td><?php e((string) ($row['path'] ?? '')); ?></td>
|
|
<td><?php e((string) ((int) ($row['hit_count'] ?? 0))); ?></td>
|
|
<td><?php e(number_format((float) ($row['avg_duration_ms'] ?? 0), 1, '.', '')); ?></td>
|
|
<td><?php e((string) ((int) ($row['max_duration_ms'] ?? 0))); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<div class="app-stats-table-empty">
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($importStatsAvailable): ?>
|
|
<div data-tab-panel="imports">
|
|
<div class="app-tiles">
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/import-audit',
|
|
'label' => t('Import runs (7d)'),
|
|
'count' => (string) $importRuns7dCount,
|
|
'icon' => 'bi bi-box-arrow-in-down',
|
|
'iconBg' => '#e9f0ff',
|
|
'iconColor' => '#264db3',
|
|
'tooltip' => t('Import runs (7d)'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/import-audit',
|
|
'label' => t('Created rows (7d)'),
|
|
'count' => (string) $importRowsCreated7dCount,
|
|
'icon' => 'bi bi-person-plus-fill',
|
|
'iconBg' => '#d9f2e6',
|
|
'iconColor' => '#1f6a3a',
|
|
'tooltip' => t('Created rows (7d)'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/import-audit',
|
|
'label' => t('Failed rows (7d)'),
|
|
'count' => (string) $importRowsFailed7dCount,
|
|
'icon' => 'bi bi-exclamation-octagon-fill',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('Failed rows (7d)'),
|
|
]);
|
|
?>
|
|
<?php
|
|
$importSuccessLabel = $importRuns7dCount > 0
|
|
? number_format($importSuccessRate7dPercent, 1, '.', '') . '%'
|
|
: 'n/a';
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/import-audit',
|
|
'label' => t('Success rate (7d)'),
|
|
'count' => $importSuccessLabel,
|
|
'icon' => 'bi bi-check2-circle',
|
|
'iconBg' => '#d9f2e6',
|
|
'iconColor' => '#1f6a3a',
|
|
'tooltip' => t('Success rate (7d)'),
|
|
]);
|
|
?>
|
|
</div>
|
|
|
|
<div class="app-stats-table">
|
|
<div class="app-stats-table-header">
|
|
<small><?php e(t('Recent import runs')); ?></small>
|
|
</div>
|
|
<?php if (!empty($importRecentRunRows)): ?>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><?php e(t('Created')); ?></th>
|
|
<th><?php e(t('Profile')); ?></th>
|
|
<th><?php e(t('Status')); ?></th>
|
|
<th><?php e(t('Rows total')); ?></th>
|
|
<th><?php e(t('Created count')); ?></th>
|
|
<th><?php e(t('Failed count')); ?></th>
|
|
<th><?php e(t('Duration (ms)')); ?></th>
|
|
<th><?php e(t('User')); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($importRecentRunRows as $row): ?>
|
|
<?php
|
|
$profileKey = strtolower(trim((string) ($row['profile_key'] ?? '')));
|
|
$profileLabel = $profileKey === 'users'
|
|
? t('Users')
|
|
: ($profileKey === 'departments' ? t('Departments') : $profileKey);
|
|
?>
|
|
<tr>
|
|
<td>
|
|
<?php if ($canViewImportAudit): ?>
|
|
<a href="admin/import-audit/view/<?php e((string) ($row['id'] ?? '')); ?>">
|
|
<?php e(dt((string) ($row['started_at'] ?? ''))); ?>
|
|
</a>
|
|
<?php else: ?>
|
|
<?php e(dt((string) ($row['started_at'] ?? ''))); ?>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?php e($profileLabel); ?></td>
|
|
<td><?php e((string) ($row['status'] ?? '')); ?></td>
|
|
<td><?php e((string) ((int) ($row['rows_total'] ?? 0))); ?></td>
|
|
<td><?php e((string) ((int) ($row['created_count'] ?? 0))); ?></td>
|
|
<td><?php e((string) ((int) ($row['failed_count'] ?? 0))); ?></td>
|
|
<td><?php e((string) ((int) ($row['duration_ms'] ?? 0))); ?></td>
|
|
<td>
|
|
<?php
|
|
$userLabel = trim((string) ($row['user_display_name'] ?? ''));
|
|
if ($userLabel === '') {
|
|
$userLabel = trim((string) ($row['user_email'] ?? ''));
|
|
}
|
|
if ($userLabel === '') {
|
|
$userLabel = '-';
|
|
}
|
|
?>
|
|
<?php e($userLabel); ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<div class="app-stats-table-empty">
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="grid">
|
|
<div class="app-stats-table">
|
|
<div class="app-stats-table-header">
|
|
<small><?php e(t('Imports by profile (7d)')); ?></small>
|
|
</div>
|
|
<?php if (!empty($importProfileRows)): ?>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><?php e(t('Profile')); ?></th>
|
|
<th><?php e(t('Runs')); ?></th>
|
|
<th><?php e(t('Created rows')); ?></th>
|
|
<th><?php e(t('Failed rows')); ?></th>
|
|
<th><?php e(t('Success rate')); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($importProfileRows as $row): ?>
|
|
<?php
|
|
$profileKey = strtolower(trim((string) ($row['profile_key'] ?? '')));
|
|
$profileLabel = $profileKey === 'users'
|
|
? t('Users')
|
|
: ($profileKey === 'departments' ? t('Departments') : $profileKey);
|
|
?>
|
|
<tr>
|
|
<td><?php e($profileLabel); ?></td>
|
|
<td><?php e((string) ((int) ($row['run_count'] ?? 0))); ?></td>
|
|
<td><?php e((string) ((int) ($row['created_count_sum'] ?? 0))); ?></td>
|
|
<td><?php e((string) ((int) ($row['failed_count_sum'] ?? 0))); ?></td>
|
|
<td><?php e(number_format((float) ($row['success_rate'] ?? 0), 1, '.', '') . '%'); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<div class="app-stats-table-empty">
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="app-stats-table">
|
|
<div class="app-stats-table-header">
|
|
<small><?php e(t('Top import error codes (7d)')); ?></small>
|
|
</div>
|
|
<?php if (!empty($importTopErrorCodeRows)): ?>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><?php e(t('Error code')); ?></th>
|
|
<th><?php e(t('Count')); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($importTopErrorCodeRows as $row): ?>
|
|
<tr>
|
|
<td><?php e((string) ($row['error_code'] ?? '')); ?></td>
|
|
<td><?php e((string) ((int) ($row['count'] ?? 0))); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php else: ?>
|
|
<div class="app-stats-table-empty">
|
|
<?php $emptyState = ['message' => t('No entries found'), 'size' => 'compact']; require templatePath('partials/app-empty-state.phtml'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($scheduledStatsAvailable): ?>
|
|
<div data-tab-panel="scheduled-jobs">
|
|
<?php
|
|
$runnerStatusLabel = $schedulerRunnerIsActive ? t('Active') : t('Inactive');
|
|
$runnerTooltipParts = [];
|
|
if ($schedulerRunnerLastHeartbeatAt !== '') {
|
|
$runnerTooltipParts[] = t('Last heartbeat') . ': ' . dt($schedulerRunnerLastHeartbeatAt);
|
|
}
|
|
if ($schedulerRunnerLastResult !== '') {
|
|
$runnerResultLabel = $schedulerRunnerLastResult;
|
|
if ($runnerResultLabel === 'ok') {
|
|
$runnerResultLabel = t('Success');
|
|
} elseif ($runnerResultLabel === 'lock_not_acquired') {
|
|
$runnerResultLabel = t('Runner lock not acquired');
|
|
} elseif ($runnerResultLabel === 'unexpected_error') {
|
|
$runnerResultLabel = t('Failed');
|
|
}
|
|
if ($schedulerRunnerLastErrorCode !== '') {
|
|
$runnerResultLabel .= ' (' . $schedulerRunnerLastErrorCode . ')';
|
|
}
|
|
$runnerTooltipParts[] = t('Last runner result') . ': ' . $runnerResultLabel;
|
|
}
|
|
$runnerTooltip = implode(' | ', $runnerTooltipParts);
|
|
?>
|
|
<div class="app-tiles">
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/scheduled-jobs',
|
|
'label' => t('Enabled jobs'),
|
|
'count' => (string) $scheduledEnabledJobsCount,
|
|
'icon' => 'bi bi-check2-square',
|
|
'iconBg' => '#d9f2e6',
|
|
'iconColor' => '#1f6a3a',
|
|
'tooltip' => t('Enabled jobs'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/scheduled-jobs',
|
|
'label' => t('Overdue jobs'),
|
|
'count' => (string) $scheduledOverdueJobsCount,
|
|
'icon' => 'bi bi-exclamation-triangle-fill',
|
|
'iconBg' => '#fff2d9',
|
|
'iconColor' => '#9a5a00',
|
|
'tooltip' => t('Overdue jobs'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/scheduled-jobs',
|
|
'label' => t('Scheduler runs (24h)'),
|
|
'count' => (string) $scheduledRuns24hCount,
|
|
'icon' => 'bi bi-clock-history',
|
|
'iconBg' => '#e9f0ff',
|
|
'iconColor' => '#264db3',
|
|
'tooltip' => t('Scheduler runs (24h)'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/scheduled-jobs',
|
|
'label' => t('Failed scheduler runs (24h)'),
|
|
'count' => (string) $scheduledFailedRuns24hCount,
|
|
'icon' => 'bi bi-x-octagon-fill',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('Failed scheduler runs (24h)'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/scheduled-jobs',
|
|
'label' => t('Cron runner active'),
|
|
'count' => $runnerStatusLabel,
|
|
'icon' => $schedulerRunnerIsActive ? 'bi bi-heart-pulse-fill' : 'bi bi-heartbreak-fill',
|
|
'iconBg' => $schedulerRunnerIsActive ? '#d9f2e6' : '#ffe9e9',
|
|
'iconColor' => $schedulerRunnerIsActive ? '#1f6a3a' : '#a32e2e',
|
|
'tooltip' => $runnerTooltip !== '' ? $runnerTooltip : t('Cron runner active'),
|
|
]);
|
|
?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Tab Panel: Email security -->
|
|
<?php if ($canViewMailLog): ?>
|
|
<div data-tab-panel="email-security">
|
|
<div class="app-tiles">
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/mail-log?status=failed',
|
|
'label' => t('Mail failures'),
|
|
'count' => (string) $mailLogFailedCount,
|
|
'icon' => 'bi bi-envelope-x-fill',
|
|
'iconBg' => '#ffe9e9',
|
|
'iconColor' => '#a32e2e',
|
|
'tooltip' => t('Mail failures'),
|
|
]);
|
|
?>
|
|
<?php
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/mail-log?status=sent',
|
|
'label' => t('Sent emails'),
|
|
'count' => (string) $mailLogSentCount,
|
|
'icon' => 'bi bi-envelope-check-fill',
|
|
'iconBg' => '#d9f2e6',
|
|
'iconColor' => '#1f6a3a',
|
|
'tooltip' => t('Sent emails'),
|
|
]);
|
|
?>
|
|
<?php
|
|
$lastSentLabel = $mailLogLastSentAt ? dt($mailLogLastSentAt) : t('Never');
|
|
MintyPHP\Support\Tile::render([
|
|
'href' => 'admin/mail-log?status=sent',
|
|
'label' => t('Last email sent'),
|
|
'count' => $lastSentLabel,
|
|
'icon' => 'bi bi-clock-history',
|
|
'iconBg' => '#e9f0ff',
|
|
'iconColor' => '#264db3',
|
|
'tooltip' => t('Last email sent'),
|
|
]);
|
|
?>
|
|
</div>
|
|
<?php if (!empty($mailLogRecentFailed)): ?>
|
|
<div class="app-stats-table">
|
|
<div class="app-stats-table-header">
|
|
<small><?php e(t('Mail failures')); ?></small>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><?php e(t('Recipient')); ?></th>
|
|
<th><?php e(t('Subject')); ?></th>
|
|
<th><?php e(t('Error')); ?></th>
|
|
<th><?php e(t('Created')); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($mailLogRecentFailed as $row): ?>
|
|
<tr>
|
|
<td><?php e($row['to_email'] ?? ''); ?></td>
|
|
<td><?php e($row['subject'] ?? ''); ?></td>
|
|
<td><?php e($row['error_message'] ?? ''); ?></td>
|
|
<td><?php e(dt($row['created_at'] ?? '')); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|