diff --git a/pages/admin/tenants/data().php b/pages/admin/tenants/data().php index 5be8377..d8763d8 100644 --- a/pages/admin/tenants/data().php +++ b/pages/admin/tenants/data().php @@ -19,11 +19,10 @@ $order = $filters['order']; $dir = $filters['dir']; $computedOrderKeys = ['users']; $userTenantRepository = app(\MintyPHP\Repository\Tenant\UserTenantRepository::class); -$tenantLogoService = app(\MintyPHP\Service\Tenant\TenantLogoService::class); $settingsDefaultsGateway = app(\MintyPHP\Service\Settings\SettingsDefaultsGateway::class); $gridUserCountEnricher = app(\MintyPHP\Service\Data\GridUserCountEnricher::class); -$fetchRows = static function (array $tenantRows) use ($userTenantRepository, $tenantLogoService, $settingsDefaultsGateway, $gridUserCountEnricher): array { +$fetchRows = static function (array $tenantRows) use ($userTenantRepository, $settingsDefaultsGateway, $gridUserCountEnricher): array { $userCounts = $gridUserCountEnricher->computeCounts( $tenantRows, $userTenantRepository->countUsersByTenantIds(...), @@ -47,10 +46,6 @@ $fetchRows = static function (array $tenantRows) use ($userTenantRepository, $te 'status_badge' => $tenantStatus->badgeVariant(), 'status_label' => t($tenantStatus->labelToken()), 'total_users' => $counts['active_users'] + $counts['inactive_users'], - 'has_logo' => $tenantUuid !== '' && ( - $tenantLogoService->hasLogo($tenantUuid, \MintyPHP\Service\Tenant\TenantLogoService::THEME_LIGHT) - || $tenantLogoService->hasLogo($tenantUuid, \MintyPHP\Service\Tenant\TenantLogoService::THEME_DARK) - ), ]; } diff --git a/pages/admin/tenants/index(default).phtml b/pages/admin/tenants/index(default).phtml index c391bac..e394697 100644 --- a/pages/admin/tenants/index(default).phtml +++ b/pages/admin/tenants/index(default).phtml @@ -43,7 +43,6 @@ $pageConfig = [ 'filterChipMeta' => $filterChipMeta, 'gridLang' => $gridLang, 'labels' => [ - 'logo' => t('Logo'), 'tenant' => t('Tenant'), 'users' => t('Users'), ], diff --git a/pages/admin/users/index(default).phtml b/pages/admin/users/index(default).phtml index 4f3213e..38045cb 100644 --- a/pages/admin/users/index(default).phtml +++ b/pages/admin/users/index(default).phtml @@ -117,19 +117,9 @@ $pageConfig = [ 'csrf' => $gridCsrf, 'gridLang' => $gridLang, 'labels' => [ - 'avatar' => t('Avatar'), - 'firstName' => t('First name'), - 'lastName' => t('Last name'), - 'email' => t('Email'), + 'user' => t('User'), 'state' => t('State'), 'tenants' => t('Tenants'), - 'departments' => t('Departments'), - 'roles' => t('Roles'), - 'phone' => t('Phone'), - 'mobile' => t('Mobile'), - 'shortDial' => t('Short dial'), - 'created' => t('Created'), - 'modified' => t('Modified'), 'lastLogin' => t('Last login'), 'selectAll' => t('Select all'), 'active' => t('Active'), diff --git a/web/css/vendor-overrides/gridjs.css b/web/css/vendor-overrides/gridjs.css index 48ec52b..84863b4 100644 --- a/web/css/vendor-overrides/gridjs.css +++ b/web/css/vendor-overrides/gridjs.css @@ -248,12 +248,15 @@ same custom properties, not by setting `color`/`background-color` directly, and we must use a token that isn't shadowed inside the button scope (`--app-form-element-color` fits). */ + /* Align pagination buttons with the Stripe-style neutral-chip secondary + outline: theme-aware surface, high-contrast text, subtle border, + raised shadow. Current page stays distinct via primary fill. */ .gridjs-pagination .gridjs-pages button, .gridjs-pagination .gridjs-pages button[type="button"] { - --app-background-color: var(--app-form-element-background-color); - --app-color: var(--app-form-element-color); - --app-border-color: var(--app-table-border-color); - --app-box-shadow: none; + --app-background-color: var(--app-button-neutral-bg); + --app-color: var(--app-button-neutral-color); + --app-border-color: var(--app-button-neutral-border); + box-shadow: var(--app-button-neutral-shadow); margin: 0; display: inline-flex; align-items: center; @@ -263,21 +266,24 @@ line-height: 1.3; min-width: 2rem; white-space: nowrap; - font-weight: 500; + font-weight: var(--font-medium); } .gridjs-pagination .gridjs-pages button:hover:not(:disabled), .gridjs-pagination .gridjs-pages button[type="button"]:hover:not(:disabled), .gridjs-pagination .gridjs-pages button:focus:not(:disabled), .gridjs-pagination .gridjs-pages button[type="button"]:focus:not(:disabled) { - --app-background-color: var(--app-primary-hover-background); - --app-border-color: var(--app-primary-hover-border); - --app-color: var(--app-primary-inverse); + --app-background-color: var(--app-button-neutral-bg-hover); + --app-color: var(--app-button-neutral-color-hover); + --app-border-color: var(--app-button-neutral-border-hover); + box-shadow: var(--app-button-neutral-shadow-hover); } .gridjs-pagination .gridjs-pages button:focus-visible { outline: none; - box-shadow: 0 0 0 var(--app-outline-width) var(--app-primary-focus); + box-shadow: + var(--app-button-neutral-shadow), + 0 0 0 var(--app-outline-width) var(--app-primary-focus); } .gridjs-pagination .gridjs-pages button.gridjs-currentPage, @@ -287,16 +293,18 @@ --app-background-color: var(--app-primary); --app-border-color: var(--app-primary); --app-color: var(--app-primary-inverse); - font-weight: 600; + box-shadow: var(--app-button-filled-shadow); + font-weight: var(--font-semibold); } .gridjs-pagination .gridjs-pages button:disabled, .gridjs-pagination .gridjs-pages button[disabled] { - --app-background-color: var(--app-background-color); + --app-background-color: var(--app-button-neutral-bg); --app-color: var(--app-muted-color); - --app-border-color: var(--app-table-border-color); + --app-border-color: var(--app-button-neutral-border); + box-shadow: none; cursor: default; - opacity: 0.7; + opacity: 0.55; } .gridjs-pagination .gridjs-pages button.gridjs-spread, @@ -634,6 +642,42 @@ font-weight: 500; } + /* Compound user cell: avatar + name (strong) + email subtitle (small, muted). */ + .grid-user-profile { + display: inline-flex; + align-items: center; + gap: 0.75rem; + min-width: 0; + } + + .grid-user-profile-text { + display: inline-flex; + flex-direction: column; + min-width: 0; + line-height: var(--leading-tight); + } + + .grid-user-profile-text strong { + font-weight: var(--font-medium); + color: var(--app-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .grid-user-profile-text small { + font-size: var(--text-xs); + color: var(--app-muted-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + a.grid-user-profile.app-grid-link-cell:hover .grid-user-profile-text strong { + color: var(--app-primary); + text-decoration: underline; + } + .gridjs-container .grid-actions { display: inline-flex; align-items: center; @@ -664,6 +708,7 @@ .gridjs-td .badge, .gridjs-td .grid-name-cell, .gridjs-td .grid-tenant-profile, + .gridjs-td .grid-user-profile, .gridjs-td .app-inline-actions { white-space: nowrap; } diff --git a/web/js/pages/admin-tenants-index.js b/web/js/pages/admin-tenants-index.js index 6400bec..bfc6121 100644 --- a/web/js/pages/admin-tenants-index.js +++ b/web/js/pages/admin-tenants-index.js @@ -12,14 +12,6 @@ createListPageModule({ const labels = config.labels ?? {}; const tenantUuidIndex = 2; - const initialsForRow = (row) => { - const descriptionCell = row?.cells?.[0]?.data; - const label = typeof descriptionCell === 'object' && descriptionCell !== null - ? String(descriptionCell.label ?? '') - : String(descriptionCell ?? ''); - const initial = label.trim().charAt(0).toUpperCase(); - return initial || '?'; - }; const { gridConfig } = initListPage({ grid: { @@ -32,27 +24,7 @@ createListPageModule({ name: labels.tenant || 'Tenant', sort: true, width: '75%', - formatter: (cell, row) => { - const hasLogo = cell?.has_logo ? 1 : 0; - const uuid = encodeURIComponent(String(cell?.uuid ?? '')); - let avatarHtml = ''; - if (hasLogo && uuid) { - const src = new URL(`admin/tenants/logo-file?uuid=${uuid}&theme=light&size=64`, appBase).toString(); - avatarHtml = ``; - } else { - const initials = escapeHtml(initialsForRow(row)); - avatarHtml = `${initials}`; - } - - const nameText = escapeHtml(String(cell?.label ?? '')); - - return gridjs.html( - `
` + - `${avatarHtml}` + - `${nameText}` + - `
` - ); - }, + formatter: (cell) => gridjs.html(escapeHtml(String(cell ?? ''))), }, { name: labels.users || 'Users', @@ -72,11 +44,7 @@ createListPageModule({ paginationLimit: 10, language: config.gridLang ?? {}, mapData: (data) => data.data.map((row) => [ - { - uuid: row.uuid ?? '', - label: row.description ?? '', - has_logo: row.has_logo ? 1 : 0, - }, + row.description ?? '', row.total_users ?? 0, row.uuid, ]), diff --git a/web/js/pages/admin-users-index.js b/web/js/pages/admin-users-index.js index 409d53d..50eab94 100644 --- a/web/js/pages/admin-users-index.js +++ b/web/js/pages/admin-users-index.js @@ -26,10 +26,20 @@ createListPageModule({ if (!rowSelection) { throw new Error('Grid.js RowSelection plugin is not available.'); } - const activeIndex = 6; - const uuidIndex = 2; + + // Layout: + // col 0: User compound (display) + // col 1: Tenants + // col 2: Last login + // col 3: State + // col 4: UUID (hidden scalar) + // Selection plugin prepends a checkbox cell, so runtime cells[] are + // shifted by +1. Cell accessors therefore use indices 1..5 (1-based + // from the user-visible column order + 1 for the selection col). + const uuidIndex = 5; + const activeIndex = 4; const canEditRow = (row) => { - const uuid = row?.cells?.[uuidIndex]?.data; + const uuid = String(row?.cells?.[uuidIndex]?.data ?? ''); if (!uuid) { return false; } if (uuid === currentUserUuid) { return canUpdateUsers || canUpdateSelf; @@ -37,10 +47,8 @@ createListPageModule({ return canUpdateUsers; }; - const initialsForRow = (row) => { - const first = row?.cells?.[3]?.data ?? ''; - const last = row?.cells?.[4]?.data ?? ''; - const parts = [first, last] + const initialsFromUser = (user) => { + const parts = [user?.firstName, user?.lastName] .map((value) => String(value || '').trim()) .filter(Boolean); const chars = parts.map((value) => value[0] || '').join('').toUpperCase(); @@ -49,32 +57,38 @@ createListPageModule({ const columns = [ { - name: labels.avatar || 'Avatar', - sort: false, - formatter: (cell, row) => { - if (!cell) { - const initials = escapeHtml(initialsForRow(row)); - return gridjs.html(`${initials}`); - } - const src = new URL(`admin/users/avatar-file?uuid=${cell}&size=64`, appBase).toString(); - const full = new URL(`admin/users/avatar-file?uuid=${cell}&size=256`, appBase).toString(); - return gridjs.html(``); - }, - }, - { name: 'UUID', hidden: true }, - { - name: labels.firstName || 'First name', + name: labels.user || 'User', sort: true, formatter: (cell, row) => { - const name = escapeHtml(String(cell || '').trim() || '-'); - const uuid = row?.cells?.[uuidIndex]?.data; - if (!uuid) { return gridjs.html(`${name}`); } + const uuid = String(row?.cells?.[uuidIndex]?.data ?? ''); + const firstName = String(cell?.firstName ?? '').trim(); + const lastName = String(cell?.lastName ?? '').trim(); + const email = String(cell?.email ?? '').trim(); + const fullName = [firstName, lastName].filter(Boolean).join(' ') || email || '-'; + const avatarHtml = cell?.hasAvatar && uuid + ? `` + : `${escapeHtml(initialsFromUser(cell))}`; + const nameEsc = escapeHtml(fullName); + const emailEsc = escapeHtml(email); + const subtitle = email && email !== fullName ? `${emailEsc}` : ''; + const inner = `${avatarHtml}${nameEsc}${subtitle}`; + if (!uuid) { + return gridjs.html(`
${inner}
`); + } const editUrl = escapeHtml(withCurrentListReturn(new URL(`admin/users/edit/${uuid}`, appBase).toString())); - return gridjs.html(`${name}`); + return gridjs.html(`${inner}`); }, }, - { name: labels.lastName || 'Last name', sort: true }, - { name: labels.email || 'Email', sort: true }, + { + name: labels.tenants || 'Tenants', + sort: false, + formatter: (cell) => gridjs.html(buildBadgeList(cell, { primaryTooltip: labels.primaryTenant || '' })), + }, + { + name: labels.lastLogin || 'Last login', + sort: true, + formatter: (cell) => badgeHtml(gridjs, cell || labels.never || 'Never'), + }, { name: labels.state || 'State', sort: true, @@ -82,24 +96,7 @@ createListPageModule({ ? gridjs.html(`${labels.active || 'Active'}`) : gridjs.html(`${labels.inactive || 'Inactive'}`), }, - { - name: labels.tenants || 'Tenants', - sort: false, - formatter: (cell) => gridjs.html(buildBadgeList(cell, { primaryTooltip: labels.primaryTenant || '' })), - }, - { name: labels.departments || 'Departments', sort: false, formatter: (cell) => gridjs.html(buildBadgeList(cell)) }, - { name: labels.roles || 'Roles', sort: false, formatter: (cell) => gridjs.html(buildBadgeList(cell)) }, - { name: labels.phone || 'Phone', sort: false }, - { name: labels.mobile || 'Mobile', sort: false }, - { name: labels.shortDial || 'Short dial', sort: false }, - { name: labels.created || 'Created', sort: true, formatter: (cell) => badgeHtml(gridjs, cell) }, - { name: labels.modified || 'Modified', sort: true, formatter: (cell) => badgeHtml(gridjs, cell) }, - { - name: labels.lastLogin || 'Last login', - sort: true, - formatter: (cell) => badgeHtml(gridjs, cell || labels.never || 'Never'), - }, - { name: 'ID', hidden: true }, + { name: 'UUID', hidden: true }, ]; const { gridConfig } = initListPage({ @@ -109,26 +106,20 @@ createListPageModule({ dataUrl: 'admin/users/data', appBase, columns, - sortColumns: [null, null, 'first_name', 'last_name', 'email', 'active', null, null, null, null, null, null, 'created', 'modified', 'last_login_at', null], + sortColumns: ['first_name', null, 'last_login_at', 'active', null], paginationLimit: 10, language: config.gridLang ?? {}, mapData: (data) => data.data.map((row) => [ - row.has_avatar ? row.uuid : '', - row.uuid, - row.first_name, - row.last_name, - row.email, - row.active, + { + firstName: row.first_name ?? '', + lastName: row.last_name ?? '', + email: row.email ?? '', + hasAvatar: !!row.has_avatar, + }, row.tenants, - row.departments, - row.roles, - row.phone, - row.mobile, - row.short_dial, - row.created, - row.modified, row.last_login, - row.id, + row.active, + row.uuid ?? '', ]), selection: { enabled: true, @@ -136,7 +127,7 @@ createListPageModule({ component: rowSelection, selectAllLabel: labels.selectAll || 'Select all', props: { - id: (row) => row.cell(uuidIndex).data, + id: (row) => String(row.cell(uuidIndex).data ?? ''), }, getSelectedIds: ({ container }) => { if (!container) { return []; } @@ -149,7 +140,7 @@ createListPageModule({ filterSchema, urlSync: true, rowDataset: (row) => ({ - uuid: row?.cells?.[uuidIndex]?.data, + uuid: String(row?.cells?.[uuidIndex]?.data ?? ''), }), rowInteraction: { linkColumn: false }, rowDblClick: { @@ -157,7 +148,7 @@ createListPageModule({ if (!canEditRow(rowData)) { return ''; } - const uuid = rowData?.cells?.[uuidIndex]?.data; + const uuid = String(rowData?.cells?.[uuidIndex]?.data ?? ''); return uuid ? new URL(`admin/users/edit/${uuid}`, appBase).toString() : ''; }, },