|
| 1 | +# Data Model: Platform Analytics Dashboard |
| 2 | + |
| 3 | +**Phase**: 1 | **Feature**: 002-admin-analytics-dashboard | **Date**: 2026-03-17 |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Source Entities (read-only — no schema changes) |
| 8 | + |
| 9 | +This feature reads existing data only. No new database tables or Prisma schema changes required. |
| 10 | + |
| 11 | +### Living Labs (`labs`) |
| 12 | + |
| 13 | +| Field | Type | Notes | |
| 14 | +|-------|------|-------| |
| 15 | +| id | BigInt (PK) | Used as grouping key | |
| 16 | +| name | String | Display label in tables/charts | |
| 17 | +| country | String? | Display in table | |
| 18 | +| created_at | DateTime? | — | |
| 19 | +| updated_at | DateTime? | — | |
| 20 | + |
| 21 | +**Relationships used**: |
| 22 | +- `kpiresults[]` → via `getLivingLabs()` populated include |
| 23 | +- `living_lab_projects_implementation[]` → via populated include (projects) |
| 24 | + |
| 25 | +### KPI Definitions (`kpidefinitions`) |
| 26 | + |
| 27 | +| Field | Type | Notes | |
| 28 | +|-------|------|-------| |
| 29 | +| id | BigInt (PK) | Used for mapping results → definitions | |
| 30 | +| kpi_number | String | Display label (e.g. "1", "2.1") | |
| 31 | +| name | String | Display label | |
| 32 | +| parent_kpi_id | BigInt? (FK → self) | `null` = main/parent KPI | |
| 33 | +| type | Enum (GLOBAL, LOCAL) | Used for type breakdown | |
| 34 | + |
| 35 | +**Main/parent KPI identification**: `parent_kpi_id IS NULL` |
| 36 | + |
| 37 | +### KPI Results (`kpiresults`) |
| 38 | + |
| 39 | +| Field | Type | Notes | |
| 40 | +|-------|------|-------| |
| 41 | +| id | BigInt (PK) | — | |
| 42 | +| kpidefinition_id | BigInt (FK → kpidefinitions) | Links result to definition | |
| 43 | +| living_lab_id | BigInt (FK → labs) | Links result to lab | |
| 44 | +| value | Float | The submitted measurement | |
| 45 | +| date | DateTime | Used for timeline aggregation (year extraction) | |
| 46 | + |
| 47 | +**Accessed via**: `ILivingLabPopulated.kpi_results: IKpiResultGroup[]`, where each group has `results: IKpiResult[]` |
| 48 | + |
| 49 | +### Projects/Measures (`projects` + `living_lab_projects_implementation`) |
| 50 | + |
| 51 | +| Field | Type | Notes | |
| 52 | +|-------|------|-------| |
| 53 | +| projects.id | BigInt (PK) | — | |
| 54 | +| projects.name | String | Display label | |
| 55 | +| projects.type | String (PUSH, PULL, OTHER) | Used for type breakdown | |
| 56 | +| impl.project_id | BigInt (FK) | Links implementation to project | |
| 57 | +| impl.living_lab_id | BigInt (FK) | Links implementation to lab | |
| 58 | +| impl.start_at | DateTime? | Used for "last updated" computation | |
| 59 | +| impl.updated_at | DateTime? | Used for "last updated" computation | |
| 60 | + |
| 61 | +### Users (`users`) |
| 62 | + |
| 63 | +| Field | Type | Notes | |
| 64 | +|-------|------|-------| |
| 65 | +| id | BigInt (PK) | — | |
| 66 | +| name | String | — | |
| 67 | +| email | String | — | |
| 68 | +| status | String (signup, active, disabled) | Used for active vs pending breakdown | |
| 69 | + |
| 70 | +--- |
| 71 | + |
| 72 | +## Computed Analytics Data Structures |
| 73 | + |
| 74 | +These are TypeScript interfaces for data computed in `src/lib/helpers/analytics.ts` and passed as props from the Astro page to components. |
| 75 | + |
| 76 | +### MetricCardData |
| 77 | + |
| 78 | +```typescript |
| 79 | +/** Data for a single summary metric card */ |
| 80 | +interface MetricCardData { |
| 81 | + label: string; // e.g. "Living Labs", "Users (active / pending)" |
| 82 | + value: string; // Primary display value, e.g. "12", "45 / 3" |
| 83 | + icon?: string; // Optional icon identifier |
| 84 | + color?: string; // Tailwind color class, e.g. "text-primary" |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +### LabKpiTimelineData |
| 89 | + |
| 90 | +```typescript |
| 91 | +/** Data for the D3 line chart — one series per living lab */ |
| 92 | +interface LabKpiTimelineSeries { |
| 93 | + labId: number; |
| 94 | + labName: string; |
| 95 | + color: string; |
| 96 | + dataPoints: { year: number; count: number }[]; |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +### AlertCardData |
| 101 | + |
| 102 | +```typescript |
| 103 | +/** Data for a single analytics alert card */ |
| 104 | +interface AlertCardData { |
| 105 | + label: string; // e.g. "Labs with no KPI results" |
| 106 | + value: number; // Count |
| 107 | + severity: "warning" | "danger" | "info"; |
| 108 | + items?: string[]; // Optional list of affected entity names |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +### LivingLabMetricsRow |
| 113 | + |
| 114 | +```typescript |
| 115 | +/** One row in the living lab metrics table */ |
| 116 | +interface LivingLabMetricsRow { |
| 117 | + labId: number; |
| 118 | + labName: string; |
| 119 | + totalResultEntries: number; // All KPI results (including child KPIs) |
| 120 | + kpisCoveredCount: number; // Distinct main/parent KPIs with results |
| 121 | + totalMainKpis: number; // Total main/parent KPI definitions |
| 122 | + pushMeasuresCount: number; |
| 123 | + pullMeasuresCount: number; |
| 124 | + lastUpdatedAt: string | null; // ISO date string or null |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +### KpiCoverageRow |
| 129 | + |
| 130 | +```typescript |
| 131 | +/** One row in the KPI coverage table */ |
| 132 | +interface KpiCoverageRow { |
| 133 | + kpiId: number; |
| 134 | + kpiNumber: string; // e.g. "1", "2", "3" |
| 135 | + kpiName: string; |
| 136 | + kpiType: "GLOBAL" | "LOCAL"; |
| 137 | + labsWithResultsCount: number; // How many labs have submitted results |
| 138 | + totalLabs: number; // Total number of labs as a reference |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +### LabMeasuresBarData |
| 143 | + |
| 144 | +```typescript |
| 145 | +/** Data for the D3 bar chart — one group per living lab */ |
| 146 | +interface LabMeasuresBarData { |
| 147 | + labName: string; |
| 148 | + pushCount: number; |
| 149 | + pullCount: number; |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +--- |
| 154 | + |
| 155 | +## Data Flow Diagram |
| 156 | + |
| 157 | +``` |
| 158 | +┌─────────────────────────────────────────────────────┐ |
| 159 | +│ analytics.astro (SSR frontmatter) │ |
| 160 | +│ │ |
| 161 | +│ 1. ApiClient.getLivingLabs() → ILivingLabPopulated[] │ |
| 162 | +│ 2. ApiClient.getKPIs({}) → IKpi[] │ |
| 163 | +│ 3. ApiClient.getMeasures() → IProject[] │ |
| 164 | +│ 4. ApiClient.getUsers() → User[] │ |
| 165 | +│ │ |
| 166 | +│ ┌───────────────────────────────┐ │ |
| 167 | +│ │ analytics.ts helpers │ │ |
| 168 | +│ │ - computeMetricCards() │ │ |
| 169 | +│ │ - computeLabKpiTimeline() │ │ |
| 170 | +│ │ - computeAlerts() │ │ |
| 171 | +│ │ - computeLabMetricsTable() │ │ |
| 172 | +│ │ - computeKpiCoverageTable() │ │ |
| 173 | +│ │ - computeLabMeasuresBar() │ │ |
| 174 | +│ └───────────────────────────────┘ │ |
| 175 | +│ │ |
| 176 | +│ Pass computed data as props ↓ │ |
| 177 | +├─────────────────────────────────────────────────────┤ |
| 178 | +│ REACT COMPONENTS (SSR-only, no client:* → zero JS) │ |
| 179 | +│ ┌──────────┐ ┌──────────────┐ ┌────────────────┐ │ |
| 180 | +│ │MetricCard│ │AnalyticsAlerts│ │LivingLabMetrics│ │ |
| 181 | +│ │ (×4) │ │ │ │ Table │ │ |
| 182 | +│ └──────────┘ └──────────────┘ └────────────────┘ │ |
| 183 | +│ ┌──────────────────┐ │ |
| 184 | +│ │KPICoverageTable │ │ |
| 185 | +│ └──────────────────┘ │ |
| 186 | +├─────────────────────────────────────────────────────┤ |
| 187 | +│ REACT ISLANDS (client:load — D3 requires DOM) │ |
| 188 | +│ ┌───────────────────────┐ ┌──────────────────────┐ │ |
| 189 | +│ │D3LineChartLabKPIs │ │D3BarChartLabMeasures │ │ |
| 190 | +│ │ Overtime │ │ │ │ |
| 191 | +│ └───────────────────────┘ └──────────────────────┘ │ |
| 192 | +└─────────────────────────────────────────────────────┘ |
| 193 | +``` |
| 194 | + |
| 195 | +--- |
| 196 | + |
| 197 | +## Mapping: KPI Result → Parent KPI |
| 198 | + |
| 199 | +To determine coverage of main/parent KPIs: |
| 200 | + |
| 201 | +``` |
| 202 | +For each IKpiResult in a lab's kpi_results: |
| 203 | + 1. Get kpidefinition_id from the result |
| 204 | + 2. Look up the IKpi definition |
| 205 | + 3. If definition.parent_kpi_id is not null → use parent_kpi_id as the "covered parent" |
| 206 | + 4. If definition.parent_kpi_id is null → this IS a parent KPI, use definition.id |
| 207 | + 5. Add resolved parent ID to the "covered" set for that lab |
| 208 | +``` |
| 209 | + |
| 210 | +This ensures a parent KPI is counted as "covered" whether the result was submitted against the parent directly or against any of its children. |
0 commit comments