Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cache-bust.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "5be459fb48f09ce6",
"version": "512de215e15f2828",
"generatedBy": "scripts/updateCacheBusting.mjs",
"assets": {
"assets/app-icon-16.png": "6645731d86da1071",
Expand Down Expand Up @@ -55,7 +55,7 @@
"src/core/hydrationEta.js": "250ec4ee2633be0f",
"src/core/icons.js": "51f534abf118b7e1",
"src/core/mobileScrollLock.js": "d197a1dc4a683409",
"src/core/mockQueryBackend.js": "3b45645564fad841",
"src/core/mockQueryBackend.js": "a69b05dccb087495",
"src/core/operatorSelectUtils.js": "4986a94dce50dcd9",
"src/core/queryErrorDetails.js": "5c26531918b7a2c8",
"src/core/queryExecution.js": "5fddde4c241c8699",
Expand Down Expand Up @@ -236,11 +236,11 @@
"src/ui/controls/selectorListPasteInput.js": "4e7f16d036dfd795",
"src/ui/controls/tableNameInput.js": "b6fc884c36961de3",
"src/ui/controls/virtualList.js": "bd7a8d24c7c48493",
"src/ui/dashboard/kpiDashboard.js": "0d3f87a50b0747e8",
"src/ui/dashboard/kpiDashboard.js": "a30231379c3c6b6f",
"src/ui/dashboard/kpiDashboardModel.js": "fa4c42b18c1245e5",
"src/ui/dashboard/kpiDashboardView.js": "82508c550975ada6",
"src/ui/dashboard/libraryDashboardExport.js": "b756c909cbadc7e9",
"src/ui/dashboard/libraryDashboardModel.js": "dece4ebdde8f32cf",
"src/ui/dashboard/libraryDashboardModel.js": "ff0861a1889a9b32",
"src/ui/dashboard/libraryDashboardView.js": "f46f69224a55ebe3",
"src/ui/field-picker/buildableFieldPreview.js": "8abd486bb0a87fe2",
"src/ui/field-picker/fieldPicker.js": "526a1bf934f5dd74",
Expand Down
39 changes: 35 additions & 4 deletions src/core/mockQueryBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ function demoFiscalPeriods(system, startMonth, source, now = new Date()) {
const shiftedDay = Math.min(Number(value.slice(6, 8)), new Date(Date.UTC(shiftedYear, shiftedMonth, 0)).getUTCDate());
return compact(shiftedYear, shiftedMonth, shiftedDay);
};
const displayDate = value => {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return `${months[Number(value.slice(4, 6)) - 1]} ${Number(value.slice(6, 8))}, ${value.slice(0, 4)}`;
};
return [0, 1, 2].map(offset => {
const fiscalYear = currentFiscalYear - offset;
const startYear = fiscalYear - 1;
Expand All @@ -66,12 +70,33 @@ function demoFiscalPeriods(system, startMonth, source, now = new Date()) {
const current = offset === 0;
const start = compact(startYear, startMonth, 1);
const end = current ? compact(year, month, now.getUTCDate()) : compact(fiscalYear, endMonth, endDay);
const dateSpan = `${displayDate(start)}–${displayDate(end)}`;
return {
value: `fy:${system}:${fiscalYear}`,
label: current ? `FY ${fiscalYear} to date` : `FY ${fiscalYear}`,
label: current ? `FY ${fiscalYear} to date (${dateSpan})` : `FY ${fiscalYear} (${dateSpan})`,
system, fiscal_year: fiscalYear, start, end,
previous_start: shiftYear(start), previous_end: shiftYear(end),
current_to_date: current, start_month: startMonth, source
current_to_date: current, start_month: startMonth,
start_label: displayDate(start), end_label: displayDate(end), date_span: dateSpan, source
};
});
}

function demoCalendarPeriods(now = new Date()) {
const currentYear = now.getUTCFullYear();
const compact = (year, month, day) => `${year}${String(month).padStart(2, '0')}${String(day).padStart(2, '0')}`;
return [0, 1, 2].map(offset => {
const calendarYear = currentYear - offset;
const current = offset === 0;
return {
value: `cy:${calendarYear}`,
label: current ? `Calendar Year ${calendarYear} to date` : `Calendar Year ${calendarYear}`,
calendar_year: calendarYear,
start: compact(calendarYear, 1, 1),
end: current ? compact(currentYear, now.getUTCMonth() + 1, now.getUTCDate()) : compact(calendarYear, 12, 31),
previous_start: compact(calendarYear - 1, 1, 1),
previous_end: current ? compact(calendarYear - 1, now.getUTCMonth() + 1, now.getUTCDate()) : compact(calendarYear - 1, 12, 31),
current_to_date: current
};
});
}
Expand Down Expand Up @@ -107,7 +132,10 @@ function buildDemoLibraryDashboard(payload = {}, data = {}) {
FRL: demoFiscalPeriods('FRL', 10, 'https://www.imls.gov/research-evaluation/surveys/public-libraries-survey-pls'),
LILS: demoFiscalPeriods('LILS', 10, 'https://www.imls.gov/research-evaluation/surveys/public-libraries-survey-pls')
};
const calendarPeriods = demoCalendarPeriods();
const fiscalPeriod = Object.values(fiscalPeriodsBySystem).flat().find(period => period.value === reportingPeriod);
const calendarPeriod = calendarPeriods.find(period => period.value === reportingPeriod);
const namedPeriod = fiscalPeriod || calendarPeriod;
const currentCheckouts = scaled(880229);
const currentRenewals = scaled(487605);
const previousCheckouts = scaled(842110);
Expand All @@ -120,6 +148,7 @@ function buildDemoLibraryDashboard(payload = {}, data = {}) {
{ value: 'LILS', label: 'Lee-Itawamba Library System' }
],
item_types: ['BOOK', 'EBOOK', 'DVD', 'AUDIOBOOK', 'KIT'],
calendar_periods: calendarPeriods,
fiscal_periods_by_system: fiscalPeriodsBySystem
};
return {
Expand All @@ -140,9 +169,11 @@ function buildDemoLibraryDashboard(payload = {}, data = {}) {
checkout_change_rate: previousCheckouts ? (currentCheckouts - previousCheckouts) / previousCheckouts : null,
renewal_change_rate: previousRenewals ? (currentRenewals - previousRenewals) / previousRenewals : null,
comparison_available: true, comparison_coverage_complete: true,
comparison_period_label: fiscalPeriod ? 'Previous fiscal year equivalent' : 'Previous equivalent period',
comparison_period_label: fiscalPeriod ? 'Previous fiscal year equivalent' : calendarPeriod ? 'Previous calendar year equivalent' : 'Previous equivalent period',
in_house_uses: scaled(61240), holds: scaled(121843), renewal_share: 0.356, holds_per_100_items: 4.3,
period_label: fiscalPeriod?.label || 'Illustrative 12-month reporting period',
period_label: namedPeriod?.label || 'Illustrative 12-month reporting period',
period_type: fiscalPeriod ? 'fiscal' : calendarPeriod ? 'calendar' : 'rolling',
calendar_year: calendarPeriod?.calendar_year,
fiscal_year: fiscalPeriod?.fiscal_year, fiscal_system: fiscalPeriod?.system
},
collection: {
Expand Down
15 changes: 13 additions & 2 deletions src/ui/dashboard/kpiDashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,19 @@ function syncPeriodOptions(elements) {
];
const fiscal = Array.isArray(libraryData.filters.fiscalPeriodsBySystem?.[system])
? libraryData.filters.fiscalPeriodsBySystem[system] : [];
const options = [...rolling, ...fiscal.map(period => ({ value: period.value, label: period.label }))];
elements.period.replaceChildren(...options.map(option => new Option(option.label, option.value)));
const calendar = Array.isArray(libraryData.filters.calendarPeriods) ? libraryData.filters.calendarPeriods : [];
const groups = [
{ label: 'Rolling periods', options: rolling },
{ label: 'Calendar years', options: calendar },
{ label: 'Fiscal years', options: fiscal }
].filter(group => group.options.length > 0);
const options = groups.flatMap(group => group.options);
elements.period.replaceChildren(...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;
}));
elements.period.value = options.some(option => option.value === selected) ? selected : '365';
}

Expand Down
1 change: 1 addition & 0 deletions src/ui/dashboard/libraryDashboardModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ function normalizeLibraryDashboard(payload = {}) {
filters: {
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 : [],
fiscalPeriodsBySystem: data.filters?.fiscal_periods_by_system && typeof data.filters.fiscal_periods_by_system === 'object'
? data.filters.fiscal_periods_by_system : {}
},
Expand Down
13 changes: 13 additions & 0 deletions tests/browser/browserSmoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -548,10 +548,23 @@ async function runSmokeTest() {
if (!exportText.startsWith('\uFEFFSection,Label,Metric,Value') || !exportText.includes('previous_checkouts')) {
throw new Error('Dashboard export should include the UTF-8 header and prior-period metrics.');
}
const reportingPeriodGroups = await page.locator('#kpi-dashboard-window optgroup').evaluateAll(groups => groups.map(group => group.label));
if (!reportingPeriodGroups.includes('Calendar years') || !reportingPeriodGroups.includes('Rolling periods')) {
throw new Error(`Dashboard should separate calendar-year and rolling choices: ${JSON.stringify(reportingPeriodGroups)}`);
}
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.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.');
}
await page.locator('#kpi-dashboard-window').selectOption('fy:MSU:2027');
await page.waitForFunction(() => document.querySelector('#kpi-dashboard-content .kpi-card')?.textContent?.includes('FY 2027 to date'));
const fiscalLabel = await page.locator('#kpi-dashboard-window option:checked').textContent();
if (!/FY 2027.*Jul 1, 2026.*Aug 21, 2026/u.test(fiscalLabel || '')) {
throw new Error(`Fiscal-year choice should display its actual calendar-date span: ${fiscalLabel}`);
}
if (process.env.QUERY_DASHBOARD_SCREENSHOT_PATH) {
await page.setViewportSize({ width: 1440, height: 1200 });
await page.screenshot({ path: process.env.QUERY_DASHBOARD_SCREENSHOT_PATH, fullPage: false });
Expand Down
4 changes: 2 additions & 2 deletions tests/browser/support/browserSmokeSupport.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ function buildDefaultQueryApiResponse(payload) {
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' },
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' : 'Recent 365 days', renewal_share: 0.356, holds: 121843, holds_per_100_items: 4.3 },
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 },
circulation_trend: [{ label: 'Jul', checkouts: 66892, renewals: 35911 }, { label: 'Aug', checkouts: 74822, renewals: 39844 }],
Expand All @@ -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'], fiscal_periods_by_system: { MSU: [{ value: 'fy:MSU:2027', label: 'FY 2027 to date' }, { value: 'fy:MSU:2026', label: 'FY 2026' }] } },
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' }] } },
privacy: { suppression_threshold: 10 },
sources: [{ label: 'Current item snapshot', detail: 'Aggregated test data.' }]
}),
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/core/mockQueryBackendLogic.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,24 @@ 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.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);
assert.ok(payload.patron_geo_breakdown.every(row => /xx$|unknown$/u.test(row.label)));
assert.ok(payload.patron_city_breakdown.length > 0);
assert.ok(payload.patron_state_breakdown.length > 0);
assert.equal(JSON.stringify(payload).includes('street_address'), false);

const calendarPeriod = initial.filters.calendar_periods[0].value;
const calendarResponse = await handleDemoQueryRequest({
body: JSON.stringify({ action: 'library_dashboard', library: 'all', reporting_period: calendarPeriod }),
headers: authHeaders
});
const calendarPayload = await calendarResponse.json();
assert.equal(calendarPayload.scope.reporting_period, calendarPeriod);
assert.equal(calendarPayload.circulation.period_type, 'calendar');
assert.equal(calendarPayload.circulation.calendar_year, Number(calendarPeriod.split(':')[1]));
assert.match(calendarPayload.circulation.period_label, /^Calendar Year \d{4}/u);
});

test('demo backend supports authenticated local bib lookup and WorldCat comparison', async () => {
Expand Down
7 changes: 6 additions & 1 deletion tests/unit/ui/libraryDashboardModelLogic.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ test('library dashboard normalizes aggregate groups and filter metadata', () =>
circulation: { checkouts: '45', period_label: 'Recent 90 days', coverage_complete: true },
patrons: { total: 32 },
library_breakdown: [{ label: 'Main', checkouts: '30' }],
filters: { libraries: [{ value: 'MAIN', label: 'Main' }], item_types: ['BOOK'] }
filters: {
libraries: [{ value: 'MAIN', label: 'Main' }],
item_types: ['BOOK'],
calendar_periods: [{ value: 'cy:2026', label: 'Calendar Year 2026 to date' }]
}
});
assert.equal(dashboard.collection.items, 120);
assert.equal(dashboard.circulation.checkouts, 45);
assert.equal(dashboard.circulation.period_label, 'Recent 90 days');
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.calendarPeriods[0].value, 'cy:2026');
assert.deepEqual(dashboard.availability, { circulation: true, collection: true, patrons: true });
assert.equal(libraryDashboardHasData(dashboard), true);
});
Expand Down