Skip to content

Commit 1f188e0

Browse files
feat: csv download for kpis, measures and impact analysis (#11)
* initial spec plan * feat: implement new service and button to download button * feat: impact analysis setup and download buttons on all screens
1 parent f644dd7 commit 1f188e0

49 files changed

Lines changed: 3208 additions & 181 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/agents/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ tests/
6262
- Tailwind + Catalyst UI Kit only for styling
6363

6464
## Recent Changes
65+
- 001-csv-download: Added [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
6566

6667
- 001-kpi-multi-value-input: Replace fixed before/after KPI input with dynamic multi-value list; add DefaultCollectionDate; expose `results: IKpiResult[]` from BFF
6768
- 001-multi-entry-kpi-charts: Added TypeScript (Astro strict preset) + React 19 islands + Astro 5, React 19, D3 7, Chart.js 4, react-chartjs-2, Tailwind 4
6869
- 001-mcda-custom-analysis: Added TypeScript 5 (strict mode via `astro/tsconfigs/strict`) + Astro 5 (`@astrojs/node` SSR adapter), React 19, Prisma 6, Tailwind CSS 4, Vitest 4
6970

70-
- 001-mcda-custom-analysis: Added
7171
<!-- MANUAL ADDITIONS START -->
7272
<!-- MANUAL ADDITIONS END -->
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Specification Quality Checklist: CSV Dataset Download
2+
3+
**Purpose**: Validate specification completeness and quality before proceeding to planning
4+
**Created**: 2026-02-27
5+
**Feature**: [spec.md](../spec.md)
6+
7+
## Content Quality
8+
9+
- [x] No implementation details (languages, frameworks, APIs)
10+
- [x] Focused on user value and business needs
11+
- [x] Written for non-technical stakeholders
12+
- [x] All mandatory sections completed
13+
14+
## Requirement Completeness
15+
16+
- [x] No [NEEDS CLARIFICATION] markers remain
17+
- [x] Requirements are testable and unambiguous
18+
- [x] Success criteria are measurable
19+
- [x] Success criteria are technology-agnostic (no implementation details)
20+
- [x] All acceptance scenarios are defined
21+
- [x] Edge cases are identified
22+
- [x] Scope is clearly bounded
23+
- [x] Dependencies and assumptions identified
24+
25+
## Feature Readiness
26+
27+
- [x] All functional requirements have clear acceptance criteria
28+
- [x] User scenarios cover primary flows
29+
- [x] Feature meets measurable outcomes defined in Success Criteria
30+
- [x] No implementation details leak into specification
31+
32+
## Notes
33+
34+
- All checklist items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
35+
- Two dataset types are scoped: KPI results (with multi-dimensional filtering) and measures implementation (lab-scoped).
36+
- The reusable component requirement (FR-009, FR-010) ensures the feature is not page-specific.
37+
- SC-005 (3 second response for 10,000 rows) is an informed default based on standard web application expectations; can be revised during planning if benchmarks suggest otherwise.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Contract: GET /api/v1/csv/kpiresults
2+
3+
**Type**: HTTP Endpoint
4+
**Method**: GET
5+
**Path**: `/api/v1/csv/kpiresults`
6+
7+
---
8+
9+
## Request
10+
11+
### Query Parameters
12+
13+
| Parameter | Type | Required | Description |
14+
|---|---|---|---|
15+
| `living_lab_id` | positive integer | No | Filter results to a single living lab |
16+
| `category_id` | positive integer | No | Filter results to a single KPI group (category) |
17+
| `kpidefinition_id` | positive integer | No | Filter results to a single KPI definition |
18+
19+
All parameters are optional and can be combined. Non-integer or negative values are rejected.
20+
21+
### Examples
22+
23+
```
24+
GET /api/v1/csv/kpiresults
25+
GET /api/v1/csv/kpiresults?living_lab_id=3
26+
GET /api/v1/csv/kpiresults?category_id=7
27+
GET /api/v1/csv/kpiresults?living_lab_id=3&kpidefinition_id=12
28+
GET /api/v1/csv/kpiresults?kpidefinition_id=12
29+
```
30+
31+
---
32+
33+
## Response
34+
35+
### 200 OK — CSV file
36+
37+
**Headers**:
38+
```
39+
Content-Type: text/csv; charset=utf-8
40+
Content-Disposition: attachment; filename="kpi-results.csv"
41+
```
42+
43+
**Body** (CSV — first line is header):
44+
```csv
45+
"Lab","KPI Number","KPI Name","KPI Group","Metric","Value","Date","Transport Mode"
46+
"Geneva Lab","1.1","Air Quality","Environment","µg/m3","45.2","2023-01-01","Car"
47+
"Lyon Lab","1.1","Air Quality","Environment","µg/m3","38.0","2023-01-01",""
48+
```
49+
50+
### 400 Bad Request — Invalid parameter
51+
52+
```json
53+
{ "error": "Invalid parameter: living_lab_id must be a positive integer" }
54+
```
55+
56+
### 404 Not Found — No rows matched the filters
57+
58+
```json
59+
{ "error": "No data found for the requested filters" }
60+
```
61+
62+
### 500 Internal Server Error
63+
64+
```json
65+
{ "error": "Internal Server Error" }
66+
```
67+
68+
---
69+
70+
## Behavior Notes
71+
72+
- When no filters are provided, all KPI results across all labs are included.
73+
- For KPI definitions linked to multiple categories, the first category name (by insertion order) is used.
74+
- `Transport Mode` column is empty string `""` when the KPI result has no transport mode.
75+
- Response body never contains an empty CSV (header-row only) — a 404 is returned instead.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Contract: GET /api/v1/csv/projects
2+
3+
**Type**: HTTP Endpoint
4+
**Method**: GET
5+
**Path**: `/api/v1/csv/projects`
6+
7+
---
8+
9+
## Request
10+
11+
### Query Parameters
12+
13+
| Parameter | Type | Required | Description |
14+
|---|---|---|---|
15+
| `living_lab_id` | positive integer | No | Filter results to a single living lab |
16+
17+
### Examples
18+
19+
```
20+
GET /api/v1/csv/projects
21+
GET /api/v1/csv/projects?living_lab_id=3
22+
```
23+
24+
---
25+
26+
## Response
27+
28+
### 200 OK — CSV file
29+
30+
**Headers**:
31+
```
32+
Content-Type: text/csv; charset=utf-8
33+
Content-Disposition: attachment; filename="projects.csv"
34+
```
35+
36+
**Body** (CSV — first line is header):
37+
```csv
38+
"Lab","Project Name","Project Type","Start Date","Description"
39+
"Geneva Lab","Micro-mobility Share","mobility","2022-03-15","City-wide scooter sharing program"
40+
"Lyon Lab","Public Transport Upgrade","public_transport","","Extended metro line 3"
41+
```
42+
43+
### 400 Bad Request — Invalid parameter
44+
45+
```json
46+
{ "error": "Invalid parameter: living_lab_id must be a positive integer" }
47+
```
48+
49+
### 404 Not Found — No rows matched the filters
50+
51+
```json
52+
{ "error": "No data found for the requested filters" }
53+
```
54+
55+
### 500 Internal Server Error
56+
57+
```json
58+
{ "error": "Internal Server Error" }
59+
```
60+
61+
---
62+
63+
## Behavior Notes
64+
65+
- When no filter is provided, all measure implementations for all labs are included.
66+
- `Start Date` column is empty string `""` when `start_at` is null.
67+
- `Description` column is empty string `""` when null.
68+
- Response body never contains an empty CSV (header-row only) — a 404 is returned instead.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Data Model: CSV Dataset Download
2+
3+
**Feature**: 001-csv-download | **Date**: 2026-02-27
4+
5+
---
6+
7+
## Export Types
8+
9+
### 1. KPI Results Export
10+
11+
Produces rows from a join of five tables. Internal technical columns (`*_id`, `created_at`, `updated_at`, `user_id`) are excluded from output.
12+
13+
#### Source Tables
14+
15+
| Table | Role in Join |
16+
|---|---|
17+
| `kpiresults` | Primary table — one row per export row |
18+
| `labs` | `kpiresults.living_lab_id → labs.id` |
19+
| `kpidefinitions` | `kpiresults.kpidefinition_id → kpidefinitions.id` |
20+
| `kpidefinitions_category` | Bridge: `kpidefinitions.id → kpidefinitions_category.kpidefinition_id` |
21+
| `categories` | `kpidefinitions_category.category_id → categories.id` |
22+
| `transport_mode` | Optional: `kpiresults.transport_mode_id → transport_mode.id` (nullable) |
23+
24+
#### Output CSV Columns
25+
26+
| CSV header | Source field | Type | Notes |
27+
|---|---|---|---|
28+
| `Lab` | `labs.name` | string | |
29+
| `KPI Number` | `kpidefinitions.kpi_number` | string | |
30+
| `KPI Name` | `kpidefinitions.name` | string | |
31+
| `KPI Group` | `categories.name` | string | First category if multiple exist |
32+
| `Metric` | `kpidefinitions.metric` | string | |
33+
| `Value` | `kpiresults.value` | number | Float; serialized as decimal |
34+
| `Date` | `kpiresults.date` | string | ISO date `YYYY-MM-DD` |
35+
| `Transport Mode` | `transport_mode.name` | string or empty | Empty string when null |
36+
37+
#### Filters
38+
39+
| Param | Type | Effect |
40+
|---|---|---|
41+
| *(none)* || All KPI results for all labs |
42+
| `living_lab_id` | positive integer | WHERE `kpiresults.living_lab_id = ?` |
43+
| `category_id` | positive integer | WHERE `categories.id = ?` |
44+
| `kpidefinition_id` | positive integer | WHERE `kpiresults.kpidefinition_id = ?` |
45+
| `living_lab_id` + `kpidefinition_id` | both | Combined AND filter |
46+
47+
Filters are applied additively (AND). No OR multi-filter logic is required.
48+
49+
---
50+
51+
### 2. Projects (Measures Implementation) Export
52+
53+
Produces rows from a join of three tables. Internal columns excluded.
54+
55+
#### Source Tables
56+
57+
| Table | Role in Join |
58+
|---|---|
59+
| `living_lab_projects_implementation` | Primary table |
60+
| `labs` | `living_lab_projects_implementation.living_lab_id → labs.id` |
61+
| `projects` | `living_lab_projects_implementation.project_id → projects.id` |
62+
63+
#### Output CSV Columns
64+
65+
| CSV header | Source field | Type | Notes |
66+
|---|---|---|---|
67+
| `Lab` | `labs.name` | string | |
68+
| `Project Name` | `projects.name` | string | |
69+
| `Project Type` | `projects.type` | string | |
70+
| `Start Date` | `living_lab_projects_implementation.start_at` | string or empty | ISO date-time, formatted as `YYYY-MM-DD`; empty string when null |
71+
| `Description` | `living_lab_projects_implementation.description` | string or empty | Empty string when null |
72+
73+
#### Filters
74+
75+
| Param | Type | Effect |
76+
|---|---|---|
77+
| *(none)* || All measure implementations for all labs |
78+
| `living_lab_id` | positive integer | WHERE `living_lab_projects_implementation.living_lab_id = ?` |
79+
80+
---
81+
82+
## Repository Output Types (typed rows returned by repository before serialization)
83+
84+
```typescript
85+
// Returned by CsvExportRepository.findKpiResultsForCsv()
86+
interface KpiResultCsvRow {
87+
lab: string;
88+
kpi_number: string;
89+
kpi_name: string;
90+
kpi_group: string;
91+
metric: string;
92+
value: number;
93+
date: string; // YYYY-MM-DD
94+
transport_mode: string; // empty string when none
95+
}
96+
97+
// Returned by CsvExportRepository.findProjectsForCsv()
98+
interface ProjectCsvRow {
99+
lab: string;
100+
project_name: string;
101+
project_type: string;
102+
start_date: string; // YYYY-MM-DD or empty string
103+
description: string; // empty string when null
104+
}
105+
```
106+
107+
---
108+
109+
## CSV Row Validation Rules
110+
111+
- `value` must be a finite number (Prisma guarantees this via Float column).
112+
- `date` is never null (Prisma `@db.Date` not optional).
113+
- `transport_mode` defaults to `""` when `transport_mode_id` is null.
114+
- `start_date` defaults to `""` when `start_at` is null.
115+
- `description` defaults to `""` when null.
116+
- All string values are passed through the `CsvSerializer` which double-quotes and escapes `"` characters.
117+
118+
---
119+
120+
## State Transitions (component)
121+
122+
```
123+
idle ──[click]──▶ loading ──[success]──▶ idle
124+
└──[error]────▶ error_shown ──[next render]──▶ idle
125+
```
126+
127+
- `idle`: button enabled (or disabled if required filter props are absent)
128+
- `loading`: button disabled + spinner SVG + "Downloading…" text
129+
- `error_shown`: inline error message visible below/near button; button re-enabled after brief display timeout or on next user interaction

0 commit comments

Comments
 (0)