Skip to content
Draft
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
12 changes: 6 additions & 6 deletions cache-bust.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "7a4f5965baa713ed",
"version": "620804a06d67d4c1",
"generatedBy": "scripts/updateCacheBusting.mjs",
"assets": {
"assets/app-icon-16.png": "6645731d86da1071",
Expand Down Expand Up @@ -190,11 +190,11 @@
"src/lib/workbook-export/workbookXmlUtils.js": "2cc0fcdda2a1d411",
"src/lib/workbook-export/xlsxZipWriter.js": "5f4c1f6e98fbcb06",
"src/styles/api-settings.css": "c5f76b60fb8684bb",
"src/styles/app.css": "0b5978c276c2585f",
"src/styles/app.css": "5c1c02d1bf7eaf03",
"src/styles/base.css": "8a925806001990b6",
"src/styles/bib-compare.css": "144f2a1093e00dcf",
"src/styles/controls.css": "30d36ccc65181c21",
"src/styles/dashboard.css": "d79fa67fe87bb96e",
"src/styles/dashboard.css": "7aed1832e6cabd24",
"src/styles/filters.css": "9e7297e65dd68a3c",
"src/styles/form-mode.css": "7de8927db30d30f6",
"src/styles/help.css": "1e75cc7af857e66a",
Expand Down Expand Up @@ -240,10 +240,10 @@
"src/ui/dashboard/kpiDashboard.js": "55a7adaaf847950b",
"src/ui/dashboard/kpiDashboardModel.js": "fa4c42b18c1245e5",
"src/ui/dashboard/kpiDashboardView.js": "82508c550975ada6",
"src/ui/dashboard/libraryDashboardCoverage.js": "d2da9d84bf50899f",
"src/ui/dashboard/libraryDashboardCoverage.js": "ba9e61d94c9af271",
"src/ui/dashboard/libraryDashboardExport.js": "ae9057c966126624",
"src/ui/dashboard/libraryDashboardModel.js": "34c866b3a028dbe2",
"src/ui/dashboard/libraryDashboardView.js": "c6debdb8b5e1ae1f",
"src/ui/dashboard/libraryDashboardModel.js": "237c98c1c1b5d332",
"src/ui/dashboard/libraryDashboardView.js": "6f86add644395314",
"src/ui/dashboard/reportingPeriodPicker.js": "334aea95b73dee72",
"src/ui/field-picker/buildableFieldPreview.js": "8abd486bb0a87fe2",
"src/ui/field-picker/fieldPicker.js": "526a1bf934f5dd74",
Expand Down
9 changes: 6 additions & 3 deletions docs/LIBRARY_DASHBOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ The private MLP environment owns:
- `collection`: current items and titles, lifetime checkout and renewal counters, recent-use rate, never-used items, item age, and price coverage;
- `patrons`: current, active, newly registered, currently borrowing, currently holding, and soon-expiring patron counts;
- `circulation_trend`: reporting-period transaction points;
- `library_breakdown` and `item_type_breakdown`: comparable scoped aggregates;
- `system_breakdown`, `library_breakdown`, and `item_type_breakdown`: complete comparable totals at consortium, system, branch, and item-type grain;
- `use_bands` and `age_bands`: current-collection distributions;
- `patron_*_breakdown`: privacy-suppressed home-library, profile, age-band, ZIP3, city/state, and state aggregates; exact addresses and full ZIP codes are never returned;
- `previous_*`, `*_change`, and `*_change_rate`: the immediately preceding equivalent period when retained-log coverage is complete;
- `filters.fiscal_periods_by_system`: current fiscal year-to-date and completed fiscal years using each MLP system's documented reporting calendar;
- `opportunities`: aggregate action groups with optional backend-generated Query configurations;
- `service_coverage`: optional source-connection metadata for visits, programs, technology, electronic resources, ILL, acquisitions/budget, and hold fulfillment; absent metrics remain visibly unavailable rather than being estimated;
- `metric_definitions`: calculation, public-safe source fields, source, grain, and time basis for each KPI;
- `freshness`, `sources`, and `notes`: exact lineage, update times, and limitations.

The UI must treat absent groups as unavailable, not as zero. Every production response should describe the time basis of each source independently.
Expand All @@ -61,11 +62,13 @@ The circulation transaction baseline follows the established BLUEcloud contract:

Current holdings come from current item records. Item creation transactions must not be labeled as holdings. Lifetime item checkout/renewal counters must not be plotted as historical monthly transactions.

An active patron is a current user whose last-activity date falls inside the selected activity window. The response must state the window. Age groups are derived only from usable birth dates, and unknown dates remain a visible category.
`patrons.total` means **current patrons**, not every historical user row. A current patron has a privilege-expiration date on or after the snapshot date or an explicit `NEVER` expiration. Expired accounts and blank/unreadable expiration values are excluded. `patrons.records_total`, `patrons.expired`, and `patrons.expiration_unknown` reconcile the source population so exclusions remain visible and auditable. `patrons.never_expires` is a subset of current patrons. An active patron is a current patron whose last-activity date falls inside the selected activity window. New registrations, current loans, current holds, profile, and age measures also use current patrons. The response must state the window. Age groups are derived only from usable birth dates, and unknown dates remain a visible category.

Patron geography is built from a separate privacy-safe address aggregate. Until that source carries privilege expiration, geography is explicitly labeled as an all-record source and must not be described as current-patron geography.

The circulation period and the collection/patron activity window are separate dimensions. Calendar-year and fiscal-year choices control checkouts, renewals, transaction trends, and demand rankings. Current collection measures remain point-in-time or lifetime measures, while recent collection use and patron activity use the explicit `scope.active_window_days` rolling window. The interface must display these time bases independently and must not claim that a named circulation period changes aggregates that the source snapshot cannot calculate for that exact period.

Patron breakdowns must display their coverage against the scoped patron total. Returned groups can exclude missing, invalid, or privacy-suppressed values and the visual ranking can show only its highest-volume groups, so a chart must not imply that its visible bars represent every patron.
Current-patron breakdowns must display their coverage against the scoped current-patron total. Geography coverage uses the all-record audit denominator. Returned groups can exclude missing, invalid, or privacy-suppressed values and the visual ranking can show only its highest-volume groups, so a chart must not imply that its visible bars represent every patron. Complete branch/system tables remain available below summary charts; patron values below the privacy threshold are labeled `Suppressed`, never rendered as zero.

## Live-data behavior

Expand Down
2 changes: 1 addition & 1 deletion src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@import url("./table.css?v=8bc187669db70079");
@import url("./controls.css?v=30d36ccc65181c21");
@import url("./history.css?v=64598bfd565a29af");
@import url("./dashboard.css?v=d79fa67fe87bb96e");
@import url("./dashboard.css?v=7aed1832e6cabd24");
@import url("./templates.css?v=1c0a1594bedc65b9");
@import url("./api-settings.css?v=c5f76b60fb8684bb");
@import url("./site-update.css?v=b16b51ef4c7bd942");
Expand Down
58 changes: 58 additions & 0 deletions src/styles/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,61 @@
line-height: 1.35;
}

.kpi-card__method {
margin-top: 0.15rem;
color: var(--theme-text-muted);
font-size: 0.7rem;
}

.kpi-card__method summary,
.kpi-breakdown-details summary {
width: fit-content;
color: var(--theme-accent);
cursor: pointer;
font-weight: 800;
}

.kpi-card__method div {
display: grid;
gap: 0.32rem;
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--theme-border);
}

.kpi-card__method p {
margin: 0;
font-size: 0.69rem;
line-height: 1.4;
}

.kpi-patron-reconciliation {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.75rem;
}

.kpi-breakdown-details {
margin-top: 1rem;
}

.kpi-breakdown-details summary {
margin-bottom: 0.75rem;
}

.kpi-breakdown-table th,
.kpi-breakdown-table td {
white-space: nowrap;
text-align: right;
}

.kpi-breakdown-table th:first-child,
.kpi-breakdown-table th:nth-child(2),
.kpi-breakdown-table td:first-child,
.kpi-breakdown-table td:nth-child(2) {
text-align: left;
}

.kpi-dashboard__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
Expand Down Expand Up @@ -900,6 +955,9 @@
}

@media (max-width: 760px) {
.kpi-patron-reconciliation {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.kpi-dashboard-toolbar {
position: static;
grid-template-columns: 1fr 1fr;
Expand Down
8 changes: 5 additions & 3 deletions src/ui/dashboard/libraryDashboardCoverage.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
const SERVICE_COVERAGE = [
{ id: 'circulation', label: 'Circulation', detail: 'Checkouts, renewals, trends, and demand rankings.' },
{ id: 'collection', label: 'Collection', detail: 'Current items, recorded use, age, holds, and price coverage.' },
{ id: 'collection', label: 'Collection', detail: 'Distinct titles, current items, turnover, inventory, availability, location, use, age, holds, and price coverage.' },
{ id: 'patrons', label: 'Patrons', detail: 'Current, active, and newly registered patrons with privacy-safe demographics.' },
{ id: 'operations', label: 'Query operations', detail: 'Report reliability, turnaround, staff activity, and Hydration outcomes.' },
{ id: 'visits', label: 'Visits and door counts', detail: 'Requires a gate-counter or reporting-system source.' },
{ id: 'programs', label: 'Programs and attendance', detail: 'Requires a program calendar or attendance source.' },
{ id: 'technology', label: 'Computers and Wi-Fi', detail: 'Requires session or network aggregates.' },
{ id: 'eresources', label: 'Electronic resources', detail: 'Requires COUNTER or vendor usage aggregates.' },
{ id: 'ill', label: 'Interlibrary loan', detail: 'Requires request, filled, and turnaround aggregates.' },
{ id: 'budget', label: 'Acquisitions and budget', detail: 'Requires approved financial or acquisitions aggregates.' },
{ id: 'fulfillment', label: 'Hold fulfillment', detail: 'Requires placed, filled, expired, and elapsed-time aggregates.' }
{ id: 'budget', label: 'Acquisitions and budget', detail: 'Sirsi acquisitions commands are installed; a dedicated privacy-safe aggregate is not connected yet.' },
{ id: 'fulfillment', label: 'Hold fulfillment', detail: 'Sirsi hold records are available; a dedicated lifecycle aggregate is not connected yet.' },
{ id: 'transit', label: 'Transit workflow', detail: 'Sirsi current-transit records and transaction logs are available; historical throughput is not connected yet.' },
{ id: 'billing', label: 'Billing', detail: 'Sirsi bill and payment selectors are installed; leadership-safe aggregates are not connected yet.' }
];

function activityWindowLabel(days = 365) {
Expand Down
4 changes: 4 additions & 0 deletions src/ui/dashboard/libraryDashboardModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ function normalizeLibraryDashboard(payload = {}) {
collection: normalizeMetricGroup(data.collection),
patrons: normalizeMetricGroup(data.patrons),
circulationTrend: normalizeSeries(data.circulation_trend),
systemBreakdown: normalizeSeries(data.system_breakdown),
libraryBreakdown: normalizeSeries(data.library_breakdown),
itemTypeBreakdown: normalizeSeries(data.item_type_breakdown),
useBands: normalizeSeries(data.use_bands),
ageBands: normalizeSeries(data.age_bands),
homeLocationBreakdown: normalizeSeries(data.home_location_breakdown),
currentLocationBreakdown: normalizeSeries(data.current_location_breakdown),
patronLibraryBreakdown: normalizeSeries(data.patron_library_breakdown),
patronProfileBreakdown: normalizeSeries(data.patron_profile_breakdown),
patronAgeBands: normalizeSeries(data.patron_age_bands),
Expand All @@ -58,6 +61,7 @@ function normalizeLibraryDashboard(payload = {}) {
? data.filters.fiscal_periods_by_system : {}
},
sources: Array.isArray(data.sources) ? data.sources : [],
metricDefinitions: data.metric_definitions && typeof data.metric_definitions === 'object' ? data.metric_definitions : {},
notes: Array.isArray(data.notes) ? data.notes : [],
privacy: data.privacy || {},
serviceCoverage: Array.isArray(data.service_coverage) ? data.service_coverage : [],
Expand Down
Loading