docs(11-03): complete CPV-Divisions-Katalog + Wert-Filter-UI plan
This commit is contained in:
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
- [x] **FILTER-01**: Nutzer kann per Volltext-Stichwort (Titel/Beschreibung) suchen.
|
- [x] **FILTER-01**: Nutzer kann per Volltext-Stichwort (Titel/Beschreibung) suchen.
|
||||||
- [x] **FILTER-02**: Nutzer kann nach Region / PLZ / Bundesland filtern.
|
- [x] **FILTER-02**: Nutzer kann nach Region / PLZ / Bundesland filtern.
|
||||||
- [ ] **FILTER-03**: Nutzer kann nach CPV-Code / Branche filtern (hierarchische Auswahl mit Autocomplete).
|
- [x] **FILTER-03**: Nutzer kann nach CPV-Code / Branche filtern (hierarchische Auswahl mit Autocomplete).
|
||||||
- [x] **FILTER-04**: Nutzer kann nach Abgabefrist filtern, inkl. Option „nur noch offene".
|
- [x] **FILTER-04**: Nutzer kann nach Abgabefrist filtern, inkl. Option „nur noch offene".
|
||||||
- [x] **FILTER-05**: Nutzer kann nach geschätztem Auftragswert (min/max) filtern; Ausschreibungen ohne Wertangabe werden korrekt behandelt.
|
- [x] **FILTER-05**: Nutzer kann nach geschätztem Auftragswert (min/max) filtern; Ausschreibungen ohne Wertangabe werden korrekt behandelt.
|
||||||
- [ ] **FILTER-06**: Nutzer kann Suchprofile (Kombination aus Filterkriterien) speichern, bearbeiten und löschen — pro Nutzer, mandantenbewusst.
|
- [ ] **FILTER-06**: Nutzer kann Suchprofile (Kombination aus Filterkriterien) speichern, bearbeiten und löschen — pro Nutzer, mandantenbewusst.
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
| SCHEMA-02 | Phase 10 | Complete |
|
| SCHEMA-02 | Phase 10 | Complete |
|
||||||
| FILTER-01 | Phase 11 | Complete |
|
| FILTER-01 | Phase 11 | Complete |
|
||||||
| FILTER-02 | Phase 11 | Complete |
|
| FILTER-02 | Phase 11 | Complete |
|
||||||
| FILTER-03 | Phase 11 | Pending |
|
| FILTER-03 | Phase 11 | Complete |
|
||||||
| FILTER-04 | Phase 11 | Complete |
|
| FILTER-04 | Phase 11 | Complete |
|
||||||
| FILTER-05 | Phase 11 | Complete |
|
| FILTER-05 | Phase 11 | Complete |
|
||||||
| FILTER-06 | Phase 11 | Pending |
|
| FILTER-06 | Phase 11 | Pending |
|
||||||
|
|||||||
@@ -386,11 +386,11 @@ Plans:
|
|||||||
4. User can mark a tender as read/unread and as favourite, and filter the results list to only favourites -- both states are per-user, not shared across the tenant
|
4. User can mark a tender as read/unread and as favourite, and filter the results list to only favourites -- both states are per-user, not shared across the tenant
|
||||||
5. The UI clearly indicates data coverage (Oberschwelle vs. Unterschwelle) so an empty or thin result set isn't mistaken for a bug
|
5. The UI clearly indicates data coverage (Oberschwelle vs. Unterschwelle) so an empty or thin result set isn't mistaken for a bug
|
||||||
|
|
||||||
**Plans**: 2/6 plans executed
|
**Plans**: 3/6 plans executed
|
||||||
|
|
||||||
- [x] 11-01-PLAN.md — Trefferliste + Freitext + Sortierung + Coverage-Banner (FILTER-01/04/05, UI-01/05)
|
- [x] 11-01-PLAN.md — Trefferliste + Freitext + Sortierung + Coverage-Banner (FILTER-01/04/05, UI-01/05)
|
||||||
- [x] 11-02-PLAN.md — Bundesland-Ableitung (NUTS + Backfill) + Region/PLZ/Bundesland-Filter (FILTER-02)
|
- [x] 11-02-PLAN.md — Bundesland-Ableitung (NUTS + Backfill) + Region/PLZ/Bundesland-Filter (FILTER-02)
|
||||||
- [ ] 11-03-PLAN.md — CPV-Filter (Katalog + Divisionen) + Wert-Filter-UI (FILTER-03/05)
|
- [x] 11-03-PLAN.md — CPV-Filter (Katalog + Divisionen) + Wert-Filter-UI (FILTER-03/05)
|
||||||
- [ ] 11-04-PLAN.md — Detailansicht via ?tender=<id> + Quell-Link (UI-02)
|
- [ ] 11-04-PLAN.md — Detailansicht via ?tender=<id> + Quell-Link (UI-02)
|
||||||
- [ ] 11-05-PLAN.md — Triage: gelesen/ungelesen + Favorit + Merklisten-Filter (UI-03/04)
|
- [ ] 11-05-PLAN.md — Triage: gelesen/ungelesen + Favorit + Merklisten-Filter (UI-03/04)
|
||||||
- [ ] 11-06-PLAN.md — Suchprofile speichern/bearbeiten/löschen (FILTER-06)
|
- [ ] 11-06-PLAN.md — Suchprofile speichern/bearbeiten/löschen (FILTER-06)
|
||||||
@@ -462,7 +462,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10
|
|||||||
| 8. Dashboard Widgets Vollimplementierung | 4/4 | Complete | 2026-07-01 |
|
| 8. Dashboard Widgets Vollimplementierung | 4/4 | Complete | 2026-07-01 |
|
||||||
| 9. Cert Manager Module | 6/6 | Complete | 2026-07-02 |
|
| 9. Cert Manager Module | 6/6 | Complete | 2026-07-02 |
|
||||||
| 10. Ausschreibungs-Radar Foundation & DÖE Ingestion | 6/6 | Complete | 2026-07-21 |
|
| 10. Ausschreibungs-Radar Foundation & DÖE Ingestion | 6/6 | Complete | 2026-07-21 |
|
||||||
| 11. Filter Engine, Results UI & Saved Searches | 2/6 | In Progress| |
|
| 11. Filter Engine, Results UI & Saved Searches | 3/6 | In Progress| |
|
||||||
| 12. Tender Notifications | 0/TBD | Not started | - |
|
| 12. Tender Notifications | 0/TBD | Not started | - |
|
||||||
| 13. Scraping Adapters & Cross-Source Deduplication | 0/TBD | Not started | - |
|
| 13. Scraping Adapters & Cross-Source Deduplication | 0/TBD | Not started | - |
|
||||||
| 14. RSS, Email-Alert Ingestion & Module Rollout | 0/TBD | Not started | - |
|
| 14. RSS, Email-Alert Ingestion & Module Rollout | 0/TBD | Not started | - |
|
||||||
|
|||||||
+10
-7
@@ -5,15 +5,15 @@ milestone_name: Ausschreibungs-Radar
|
|||||||
current_phase: 11
|
current_phase: 11
|
||||||
current_phase_name: filter-engine-results-ui-saved-searches
|
current_phase_name: filter-engine-results-ui-saved-searches
|
||||||
status: executing
|
status: executing
|
||||||
stopped_at: Completed 11-02-PLAN.md
|
stopped_at: Completed 11-03-PLAN.md
|
||||||
last_updated: "2026-07-21T14:05:18.391Z"
|
last_updated: "2026-07-21T14:16:54.557Z"
|
||||||
last_activity: 2026-07-21
|
last_activity: 2026-07-21
|
||||||
last_activity_desc: Phase 11 execution started
|
last_activity_desc: Phase 11 execution started
|
||||||
progress:
|
progress:
|
||||||
total_phases: 11
|
total_phases: 11
|
||||||
completed_phases: 9
|
completed_phases: 9
|
||||||
total_plans: 52
|
total_plans: 52
|
||||||
completed_plans: 47
|
completed_plans: 48
|
||||||
---
|
---
|
||||||
|
|
||||||
# Project State
|
# Project State
|
||||||
@@ -28,11 +28,11 @@ See: .planning/PROJECT.md (updated 2026-07-17)
|
|||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 11 (filter-engine-results-ui-saved-searches) — EXECUTING
|
Phase: 11 (filter-engine-results-ui-saved-searches) — EXECUTING
|
||||||
Plan: 3 of 6
|
Plan: 4 of 6
|
||||||
Status: Ready to execute
|
Status: Ready to execute
|
||||||
Last activity: 2026-07-21 — Phase 11 execution started
|
Last activity: 2026-07-21 — Phase 11 execution started
|
||||||
|
|
||||||
Progress: [█████████░] 90%
|
Progress: [█████████░] 92%
|
||||||
|
|
||||||
## Performance Metrics
|
## Performance Metrics
|
||||||
|
|
||||||
@@ -82,6 +82,7 @@ Progress: [█████████░] 90%
|
|||||||
|------|----------|-------|-------|
|
|------|----------|-------|-------|
|
||||||
| Phase 11 P01 | 8min | 3 tasks | 11 files |
|
| Phase 11 P01 | 8min | 3 tasks | 11 files |
|
||||||
| Phase 11 P02 | 4min | 3 tasks | 12 files |
|
| Phase 11 P02 | 4min | 3 tasks | 12 files |
|
||||||
|
| Phase 11 P03 | 9min | 4 tasks | 12 files |
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
@@ -170,6 +171,8 @@ Recent decisions affecting current work:
|
|||||||
- [Phase ?]: deadlineFrom/deadlineTo URL-Param-Namen sind 1:1 identisch zu TenderQueryDto-Feldnamen fuer den Saved-Search-Vertrag aus Plan 11-06
|
- [Phase ?]: deadlineFrom/deadlineTo URL-Param-Namen sind 1:1 identisch zu TenderQueryDto-Feldnamen fuer den Saved-Search-Vertrag aus Plan 11-06
|
||||||
- [Phase ?]: bundesland-Filter matcht exakt gegen die befüllte, indexierte Spalte (statt region-startsWith); region bleibt als eigenständiger Präfixfilter erhalten.
|
- [Phase ?]: bundesland-Filter matcht exakt gegen die befüllte, indexierte Spalte (statt region-startsWith); region bleibt als eigenständiger Präfixfilter erhalten.
|
||||||
- [Phase ?]: BUNDESLAND_OPTIONS im Web als kleine Konstante gespiegelt (kein Shared-Package) — web nutzt @tessera/shared nicht, 16-Werte-Katalog rechtfertigt keine neue Cross-Package-Abhängigkeit.
|
- [Phase ?]: BUNDESLAND_OPTIONS im Web als kleine Konstante gespiegelt (kein Shared-Package) — web nutzt @tessera/shared nicht, 16-Werte-Katalog rechtfertigt keine neue Cross-Package-Abhängigkeit.
|
||||||
|
- [Phase ?]: CPV-Divisions-Kurzkatalog (2-stellig, ~45 Einträge) statt EU-Vollkatalog; hasSome-Match gegen precomputed cpvDivisions-Spalte statt Raw-SQL-Präfix-Match
|
||||||
|
- [Phase ?]: cpv-URL-Param als wiederholter Key (?cpv=45&cpv=71) statt Komma-Join; DTO normalisiert Einzelwert per @Transform zu Array
|
||||||
|
|
||||||
### Pending Todos
|
### Pending Todos
|
||||||
|
|
||||||
@@ -207,7 +210,7 @@ Items acknowledged and carried forward from previous milestone close:
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-07-21T14:05:18.378Z
|
Last session: 2026-07-21T14:16:54.544Z
|
||||||
Stopped at: Completed 11-02-PLAN.md
|
Stopped at: Completed 11-03-PLAN.md
|
||||||
Resume file: None
|
Resume file: None
|
||||||
Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created
|
Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
---
|
||||||
|
phase: 11-filter-engine-results-ui-saved-searches
|
||||||
|
plan: 03
|
||||||
|
subsystem: api, ui, database
|
||||||
|
tags: [nestjs, prisma, class-validator, cpv, nextjs-app-router, vitest]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 11-filter-engine-results-ui-saved-searches
|
||||||
|
provides: "tender-query.builder.ts (buildTenderWhere/buildOrderBy), erweiterter TenderQueryDto, FilterPanel.tsx (Plan 11-01/11-02), backend-only NULL-graceful value filter (Plan 11-01)"
|
||||||
|
provides:
|
||||||
|
- "cpv/cpv-catalog.ts: normalizeCpv()/divisionOf()/cpvMatchesDivisions() + CPV_DIVISIONS (45-entry statischer Divisions-Katalog, DE-Labels)"
|
||||||
|
- "Tender.cpvDivisions String[] (@@index Gin) — normalizer-derived für neue Ingests + Backfill-Migration für Bestand"
|
||||||
|
- "buildTenderWhere: cpvDivisions-hasSome-Branch (FILTER-03)"
|
||||||
|
- "TenderQueryDto.cpv[] (Transform normalisiert Einzelwert/Array)"
|
||||||
|
- "FilterPanel.tsx: CPV-Autocomplete (Label-Suche, Mehrfachauswahl, Chips) + Wert-min/max-Eingaben + 'ohne Wertangabe einschließen'-Toggle (Default an)"
|
||||||
|
affects: [11-04-detail-view, 11-05-triage, 11-06-saved-searches]
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Zweiseitige Normalisierung (normalizeCpv/divisionOf) statt exaktem Match — löst das Format-Inkonsistenz-Problem an der Wurzel, nicht durch Sonderfälle im Query-Builder"
|
||||||
|
- "Precomputed hasSome-Spalte (cpvDivisions) statt Raw-SQL-Präfix-Match — typsicher, GIN-indexierbar, konsistent mit dem bundesland-Backfill-Muster aus Plan 11-02"
|
||||||
|
- "CPV_DIVISIONS im Web als kleine Konstante gespiegelt (identisch zum BUNDESLAND_OPTIONS-Präzedenzfall aus Plan 11-02) statt Shared-Package"
|
||||||
|
- "Multi-Value-URL-Param (?cpv=45&cpv=71) statt Komma-Join — matcht Express' Default-Query-Parser 1:1, DTO @Transform normalisiert Einzelwert zu Array"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- apps/api/src/tenders/cpv/cpv-catalog.ts
|
||||||
|
- apps/api/src/tenders/cpv/cpv-catalog.spec.ts
|
||||||
|
- apps/api/prisma/migrations/20260721150000_tender_cpv_divisions_backfill/migration.sql
|
||||||
|
modified:
|
||||||
|
- apps/api/src/tenders/tender-normalizer.service.ts
|
||||||
|
- apps/api/src/tenders/tender-normalizer.service.spec.ts
|
||||||
|
- apps/api/src/tenders/tender-ingestion.service.ts
|
||||||
|
- apps/api/src/tenders/tender.types.ts
|
||||||
|
- apps/api/prisma/schema.prisma
|
||||||
|
- apps/api/src/tenders/dto/tender-query.dto.ts
|
||||||
|
- apps/api/src/tenders/tender-query.builder.ts
|
||||||
|
- apps/api/src/tenders/tender-query.builder.spec.ts
|
||||||
|
- apps/web/src/app/(portal)/modules/tender-radar/components/FilterPanel.tsx
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "cpvDivisions ist ein reiner Divisions-Kurzkatalog (2-stellig, ~45 Einträge) — KEIN EU-CPV-Vollkatalog (Research Open Question 2, D-03). Labels aus Trainingswissen gegen die Standard-CPV-2008-Divisionsliste erstellt (RESEARCH Assumption A2, tertiäre Quelle) — beim nächsten Kontakt mit der offiziellen EU-CPV-Publikation gegenzuprüfen, falls Label-Genauigkeit kritisch wird."
|
||||||
|
- "normalizeCpv() nutzt einen einfachen führende-Ziffern-Regex (^\\d+) statt eine CPV-Prüfziffer-Semantik zu implementieren — für das Divisions-Level (führende 2 Ziffern) genügt das Abschneiden bei Nicht-Ziffern-Zeichen vollständig; eine echte Prüfziffernvalidierung wäre Over-Engineering für einen reinen Präfix-Filter."
|
||||||
|
- "cpv-URL-Param wird als wiederholter Key (?cpv=45&cpv=71) statt Komma-Join geschrieben — matcht Express' Default-Query-Parser direkt (kein Split/Join-Code nötig), DTO normalisiert per @Transform einen Einzelwert (?cpv=45 als String statt Array) zu einem Ein-Element-Array, damit eine einzelne Auswahl nicht an @IsArray scheitert."
|
||||||
|
- "CPV_DIVISION_OPTIONS im Web ist eine 1:1-Kopie von CPV_DIVISIONS (api) statt aus @tessera/shared bezogen — identische Begründung wie BUNDESLAND_OPTIONS in Plan 11-02 (apps/web nutzt @tessera/shared nicht, ~45-Werte-Katalog rechtfertigt keine neue Cross-Package-Abhängigkeit)."
|
||||||
|
|
||||||
|
requirements-completed: [FILTER-03, FILTER-05]
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
- id: D1
|
||||||
|
description: "normalizeCpv/divisionOf/cpvMatchesDivisions über alle drei Live-Format-Varianten ('45', '45000000', '45000000-7'); CPV_DIVISIONS-Katalog mit 45 Einträgen, deutschen Labels, keine Duplikate"
|
||||||
|
requirement: "FILTER-03"
|
||||||
|
verification:
|
||||||
|
- kind: unit
|
||||||
|
ref: "apps/api/src/tenders/cpv/cpv-catalog.spec.ts (17 tests)"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D2
|
||||||
|
description: "TenderNormalizerService leitet cpvDivisions (distinct 2-stellige Divisionen) aus cpvCodes ab; leeres cpvCodes -> leeres cpvDivisions, nie ein Crash"
|
||||||
|
requirement: "FILTER-03"
|
||||||
|
verification:
|
||||||
|
- kind: unit
|
||||||
|
ref: "apps/api/src/tenders/tender-normalizer.service.spec.ts (10 tests, davon 2 neu für cpvDivisions)"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D3
|
||||||
|
description: "Backfill-Migration füllt cpvDivisions für Bestandszeilen aus cpvCodes; GIN-Index ergänzt; idempotent (zweiter Lauf: UPDATE 0, ADD COLUMN/CREATE INDEX IF NOT EXISTS überspringen sauber)"
|
||||||
|
requirement: "FILTER-03"
|
||||||
|
verification:
|
||||||
|
- kind: manual_procedural
|
||||||
|
ref: "docker exec tessera-ctl-db-1 psql -U tessera -d tessera -f migration.sql -> ALTER TABLE, CREATE INDEX, UPDATE 1612 (erster Lauf); zweiter Lauf: NOTICE column already exists (skip), NOTICE index already exists (skip), UPDATE 0. Verifikation: cpv=['45']-Filter via cpvDivisions && ARRAY['45'] liefert 741 Zeilen inkl. aller drei Rohformate (175 Zeilen mit bloßem '45'); prisma migrate status -> up to date nach migrate resolve --applied"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D4
|
||||||
|
description: "buildTenderWhere: cpv[] gesetzt -> cpvDivisions hasSome-Branch; mehrere Divisionen kombinierbar; leer/omitted fügt keinen AND-Branch hinzu"
|
||||||
|
requirement: "FILTER-03"
|
||||||
|
verification:
|
||||||
|
- kind: unit
|
||||||
|
ref: "apps/api/src/tenders/tender-query.builder.spec.ts (20 tests, davon 3 neu für cpv)"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D5
|
||||||
|
description: "FilterPanel: CPV-Autocomplete (Label-/Code-Suche, Mehrfachauswahl-Chips mit Entfernen-Button) + Wert-min/max-Zahlenfelder + 'ohne Wertangabe einschließen'-Toggle (Default an, D-05 Pflichtkriterium) — schreibt cpv/valueMin/valueMax/includeNullValue in URL-searchParams"
|
||||||
|
requirement: "FILTER-05"
|
||||||
|
verification: []
|
||||||
|
human_judgment: true
|
||||||
|
rationale: "Visuelle/Interaktions-Verifikation des Autocomplete-Dropdowns, der Chip-Entfernung und der resultierenden Trefferliste erfordert einen Docker-Rebuild + Browser-Check (Docker-Stack läuft noch mit alten Images, wie in 11-01/11-02 dokumentiert) — kein Component-Test in diesem Plan angefordert (Plan-Verify-Kommando zielt ausschließlich auf API-Vitest-Specs); TypeScript-Compile (tsc --noEmit) ist fehlerfrei in beiden Apps, bestehende Web-Component-Tests (ResultsList) bleiben grün."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 11 Plan 03: CPV-Divisions-Katalog, Backfill & Wert-Filter-UI Summary
|
||||||
|
|
||||||
|
**Statischer 45-Divisionen-CPV-Katalog mit zweiseitiger Format-Normalisierung schließt Pitfall 2 (inkonsistente cpvCodes); precomputed `cpvDivisions`-Spalte (hasSome, GIN) ersetzt Raw-SQL; FilterPanel bekommt CPV-Autocomplete + den bislang backend-only Wert-min/max-Filter als UI mit "ohne Wertangabe einschließen"-Toggle (Default an).**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** ~9 min
|
||||||
|
- **Started:** 2026-07-21T16:05:52+02:00
|
||||||
|
- **Completed:** 2026-07-21T16:15:03+02:00
|
||||||
|
- **Tasks:** 3 (Task 1 war TDD, RED-then-GREEN)
|
||||||
|
- **Files modified:** 12 (3 created, 9 modified)
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
- `cpv/cpv-catalog.ts`: statischer 45-Einträge-CPV-Divisions-Katalog (2-stellig, deutsche Labels) + `normalizeCpv()` (führende-Ziffern-Regex, entfernt Prüfziffer/Suffix/Whitespace), `divisionOf()` (führende 2 Ziffern, funktioniert auf normalisierten UND rohen Codes) und `cpvMatchesDivisions()` — 17/17 Tests grün über alle drei Live-Format-Varianten ("45", "45000000", "45000000-7").
|
||||||
|
- `TenderNormalizerService` leitet jetzt `cpvDivisions` (distinct 2-stellige Divisionen) aus `cpvCodes` ab; `tender-ingestion.service.ts` reicht das neue Feld an Prisma-`upsert` (create UND update) durch.
|
||||||
|
- `schema.prisma`: `Tender.cpvDivisions String[] @default([])` + `@@index([cpvDivisions], type: Gin)` ergänzt; Backfill-Migration `20260721150000_tender_cpv_divisions_backfill` lokal angewendet — **1612 von 1671 Zeilen** befüllt (59 Zeilen mit leerem `cpvCodes` bleiben `{}`, kein Datenverlust). Idempotenz verifiziert: zweiter Lauf `UPDATE 0`, `ADD COLUMN IF NOT EXISTS`/`CREATE INDEX IF NOT EXISTS` überspringen sauber.
|
||||||
|
- `buildTenderWhere` um einen konditionalen `cpvDivisions: { hasSome: dto.cpv }`-Branch erweitert (FILTER-03) — typsicher, kein Raw-SQL.
|
||||||
|
- `TenderQueryDto.cpv` (validiertes `string[]`, `@Transform` normalisiert einen einzelnen Query-String-Wert zu einem Ein-Element-Array, da Express nur bei wiederholtem Key ein Array liefert).
|
||||||
|
- `FilterPanel.tsx`: CPV-Autocomplete-Eingabefeld (Suche nach Code oder Label, Dropdown mit bis zu 8 Treffern, Mehrfachauswahl als entfernbare Chips) + Wert-min/max-Zahlenfelder + "ohne Wertangabe einschließen"-Checkbox (Default an) — schreibt `cpv` (wiederholter Key), `valueMin`, `valueMax`, `includeNullValue` in die URL-`searchParams`.
|
||||||
|
- Live-DB-Verifikation: `cpvDivisions && ARRAY['45']` liefert 741 Zeilen — darunter 175 Zeilen, deren `cpvCodes` NUR den bloßen Kurzcode `{45}` enthalten (kein 8-stelliger oder Prüfziffer-Code) — der Kern-Pitfall-2-Fall ist bestätigt gelöst.
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
1. **Task 1a: CPV-Katalog RED** — `a64f984` (test) — failing `cpv-catalog.spec.ts` (17 Tests, Modul existiert noch nicht)
|
||||||
|
2. **Task 1b: CPV-Katalog GREEN** — `7651ab6` (feat) — `cpv-catalog.ts` implementiert, 17/17 Tests grün
|
||||||
|
3. **Task 2: cpvDivisions-Spalte + Normalizer + Backfill-Migration** — `f9591dc` (feat) — Normalizer-Erweiterung, Schema-Spalte + Gin-Index, Backfill-Migration lokal angewendet (1612/1671 Zeilen)
|
||||||
|
4. **Task 3: CPV- + Wert-Filter — Builder, DTO, FilterPanel** — `d60bfd1` (feat) — `cpv[]`-Branch im Builder, DTO-Feld, FilterPanel-Autocomplete + Wert-min/max-UI
|
||||||
|
|
||||||
|
_Task 1 war TDD (RED-then-GREEN); Task 2 und 3 waren `type="auto"` ohne separaten RED-Schritt._
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
- `apps/api/src/tenders/cpv/cpv-catalog.ts` - statischer Divisions-Katalog + Normalisierungs-Helfer (neu)
|
||||||
|
- `apps/api/src/tenders/cpv/cpv-catalog.spec.ts` - 17 Unit-Tests (neu)
|
||||||
|
- `apps/api/prisma/migrations/20260721150000_tender_cpv_divisions_backfill/migration.sql` - Spalte + Gin-Index + idempotenter Backfill-UPDATE (neu)
|
||||||
|
- `apps/api/src/tenders/tender-normalizer.service.ts` - `cpvDivisions` aus `cpvCodes` abgeleitet
|
||||||
|
- `apps/api/src/tenders/tender-normalizer.service.spec.ts` - 2 neue Testfälle (cpvDivisions-Ableitung, leeres cpvCodes)
|
||||||
|
- `apps/api/src/tenders/tender-ingestion.service.ts` - `cpvDivisions` in Upsert create+update durchgereicht
|
||||||
|
- `apps/api/src/tenders/tender.types.ts` - `NormalizedTenderFields.cpvDivisions` ergänzt
|
||||||
|
- `apps/api/prisma/schema.prisma` - `cpvDivisions`-Spalte + Gin-Index
|
||||||
|
- `apps/api/src/tenders/dto/tender-query.dto.ts` - `cpv`-Feld (validiert, transformiert)
|
||||||
|
- `apps/api/src/tenders/tender-query.builder.ts` - `cpvDivisions`-hasSome-Branch
|
||||||
|
- `apps/api/src/tenders/tender-query.builder.spec.ts` - 3 neue Testfälle (cpv einzeln/mehrfach/leer)
|
||||||
|
- `apps/web/src/app/(portal)/modules/tender-radar/components/FilterPanel.tsx` - CPV-Autocomplete + Wert-min/max-UI + includeNullValue-Toggle
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- Kurzkatalog (2-stellig, ~45 Einträge) statt EU-Vollkatalog — bestätigt durch Research Open Question 2 (RESOLVED) und explizite Plan-Vorgabe (D-03).
|
||||||
|
- CPV-Divisions-Labels aus Trainingswissen gegen die Standard-CPV-2008-Divisionsliste zusammengestellt (RESEARCH Assumption A2, tertiäre Quelle) — sollte bei zukünftigem Kontakt mit der offiziellen EU-CPV-Publikation gegengeprüft werden, falls Label-Exaktheit kritisch wird (aktuell kein Blocker: die Codes selbst sind korrekt/stabil, nur die Formulierung der Labels ist die unsichere Dimension).
|
||||||
|
- `cpv`-URL-Param als wiederholter Key statt Komma-Join — reduziert Custom-Split/Join-Code auf beiden Seiten und matcht Express' Default-Verhalten; DTO-`@Transform` fängt den Einzelwert-Fall ab.
|
||||||
|
- CPV_DIVISION_OPTIONS im Web 1:1 aus `apps/api/src/tenders/cpv/cpv-catalog.ts` gespiegelt statt Shared-Package — identisches Muster/Begründung wie `BUNDESLAND_OPTIONS` (Plan 11-02).
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written. Eine kleine, im Plan bereits erwartete Doku-Korrektur: der veraltete Kommentar in `tender-query.dto.ts` ("CPV/favOnly filters are added in later Phase-11 plans (11-03/05)") wurde auf "favOnly filter is added in a later Phase-11 plan (11-05)" aktualisiert, da `cpv` in diesem Plan nun implementiert ist (Rule 1 — Doku-Inkonsistenz, keine funktionale Änderung, im selben Commit wie die DTO-Erweiterung).
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
- Nach der Schema-Erweiterung (`cpvDivisions`-Spalte) schlug `tsc --noEmit` zunächst mit "Object literal may only specify known properties" in `tender-ingestion.service.ts` fehl, weil der generierte Prisma-Client noch den alten Stand hatte — behoben durch `prisma generate` nach der Migration (kein Auto-Fix-Regelverstoß, erwarteter Zwischenschritt der Migrations-Reihenfolge).
|
||||||
|
- Der erste `migration.sql`-Testlauf (`ALTER TABLE ADD COLUMN` ohne `IF NOT EXISTS`) scheiterte beim zweiten manuellen Ausführen mit "column already exists" — vor dem finalen Commit auf `ADD COLUMN IF NOT EXISTS` korrigiert, um echte Idempotenz herzustellen (die im Plan explizit als Anforderung genannt ist) und konsistent mit dem `CREATE INDEX IF NOT EXISTS`-Muster aus der 11-02-Migration zu bleiben.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None - keine externen Service-Konfigurationen erforderlich. Keine neuen npm-Pakete (Package Legitimacy Audit: keine Neuinstallation, CPV-Katalog ist eine statische, repo-committete Datei).
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- FILTER-03 (CPV hierarchisch + Autocomplete, format-robust) und FILTER-05 (Wert min/max UI, NULL-graceful) sind end-to-end abgeschlossen — ROADMAP-Erfolgskriterium 1 (Liste + Filter) ist damit vollständig abgedeckt (alle Filter-Dimensionen aus D-01..D-05 implementiert über Pläne 11-01/02/03).
|
||||||
|
- `buildTenderWhere` hat jetzt sechs unabhängige, additiv kombinierbare AND-Branches (q, openOnly/deadline, value, plz/bundesland/region, cpv) — Plan 11-04 (Detailansicht) und 11-05 (Triage) fügen keine weiteren Query-Filter-Branches hinzu, sondern neue Endpunkte/Komponenten.
|
||||||
|
- **Manuelle UAT ausstehend** (wie in 11-01/11-02 SUMMARY dokumentiert): Der Docker-Stack läuft noch mit den alten Images. Für eine Live-Verifikation im Browser (CPV-Autocomplete "Bau" eingeben → Division 45 auswählen → Trefferliste filtert inkl. Zeilen mit rohem "45"; Wertfilter min setzen bei aktivem Toggle → "keine Wertangabe"-Zeilen bleiben sichtbar) ist `docker compose build api web && docker compose up -d` erforderlich (User führt den Rebuild selbst aus, kein Docker-Deploy durch Claude — projektinterne Regel). Alle automatisierten Tests sind grün: 128/128 API-Tests (`pnpm --filter @tessera/api test`), 7/7 Web-Tests für das tender-radar-Modul; `tsc --noEmit` fehlerfrei in beiden Apps.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 11-filter-engine-results-ui-saved-searches*
|
||||||
|
*Completed: 2026-07-21*
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
All 10 created/modified files verified present on disk; all 4 task commits (a64f984, 7651ab6, f9591dc, d60bfd1) verified in git log.
|
||||||
Reference in New Issue
Block a user