diff --git a/CLAUDE.md b/CLAUDE.md index 3cfc968..8c7d6c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -191,6 +191,7 @@ docker/ # Dockerfiles, Nginx configs, PHP configs - **Remove button:** uses the shared `.app-icon-button` primitive + `` — never hand-roll a close button. - **Coexistence:** `multiSelectForm()` (vendor MultiSelect) remains available for small closed lists where the chip-header is acceptable. Pick `tokenSelectForm()` when the selected set can grow beyond ~10 items or typeahead is the expected interaction. - **Tests:** helper contract lives in `tests/Support/Helpers/TokenSelectFormHelperTest.php`; component lifecycle + entrypoint registration are enforced by `tests/Architecture/FrontendRuntime*ContractTest.php`. +- **Dashboard tile** (icon + count + label link): use `appTile()` from `core/Support/helpers/ui.php` for dashboard / stat-grid tiles that link to a filtered list view. Renders the shared `templates/partials/app-tile.phtml` with `.app-tile` CSS. Options: `href`, `label`, `count`, `icon` (bi-*), `iconBg`, `iconColor`, `class`, `tooltip`, `tooltipPos`. Group tiles inside `
` for the grid layout. Modules may call `appTile()` directly — no wrapper class needed. ### Never Do This diff --git a/core/Support/Tile.php b/core/Support/Tile.php deleted file mode 100644 index 2432ca0..0000000 --- a/core/Support/Tile.php +++ /dev/null @@ -1,30 +0,0 @@ - '#', - 'label' => '', - 'count' => null, - 'icon' => 'bi bi-grid-1x2', - 'iconBg' => null, - 'iconColor' => null, - 'class' => '', - 'tooltip' => '', - 'tooltipPos' => 'top', - ]; - - $data = array_merge($defaults, $options); - $template = dirname(__DIR__, 2) . '/templates/partials/app-tile.phtml'; - if (!is_file($template)) { - return; - } - - extract($data, EXTR_SKIP); - require $template; - } -} diff --git a/core/Support/helpers/ui.php b/core/Support/helpers/ui.php index 8f36577..99ec3ec 100644 --- a/core/Support/helpers/ui.php +++ b/core/Support/helpers/ui.php @@ -803,3 +803,43 @@ function renderPageStyles(): void { renderStylePaths(appPageStylePaths()); } + +/** + * Render a dashboard tile (icon + count + label, linking to a target URL). + * + * Options: + * - href (string) Link target. Default: '#'. + * - label (string) Tile label. Default: ''. + * - count (string|null) Numeric or short textual value. Default: null. + * - icon (string) Bootstrap-icon class. Default: 'bi bi-grid-1x2'. + * - iconBg (string|null) CSS color for icon background. + * - iconColor (string|null) CSS color for icon glyph. + * - class (string) Extra CSS classes appended to .app-tile. + * - tooltip (string) Tooltip text (already translated). + * - tooltipPos (string) Tooltip position. Default: 'top'. + * + * @param array $options + */ +function appTile(array $options = []): void +{ + $defaults = [ + 'href' => '#', + 'label' => '', + 'count' => null, + 'icon' => 'bi bi-grid-1x2', + 'iconBg' => null, + 'iconColor' => null, + 'class' => '', + 'tooltip' => '', + 'tooltipPos' => 'top', + ]; + + $data = array_merge($defaults, $options); + $template = templatePath('partials/app-tile.phtml'); + if (!is_file($template)) { + return; + } + + extract($data, EXTR_SKIP); + require $template; +} diff --git a/pages/admin/stats/index(default).phtml b/pages/admin/stats/index(default).phtml index 0ccbefc..2f9d454 100644 --- a/pages/admin/stats/index(default).phtml +++ b/pages/admin/stats/index(default).phtml @@ -175,7 +175,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && (
'admin/users?active=active', 'label' => t('Active users'), 'count' => (string) $activeUserCount, @@ -186,7 +186,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/users?active=inactive', 'label' => t('Inactive users'), 'count' => (string) $inactiveUserCount, @@ -197,7 +197,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/users?login_status=never', 'label' => t('Never logged in'), 'count' => (string) $usersNeverLoggedCount, @@ -208,7 +208,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/users?email_verified=unverified', 'label' => t('Email unverified'), 'count' => (string) $usersUnverifiedCount, @@ -293,7 +293,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && (
'admin/tenants', 'label' => t('Active tenants'), 'count' => (string) $activeTenantCount, @@ -304,7 +304,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/tenants', 'label' => t('Inactive tenants'), 'count' => (string) $inactiveTenantCount, @@ -321,7 +321,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && (
'admin/departments?active=active', 'label' => t('Active departments'), 'count' => (string) $activeDepartmentCount, @@ -332,7 +332,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/departments?active=inactive', 'label' => t('Inactive departments'), 'count' => (string) $inactiveDepartmentCount, @@ -349,7 +349,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && (
'admin/roles?active=active', 'label' => t('Active roles'), 'count' => (string) $activeRoleCount, @@ -360,7 +360,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/roles?active=inactive', 'label' => t('Inactive roles'), 'count' => (string) $inactiveRoleCount, @@ -371,7 +371,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/roles', 'label' => t('Roles without permissions'), 'count' => (string) $rolesWithoutPermissionsCount, @@ -382,7 +382,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/permissions', 'label' => t('Permissions without roles'), 'count' => (string) $permissionsWithoutRolesCount, @@ -399,7 +399,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && (
'admin/users?active=inactive', 'label' => t('Inactive users'), 'count' => (string) $inactiveUserCount, @@ -410,7 +410,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/users', 'label' => t('Users without roles'), 'count' => (string) $usersWithoutRolesCount, @@ -421,7 +421,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/users', 'label' => t('Users without tenants'), 'count' => (string) $usersWithoutTenantsCount, @@ -443,7 +443,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && (
$systemAuditAll24hHref, 'label' => t('System audit events (24h)'), 'count' => (string) $systemAuditEvents24hCount, @@ -454,7 +454,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> $systemAuditFailed24hHref, 'label' => t('System audit risks (24h)'), 'count' => (string) $systemAuditRisk24hCount, @@ -468,7 +468,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( $lifecycleAll7dHref, 'label' => t('Lifecycle runs (7d)'), 'count' => (string) $lifecycleRuns7dCount, @@ -479,7 +479,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> $lifecycleFailed7dHref, 'label' => t('Lifecycle risks (7d)'), 'count' => (string) $lifecycleRisk7dCount, @@ -490,7 +490,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> $lifecycleRestore7dHref, 'label' => t('Restore actions (7d)'), 'count' => (string) $lifecycleRestore7dCount, @@ -660,7 +660,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && (
'admin/tenants', 'label' => t('SSO-enabled tenants'), 'count' => (string) $ssoEnabledTenantCount, @@ -671,7 +671,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/tenants', 'label' => t('SSO-enforced tenants'), 'count' => (string) $ssoEnforcedTenantCount, @@ -682,7 +682,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/tenants', 'label' => t('SSO rollout'), 'count' => number_format($ssoEnabledTenantPercent, 1, '.', '') . '%', @@ -693,7 +693,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/users', 'label' => t('Users with Microsoft-only login'), 'count' => (string) $usersMicrosoftOnlyCount, @@ -704,7 +704,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/users', 'label' => t('Microsoft profile sync gaps'), 'count' => (string) $usersMicrosoftSyncGapsCount, @@ -722,7 +722,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && (
$customFieldsSelectionWithoutOptionsHref, 'label' => t('Selection fields without options'), 'count' => (string) $customFieldsSelectionWithoutOptionsCount, @@ -733,7 +733,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> $customFieldTenantsWithSelectionWithoutOptionsHref, 'label' => t('Tenants with broken selection fields'), 'count' => (string) $customFieldTenantsWithSelectionWithoutOptionsCount, @@ -744,7 +744,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> $customFieldsInactiveWithValuesHref, 'label' => t('Inactive custom fields with values'), 'count' => (string) $customFieldsInactiveWithValuesCount, @@ -755,7 +755,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> $customFieldsUnusedActiveHref, 'label' => t('Unused active custom fields'), 'count' => (string) $customFieldsUnusedActiveCount, @@ -815,7 +815,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && (
'admin/api-audit', 'label' => t('API requests (24h)'), 'count' => (string) $apiRequests24hCount, @@ -829,7 +829,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( $trendLabel = $apiRequestsPrev24hCount > 0 ? (($apiRequestTrendPercent > 0 ? '+' : '') . number_format($apiRequestTrendPercent, 1, '.', '') . '%') : 'n/a'; - MintyPHP\Support\Tile::render([ + appTile([ 'href' => 'admin/api-audit', 'label' => t('Request trend vs previous 24h'), 'count' => $trendLabel, @@ -840,7 +840,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/api-audit', 'label' => t('4xx errors (24h)'), 'count' => (string) $apiErrors4xx24hCount, @@ -851,7 +851,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/api-audit', 'label' => t('5xx errors (24h)'), 'count' => (string) $apiErrors5xx24hCount, @@ -862,7 +862,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/api-audit', 'label' => t('P95 response time (ms)'), 'count' => (string) $apiP95DurationMs, @@ -873,7 +873,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/settings?tab=api', 'label' => t('Tokens expiring in 7 days'), 'count' => (string) $apiTokensExpiring7dCount, @@ -953,7 +953,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && (
'admin/import-audit', 'label' => t('Import runs (7d)'), 'count' => (string) $importRuns7dCount, @@ -964,7 +964,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/import-audit', 'label' => t('Created rows (7d)'), 'count' => (string) $importRowsCreated7dCount, @@ -975,7 +975,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/import-audit', 'label' => t('Failed rows (7d)'), 'count' => (string) $importRowsFailed7dCount, @@ -989,7 +989,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( $importSuccessLabel = $importRuns7dCount > 0 ? number_format($importSuccessRate7dPercent, 1, '.', '') . '%' : 'n/a'; - MintyPHP\Support\Tile::render([ + appTile([ 'href' => 'admin/import-audit', 'label' => t('Success rate (7d)'), 'count' => $importSuccessLabel, @@ -1164,7 +1164,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ?>
'admin/scheduled-jobs', 'label' => t('Enabled jobs'), 'count' => (string) $scheduledEnabledJobsCount, @@ -1175,7 +1175,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/scheduled-jobs', 'label' => t('Overdue jobs'), 'count' => (string) $scheduledOverdueJobsCount, @@ -1186,7 +1186,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/scheduled-jobs', 'label' => t('Scheduler runs (24h)'), 'count' => (string) $scheduledRuns24hCount, @@ -1197,7 +1197,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/scheduled-jobs', 'label' => t('Failed scheduler runs (24h)'), 'count' => (string) $scheduledFailedRuns24hCount, @@ -1208,7 +1208,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/scheduled-jobs', 'label' => t('Cron runner active'), 'count' => $runnerStatusLabel, @@ -1227,7 +1227,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && (
'admin/mail-log?status=failed', 'label' => t('Mail failures'), 'count' => (string) $mailLogFailedCount, @@ -1238,7 +1238,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ]); ?> 'admin/mail-log?status=sent', 'label' => t('Sent emails'), 'count' => (string) $mailLogSentCount, @@ -1250,7 +1250,7 @@ $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ?> 'admin/mail-log?status=sent', 'label' => t('Last email sent'), 'count' => $lastSentLabel, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2b830bc..127aa9c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1428,12 +1428,6 @@ parameters: count: 1 path: core/Support/SearchConfig.php - - - message: '#^Public method "MintyPHP\\Support\\Tile\:\:render\(\)" is never used$#' - identifier: public.method.unused - count: 1 - path: core/Support/Tile.php - - message: '#^Public constant "MintyPHP\\Module\\AddressBook\\AddressBookAuthorizationPolicy\:\:PERMISSION_KEY" is never used$#' identifier: public.classConstant.unused