import { createListPageModule } from '../core/app-list-page-module.js'; import { initUsersListPage } from './app-users-list.js'; import { bindBulkVisibility } from '../components/app-bulk-selection.js'; import { initDetailDrawer } from '../components/app-detail-drawer.js'; import { buildUrl, badgeHtml, escapeHtml, buildBadgeList, withCurrentListReturn } from './app-list-utils.js'; import { showAsyncFlash, withLoading } from '../components/app-async-flash.js'; createListPageModule({ configId: 'admin-users-index', moduleId: 'admin-users-index', missingGridMessage: 'Users grid init failed: Grid.js missing', initErrorMessage: 'Users grid init failed', setup: ({ config, gridjs, appBase, initListPage }) => { const gridSearch = config.gridSearch ?? null; const filterSchema = config.filterSchema ?? []; const usersFilterChipMeta = config.filterChipMeta ?? []; const currentUserUuid = String(config.currentUserUuid || ''); const canUpdateUsers = config.canUpdateUsers === true; const canUpdateSelf = config.canUpdateSelf === true; const csrf = config.csrf ?? null; const labels = config.labels ?? {}; const buildBulkUrl = (action) => buildUrl(appBase, `admin/users/bulk/${action}`); const rowSelection = gridjs?.plugins?.selection?.RowSelection; if (!rowSelection) { throw new Error('Grid.js RowSelection plugin is not available.'); } // 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 = String(row?.cells?.[uuidIndex]?.data ?? ''); if (!uuid) { return false; } if (uuid === currentUserUuid) { return canUpdateUsers || canUpdateSelf; } return canUpdateUsers; }; 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(); return chars || '?'; }; const columns = [ { name: labels.user || 'User', sort: true, formatter: (cell, row) => { 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(`${inner}`); }, }, { 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, formatter: (cell) => cell ? gridjs.html(`${labels.active || 'Active'}`) : gridjs.html(`${labels.inactive || 'Inactive'}`), }, { name: 'UUID', hidden: true }, ]; const { gridConfig } = initListPage({ grid: { gridjs, container: '#users-grid', dataUrl: 'admin/users/data', appBase, columns, sortColumns: ['first_name', null, 'last_login_at', 'active', null], paginationLimit: 10, language: config.gridLang ?? {}, mapData: (data) => data.data.map((row) => [ { firstName: row.first_name ?? '', lastName: row.last_name ?? '', email: row.email ?? '', hasAvatar: !!row.has_avatar, }, row.tenants, row.last_login, row.active, row.uuid ?? '', ]), selection: { enabled: true, id: 'selectRow', component: rowSelection, selectAllLabel: labels.selectAll || 'Select all', props: { id: (row) => String(row.cell(uuidIndex).data ?? ''), }, getSelectedIds: ({ container }) => { if (!container) { return []; } return Array.from(container.querySelectorAll('.gridjs-tr-selected')) .map((row) => row.getAttribute('data-uuid')) .filter(Boolean); }, }, search: gridSearch, filterSchema, urlSync: true, rowDataset: (row) => ({ uuid: String(row?.cells?.[uuidIndex]?.data ?? ''), }), rowInteraction: { linkColumn: false }, rowDblClick: { getUrl: (rowData) => { if (!canEditRow(rowData)) { return ''; } const uuid = String(rowData?.cells?.[uuidIndex]?.data ?? ''); return uuid ? new URL(`admin/users/edit/${uuid}`, appBase).toString() : ''; }, }, }, filters: { mode: 'drawer', chipMeta: usersFilterChipMeta, watchInputs: ['#user-search'], preserveFilterParamsOnReset: ['tenant'], drawer: { countExcludeParams: ['tenant'], }, clearMetaOptions: { preserveFilterParams: ['tenant'], }, }, }) || {}; const usersGridConfig = gridConfig ? { ...gridConfig, indices: { activeIndex, uuidIndex } } : null; initUsersListPage({ gridConfig: usersGridConfig, appBase, csrf, labels, exportUrl: config.exportUrl, buildBulkUrl, bulkDownloadForms: { 'access-pdf': '#users-access-pdf-bulk-form', }, resetOptions: { resetPage: true, urlHistoryMode: 'push', preserveFilterParams: ['tenant'], }, showFlash: showAsyncFlash, withLoading, }); bindBulkVisibility({ containerSelector: '#users-grid', buttonSelector: '[data-users-bulk]', }); initDetailDrawer({ gridConfig, triggerSelector: '[data-drawer-trigger]', rowUuidAttr: 'uuid', fetchUrl: (uuid) => new URL(`admin/users/view-fragment/${uuid}`, appBase).toString(), fullUrl: (uuid) => withCurrentListReturn(new URL(`admin/users/edit/${uuid}`, appBase).toString()), hashPrefix: 'user', }); return usersGridConfig; }, });