diff --git a/cache-bust.json b/cache-bust.json index c77c43c6..d032c795 100644 --- a/cache-bust.json +++ b/cache-bust.json @@ -1,5 +1,5 @@ { - "version": "512de215e15f2828", + "version": "4571b2de4aa33cd0", "generatedBy": "scripts/updateCacheBusting.mjs", "assets": { "assets/app-icon-16.png": "6645731d86da1071", @@ -14,7 +14,7 @@ "assets/favicon-32.png": "c9b411f8fdc302f2", "assets/favicon.svg": "8596af3b51a53409", "backgroundNotificationServiceWorker.js": "2f8933df91d32a1a", - "index.html": "9f9af1d24e2a3e90", + "index.html": "120ac11359474cd7", "site.webmanifest": "7641f91a81ee63a2", "src/appModules.js": "f4135fb45be489bb", "src/components/date-picker/index.js": "c9a9363a3fdae5ee", @@ -54,8 +54,9 @@ "src/core/formatting/valueFormatting.js": "3f1e24d4cdc46608", "src/core/hydrationEta.js": "250ec4ee2633be0f", "src/core/icons.js": "51f534abf118b7e1", + "src/core/libraryScopes.js": "9fd73251bf6e3fcc", "src/core/mobileScrollLock.js": "d197a1dc4a683409", - "src/core/mockQueryBackend.js": "a69b05dccb087495", + "src/core/mockQueryBackend.js": "9147b5dea4adbced", "src/core/operatorSelectUtils.js": "4986a94dce50dcd9", "src/core/queryErrorDetails.js": "5c26531918b7a2c8", "src/core/queryExecution.js": "5fddde4c241c8699", @@ -189,10 +190,10 @@ "src/lib/workbook-export/workbookXmlUtils.js": "2cc0fcdda2a1d411", "src/lib/workbook-export/xlsxZipWriter.js": "5f4c1f6e98fbcb06", "src/styles/api-settings.css": "c5f76b60fb8684bb", - "src/styles/app.css": "09faabcfd35d38aa", + "src/styles/app.css": "410351104445b653", "src/styles/base.css": "8a925806001990b6", "src/styles/bib-compare.css": "144f2a1093e00dcf", - "src/styles/controls.css": "5372b30b175cd1e7", + "src/styles/controls.css": "30d36ccc65181c21", "src/styles/dashboard.css": "3d11ffe059411be7", "src/styles/filters.css": "9e7297e65dd68a3c", "src/styles/form-mode.css": "7f332bdcf58c2df0", @@ -232,15 +233,15 @@ "src/ui/cliPairing.js": "972206be3731df5f", "src/ui/controls/customDatePicker.js": "88e4eb3245ac256c", "src/ui/controls/searchUI.js": "9d46afc995531905", - "src/ui/controls/selectorControls.js": "bfcb8b0a6aba4bd4", + "src/ui/controls/selectorControls.js": "ee4bde178cbce906", "src/ui/controls/selectorListPasteInput.js": "4e7f16d036dfd795", "src/ui/controls/tableNameInput.js": "b6fc884c36961de3", "src/ui/controls/virtualList.js": "bd7a8d24c7c48493", - "src/ui/dashboard/kpiDashboard.js": "a30231379c3c6b6f", + "src/ui/dashboard/kpiDashboard.js": "6f73f5e9f2b62c49", "src/ui/dashboard/kpiDashboardModel.js": "fa4c42b18c1245e5", "src/ui/dashboard/kpiDashboardView.js": "82508c550975ada6", "src/ui/dashboard/libraryDashboardExport.js": "b756c909cbadc7e9", - "src/ui/dashboard/libraryDashboardModel.js": "ff0861a1889a9b32", + "src/ui/dashboard/libraryDashboardModel.js": "526c678143b0373e", "src/ui/dashboard/libraryDashboardView.js": "f46f69224a55ebe3", "src/ui/field-picker/buildableFieldPreview.js": "8abd486bb0a87fe2", "src/ui/field-picker/fieldPicker.js": "526a1bf934f5dd74", @@ -255,7 +256,7 @@ "src/ui/form-mode/formMode.js": "bac8e41c43689e9c", "src/ui/form-mode/formModeCard.js": "ec0b92176b97872b", "src/ui/form-mode/formModeCardBuilder.js": "f906542bfb990667", - "src/ui/form-mode/formModeControls.js": "ce0cb516c6920165", + "src/ui/form-mode/formModeControls.js": "b84b20e082760f7d", "src/ui/form-mode/formModeDateInput.js": "f08093e5d0548828", "src/ui/form-mode/formModeFieldPicker.js": "a6825b92b2a9fbb6", "src/ui/form-mode/formModePresentation.js": "caaa646c76cdf735", diff --git a/index.html b/index.html index 5982889c..56753a41 100644 --- a/index.html +++ b/index.html @@ -833,7 +833,7 @@

- + diff --git a/src/core/libraryScopes.js b/src/core/libraryScopes.js new file mode 100644 index 00000000..a3d5426f --- /dev/null +++ b/src/core/libraryScopes.js @@ -0,0 +1,21 @@ +const ALL_LIBRARY_SYSTEMS_LABEL = 'All library systems'; + +function normalizeLibraryScopeOptions(options = []) { + return (Array.isArray(options) ? options : []).map(option => typeof option === 'string' + ? { value: option, label: option } + : { value: option.value ?? option.code, label: option.label ?? option.name ?? option.code }); +} + +function buildLibraryScopeGroups(systems = [], libraries = []) { + return [ + { label: 'Library systems', options: normalizeLibraryScopeOptions(systems) }, + { label: 'Item libraries', options: normalizeLibraryScopeOptions(libraries) } + ].filter(group => group.options.length > 0); +} + +function systemCodeForLibraryScope(scope = '') { + const normalized = String(scope || '').replace(/^system:/u, ''); + return normalized && normalized !== 'all' ? normalized.split('-')[0].toUpperCase() : ''; +} + +export { ALL_LIBRARY_SYSTEMS_LABEL, buildLibraryScopeGroups, systemCodeForLibraryScope }; diff --git a/src/core/mockQueryBackend.js b/src/core/mockQueryBackend.js index b279f5dc..fbad2a67 100644 --- a/src/core/mockQueryBackend.js +++ b/src/core/mockQueryBackend.js @@ -104,7 +104,7 @@ function demoCalendarPeriods(now = new Date()) { function buildDemoLibraryDashboard(payload = {}, data = {}) { const library = payload.library || 'all'; const itemType = payload.item_type || 'all'; - const scopeFactor = library === 'all' ? 1 : 0.08; + const scopeFactor = library === 'all' ? 1 : library.startsWith('system:') ? 0.25 : 0.08; const typeFactor = itemType === 'all' ? 1 : 0.22; const factor = scopeFactor * typeFactor; const reportingPeriod = String(payload.reporting_period || payload.active_window_days || 365); @@ -141,11 +141,17 @@ function buildDemoLibraryDashboard(payload = {}, data = {}) { const previousCheckouts = scaled(842110); const previousRenewals = scaled(469220); const filters = { + systems: [ + { value: 'system:MSU', code: 'MSU', label: 'Mississippi State University' }, + { value: 'system:MMRLS', code: 'MMRLS', label: 'Mid-Mississippi Regional Library System' }, + { value: 'system:FRL', code: 'FRL', label: 'First Regional Library' }, + { value: 'system:LILS', code: 'LILS', label: 'Lee-Itawamba Library System' } + ], libraries: [ - { value: 'MSU', label: 'Mississippi State University' }, - { value: 'MMRLS', label: 'Mid-Mississippi Regional Library System' }, - { value: 'FRL', label: 'First Regional Library' }, - { value: 'LILS', label: 'Lee-Itawamba Library System' } + { value: 'MSU-MAIN', label: 'MSU Main Library' }, + { value: 'MMRLS-CARTHAGE', label: 'MMRLS Carthage' }, + { value: 'FRL-HERNANDO', label: 'First Regional Hernando' }, + { value: 'LILS-TUPELO', label: 'Lee-Itawamba Tupelo' } ], item_types: ['BOOK', 'EBOOK', 'DVD', 'AUDIOBOOK', 'KIT'], calendar_periods: calendarPeriods, @@ -157,7 +163,7 @@ function buildDemoLibraryDashboard(payload = {}, data = {}) { sample_data: true, scope: { library, - library_label: library === 'all' ? 'All MLP libraries' : (filters.libraries.find(entry => entry.value === library)?.label || library), + library_label: library === 'all' ? 'All MLP libraries' : ([...filters.systems, ...filters.libraries].find(entry => entry.value === library)?.label || library), item_type: itemType, item_type_label: itemType === 'all' ? 'All item types' : itemType, active_window_days: Number(payload.active_window_days || 365), diff --git a/src/styles/app.css b/src/styles/app.css index ae27eb4a..6ca2a25d 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -5,7 +5,7 @@ @import url("./filters.css?v=9e7297e65dd68a3c"); @import url("./form-mode.css?v=7f332bdcf58c2df0"); @import url("./table.css?v=8bc187669db70079"); -@import url("./controls.css?v=5372b30b175cd1e7"); +@import url("./controls.css?v=30d36ccc65181c21"); @import url("./history.css?v=64598bfd565a29af"); @import url("./dashboard.css?v=3d11ffe059411be7"); @import url("./templates.css?v=1c0a1594bedc65b9"); diff --git a/src/styles/controls.css b/src/styles/controls.css index 1cd6dafb..31adf8f8 100644 --- a/src/styles/controls.css +++ b/src/styles/controls.css @@ -523,6 +523,20 @@ input[type="search"]:-webkit-autofill, transition: background-color var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast); } +.selector-all-option { + border: 0; + cursor: pointer; + font: inherit; + margin: 0 0 0.35rem; + text-align: left; + width: 100%; +} + +.selector-all-option.is-selected { + box-shadow: inset 3px 0 0 var(--accent-color); + font-weight: 700; +} + /* ========================================== 12. CUSTOM DATE PICKER ========================================== */ diff --git a/src/ui/controls/selectorControls.js b/src/ui/controls/selectorControls.js index b77faccc..e86aad94 100644 --- a/src/ui/controls/selectorControls.js +++ b/src/ui/controls/selectorControls.js @@ -132,6 +132,8 @@ function createGroupedSelector(values, isMultiSelect, currentValues = [], option const enableGrouping = options.enableGrouping !== false; const groupSelectionLabel = String(options.groupSelectionLabel || '').trim(); const groupSelectionDescription = String(options.groupSelectionDescription || '').trim(); + const allSelectionLabel = String(options.allSelectionLabel || '').trim(); + const allSelectionDescription = String(options.allSelectionDescription || '').trim(); const containerId = Object.prototype.hasOwnProperty.call(options, 'containerId') ? options.containerId : 'condition-select-container'; @@ -165,6 +167,21 @@ function createGroupedSelector(values, isMultiSelect, currentValues = [], option initializeSearchInputs(searchWrapper); container.appendChild(searchWrapper); + let allSelectionButton = null; + if (allSelectionLabel) { + allSelectionButton = document.createElement('button'); + allSelectionButton.type = 'button'; + allSelectionButton.className = 'group-header selector-all-option'; + allSelectionButton.textContent = allSelectionLabel; + allSelectionButton.title = allSelectionDescription; + allSelectionButton.addEventListener('click', () => { + selectedValues.clear(); + container.dispatchEvent(new Event('change', { bubbles: true })); + rebuildVisibleRows(); + }); + container.appendChild(allSelectionButton); + } + const optionsContainer = document.createElement('div'); optionsContainer.className = 'grouped-options-container grouped-options-container--virtualized'; container.appendChild(optionsContainer); @@ -288,6 +305,11 @@ function createGroupedSelector(values, isMultiSelect, currentValues = [], option const searchTerm = searchInput.value.toLowerCase().trim(); visibleRows = []; optionIndex.clear(); + if (allSelectionButton) { + const selected = selectedValues.size === 0; + allSelectionButton.classList.toggle('is-selected', selected); + allSelectionButton.setAttribute('aria-pressed', selected ? 'true' : 'false'); + } topLevelEntries .slice() diff --git a/src/ui/dashboard/kpiDashboard.js b/src/ui/dashboard/kpiDashboard.js index d247c87d..338659dc 100644 --- a/src/ui/dashboard/kpiDashboard.js +++ b/src/ui/dashboard/kpiDashboard.js @@ -1,6 +1,7 @@ import { BackendApi } from '../../core/backendApi.js'; import { appServices } from '../../core/appServices.js'; import { getClientErrorMessage } from '../../core/clientErrorMessages.js'; +import { ALL_LIBRARY_SYSTEMS_LABEL, buildLibraryScopeGroups, systemCodeForLibraryScope } from '../../core/libraryScopes.js'; import { onDOMReady } from '../../core/domReady.js'; import { libraryDashboardHasData, normalizeLibraryDashboard } from './libraryDashboardModel.js'; import { downloadLibraryDashboardCsv } from './libraryDashboardExport.js'; @@ -46,9 +47,28 @@ function replaceOptions(select, baseLabel, options, selected) { select.value = normalized.some(option => option.value === selected) ? selected : 'all'; } +function replaceLibraryOptions(select, systems, libraries, selected) { + if (!select) return; + const groups = buildLibraryScopeGroups(systems, libraries); + const options = groups.flatMap(group => group.options); + const all = new Option(ALL_LIBRARY_SYSTEMS_LABEL, 'all'); + select.replaceChildren(all, ...groups.map(group => { + const element = document.createElement('optgroup'); + element.label = group.label; + element.append(...group.options.map(option => new Option(option.label, option.value))); + return element; + })); + select.value = options.some(option => option.value === selected) ? selected : 'all'; +} + function syncFilterOptions(elements) { if (!libraryData) return; - replaceOptions(elements.library, 'All MLP libraries', libraryData.filters.libraries, elements.library?.value || 'all'); + replaceLibraryOptions( + elements.library, + libraryData.filters.systems, + libraryData.filters.libraries, + elements.library?.value || 'all' + ); replaceOptions(elements.itemType, 'All item types', libraryData.filters.itemTypes, elements.itemType?.value || 'all'); syncPeriodOptions(elements); } @@ -57,7 +77,7 @@ function syncPeriodOptions(elements) { if (!elements.period || !libraryData) return; const selected = elements.period.value || '365'; const library = elements.library?.value || 'all'; - const system = library === 'all' ? '' : String(library).split('-')[0].toUpperCase(); + const system = systemCodeForLibraryScope(library); const rolling = [ { value: '90', label: 'Last 90 days' }, { value: '365', label: 'Last 12 months' }, diff --git a/src/ui/dashboard/libraryDashboardModel.js b/src/ui/dashboard/libraryDashboardModel.js index ffca4ae9..d6052f29 100644 --- a/src/ui/dashboard/libraryDashboardModel.js +++ b/src/ui/dashboard/libraryDashboardModel.js @@ -49,6 +49,7 @@ function normalizeLibraryDashboard(payload = {}) { patronStateBreakdown: normalizeSeries(data.patron_state_breakdown), opportunities: Array.isArray(data.opportunities) ? data.opportunities : [], filters: { + systems: Array.isArray(data.filters?.systems) ? data.filters.systems : [], libraries: Array.isArray(data.filters?.libraries) ? data.filters.libraries : [], itemTypes: Array.isArray(data.filters?.item_types) ? data.filters.item_types : [], calendarPeriods: Array.isArray(data.filters?.calendar_periods) ? data.filters.calendar_periods : [], diff --git a/src/ui/form-mode/formModeControls.js b/src/ui/form-mode/formModeControls.js index 06fa8a9b..ac30216f 100644 --- a/src/ui/form-mode/formModeControls.js +++ b/src/ui/form-mode/formModeControls.js @@ -8,6 +8,7 @@ import { SelectorControls } from '../controls/selectorControls.js'; import { fieldDefs, getFieldFilterOperators, isFieldBackendFilterable } from '../../features/filters/fieldDefs.js'; import { CustomDatePicker } from '../controls/customDatePicker.js'; import { createFormModeDateInputState } from './formModeDateInput.js'; +import { ALL_LIBRARY_SYSTEMS_LABEL } from '../../core/libraryScopes.js'; function parseFieldOptions(fieldDef, inputSpec, normalizeOperatorForField) { const source = Array.isArray(inputSpec.options) && inputSpec.options.length > 0 @@ -382,6 +383,8 @@ import { createFormModeDateInputState } from './formModeDateInput.js'; if (SelectorControls.createGroupedSelector) { const selector = SelectorControls.createGroupedSelector(values, isMultiSelect, initialValues, { enableGrouping: shouldGroupValues && hasDashes, + allSelectionLabel: fieldDef?.allSelectionLabel || fieldDef?.all_selection_label || (shouldGroupValues ? ALL_LIBRARY_SYSTEMS_LABEL : ''), + allSelectionDescription: fieldDef?.allSelectionDescription || fieldDef?.all_selection_description || '', groupSelectionLabel: fieldDef?.groupSelectionLabel || fieldDef?.group_selection_label || '', groupSelectionDescription: fieldDef?.groupSelectionDescription || fieldDef?.group_selection_description || '', containerId: null @@ -389,7 +392,7 @@ import { createFormModeDateInputState } from './formModeDateInput.js'; return createPopupListControl( selector, inputSpec.label || (fieldDef && fieldDef.name) || 'Select values', - inputSpec.placeholder || (isMultiSelect ? 'Click to select values…' : 'Click to select a value…') + inputSpec.placeholder || fieldDef?.allSelectionLabel || fieldDef?.all_selection_label || (isMultiSelect ? 'Click to select values…' : 'Click to select a value…') ); } diff --git a/tests/browser/browserSmoke.mjs b/tests/browser/browserSmoke.mjs index e68c7d7f..7862beb9 100644 --- a/tests/browser/browserSmoke.mjs +++ b/tests/browser/browserSmoke.mjs @@ -520,6 +520,7 @@ async function runSmokeTest() { chartCount: panel.querySelectorAll('.kpi-chart-card').length, opportunityRows: panel.querySelectorAll('.kpi-opportunity-table tbody tr').length, libraryOptions: Array.from(panel.querySelectorAll('#kpi-dashboard-library option')).map(option => option.value), + libraryGroups: Array.from(panel.querySelectorAll('#kpi-dashboard-library optgroup')).map(group => group.label), exportVisible: !panel.querySelector('#kpi-dashboard-export')?.classList.contains('hidden'), comparisonText: panel.querySelector('.kpi-card')?.textContent || '', selectedTab: panel.querySelector('[data-kpi-view][aria-selected="true"]')?.dataset.kpiView || '' @@ -530,7 +531,10 @@ async function runSmokeTest() { || dashboardState.cardValues[2] !== '2,813,442' || dashboardState.chartCount !== 6 || dashboardState.opportunityRows !== 1 - || !dashboardState.libraryOptions.includes('MSU') + || !dashboardState.libraryOptions.includes('system:MSU') + || !dashboardState.libraryOptions.includes('MSU-MAIN') + || !dashboardState.libraryGroups.includes('Library systems') + || !dashboardState.libraryGroups.includes('Item libraries') || !dashboardState.exportVisible || !/up 38,119/iu.test(dashboardState.comparisonText) || dashboardState.selectedTab !== 'overview' @@ -554,7 +558,7 @@ async function runSmokeTest() { } await page.locator('#kpi-dashboard-window').selectOption('cy:2026'); await page.waitForFunction(() => document.querySelector('#kpi-dashboard-content .kpi-card')?.textContent?.includes('Calendar Year 2026 to date')); - await page.locator('#kpi-dashboard-library').selectOption('MSU'); + await page.locator('#kpi-dashboard-library').selectOption('system:MSU'); await page.waitForFunction(() => Array.from(document.querySelectorAll('#kpi-dashboard-window option')).some(option => option.value === 'fy:MSU:2027')); if (!await page.locator('#kpi-dashboard-window optgroup[label="Fiscal years"]').count()) { throw new Error('Dashboard should expose fiscal years as a distinct reporting-period group after choosing a system.'); diff --git a/tests/browser/support/browserSmokeSupport.mjs b/tests/browser/support/browserSmokeSupport.mjs index 85d73644..b7818fd9 100644 --- a/tests/browser/support/browserSmokeSupport.mjs +++ b/tests/browser/support/browserSmokeSupport.mjs @@ -293,7 +293,7 @@ function buildDefaultQueryApiResponse(payload) { body: JSON.stringify({ schema_version: 1, generated_at: new Date().toISOString(), - scope: { library: payload.library || 'all', library_label: payload.library === 'MSU' ? 'Mississippi State University' : 'All MLP libraries', item_type_label: 'All item types', reporting_period: payload.reporting_period || '365' }, + scope: { library: payload.library || 'all', library_label: payload.library === 'system:MSU' ? 'Mississippi State University' : 'All MLP libraries', item_type_label: 'All item types', reporting_period: payload.reporting_period || '365' }, circulation: { checkouts: 880229, renewals: 487605, previous_checkouts: 842110, previous_renewals: 469220, checkout_change: 38119, renewal_change: 18385, checkout_change_rate: 0.045, renewal_change_rate: 0.039, comparison_available: true, comparison_coverage_complete: true, period_label: payload.reporting_period === 'fy:MSU:2027' ? 'FY 2027 to date (Jul 1, 2026–Aug 21, 2026)' : payload.reporting_period === 'cy:2026' ? 'Calendar Year 2026 to date' : 'Recent 365 days', renewal_share: 0.356, holds: 121843, holds_per_100_items: 4.3 }, collection: { items: 2813442, titles: 1601291, lifetime_checkouts: 12844308, lifetime_renewals: 5160244, in_house_uses: 843108, used_recently: 947835, recent_use_rate: 0.337, never_used: 1023995, never_used_rate: 0.364, checkouts_per_item: 4.6, total_value: 42850300, price_coverage: 0.71 }, patrons: { total: 618420, active: 183804, active_rate: 0.297, new: 42640, with_charges: 74482, with_holds: 18814, expiring_soon: 29711 }, @@ -309,7 +309,7 @@ function buildDefaultQueryApiResponse(payload) { patron_city_breakdown: [{ label: 'Tupelo, MS', patrons: 49220 }, { label: 'Other / unknown', patrons: 420700 }], patron_state_breakdown: [{ label: 'Mississippi', patrons: 582400 }, { label: 'Other / unknown', patrons: 36020 }], opportunities: [{ label: 'Older items with no recorded use', count: 618220, detail: 'Created more than five years ago with zero lifetime checkouts.' }], - filters: { libraries: [{ value: 'MSU', label: 'Mississippi State University' }], item_types: ['BOOK', 'EBOOK'], calendar_periods: [{ value: 'cy:2026', label: 'Calendar Year 2026 to date' }, { value: 'cy:2025', label: 'Calendar Year 2025' }], fiscal_periods_by_system: { MSU: [{ value: 'fy:MSU:2027', label: 'FY 2027 to date (Jul 1, 2026–Aug 21, 2026)', date_span: 'Jul 1, 2026–Aug 21, 2026' }, { value: 'fy:MSU:2026', label: 'FY 2026 (Jul 1, 2025–Jun 30, 2026)', date_span: 'Jul 1, 2025–Jun 30, 2026' }] } }, + filters: { systems: [{ value: 'system:MSU', code: 'MSU', label: 'Mississippi State University' }], libraries: [{ value: 'MSU-MAIN', label: 'MSU Main Library' }], item_types: ['BOOK', 'EBOOK'], calendar_periods: [{ value: 'cy:2026', label: 'Calendar Year 2026 to date' }, { value: 'cy:2025', label: 'Calendar Year 2025' }], fiscal_periods_by_system: { MSU: [{ value: 'fy:MSU:2027', label: 'FY 2027 to date (Jul 1, 2026–Aug 21, 2026)', date_span: 'Jul 1, 2026–Aug 21, 2026' }, { value: 'fy:MSU:2026', label: 'FY 2026 (Jul 1, 2025–Jun 30, 2026)', date_span: 'Jul 1, 2025–Jun 30, 2026' }] } }, privacy: { suppression_threshold: 10 }, sources: [{ label: 'Current item snapshot', detail: 'Aggregated test data.' }] }), diff --git a/tests/unit/core/mockQueryBackendLogic.mjs b/tests/unit/core/mockQueryBackendLogic.mjs index e46119c5..61cfb80d 100644 --- a/tests/unit/core/mockQueryBackendLogic.mjs +++ b/tests/unit/core/mockQueryBackendLogic.mjs @@ -75,13 +75,13 @@ test('demo dashboard status includes clearly marked sample operational activity' test('demo library dashboard demonstrates comparisons, fiscal periods, and privacy-safe geography', async () => { const initialResponse = await handleDemoQueryRequest({ - body: JSON.stringify({ action: 'library_dashboard', library: 'MSU' }), + body: JSON.stringify({ action: 'library_dashboard', library: 'system:MSU' }), headers: authHeaders }); const initial = await initialResponse.json(); const reportingPeriod = initial.filters.fiscal_periods_by_system.MSU[0].value; const response = await handleDemoQueryRequest({ - body: JSON.stringify({ action: 'library_dashboard', library: 'MSU', reporting_period: reportingPeriod }), + body: JSON.stringify({ action: 'library_dashboard', library: 'system:MSU', reporting_period: reportingPeriod }), headers: authHeaders }); const payload = await response.json(); @@ -89,6 +89,8 @@ test('demo library dashboard demonstrates comparisons, fiscal periods, and priva assert.equal(payload.scope.reporting_period, reportingPeriod); assert.equal(payload.circulation.comparison_available, true); assert.equal(payload.circulation.fiscal_system, 'MSU'); + assert.equal(payload.scope.library, 'system:MSU'); + assert.ok(payload.filters.systems.some(system => system.value === 'system:MSU')); assert.match(payload.circulation.period_label, /^FY \d{4}.*\([A-Z][a-z]{2} \d{1,2}, \d{4}–[A-Z][a-z]{2} \d{1,2}, \d{4}\)$/u); assert.match(initial.filters.fiscal_periods_by_system.MSU[0].date_span, /^[A-Z][a-z]{2} \d{1,2}, \d{4}–[A-Z][a-z]{2} \d{1,2}, \d{4}$/u); assert.ok(payload.filters.fiscal_periods_by_system.MSU.length >= 3); diff --git a/tests/unit/ui/libraryDashboardModelLogic.mjs b/tests/unit/ui/libraryDashboardModelLogic.mjs index 5c5ee2dc..ba09eca5 100644 --- a/tests/unit/ui/libraryDashboardModelLogic.mjs +++ b/tests/unit/ui/libraryDashboardModelLogic.mjs @@ -10,6 +10,7 @@ test('library dashboard normalizes aggregate groups and filter metadata', () => patrons: { total: 32 }, library_breakdown: [{ label: 'Main', checkouts: '30' }], filters: { + systems: [{ value: 'system:MAIN', label: 'Main System' }], libraries: [{ value: 'MAIN', label: 'Main' }], item_types: ['BOOK'], calendar_periods: [{ value: 'cy:2026', label: 'Calendar Year 2026 to date' }] @@ -21,6 +22,7 @@ test('library dashboard normalizes aggregate groups and filter metadata', () => assert.equal(dashboard.circulation.coverage_complete, true); assert.equal(dashboard.libraryBreakdown[0].checkouts, 30); assert.equal(dashboard.filters.libraries[0].value, 'MAIN'); + assert.equal(dashboard.filters.systems[0].value, 'system:MAIN'); assert.equal(dashboard.filters.calendarPeriods[0].value, 'cy:2026'); assert.deepEqual(dashboard.availability, { circulation: true, collection: true, patrons: true }); assert.equal(libraryDashboardHasData(dashboard), true);