From a4c02116c174cc6f4c1fee6dd726de85a74bbf95 Mon Sep 17 00:00:00 2001 From: Schalli Date: Tue, 21 Jul 2026 16:05:25 +0200 Subject: [PATCH] docs(11-02): complete region/plz/bundesland filter plan --- .planning/REQUIREMENTS.md | 4 +- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 17 +- .../11-02-SUMMARY.md | 159 ++++++++++++++++++ 4 files changed, 174 insertions(+), 12 deletions(-) create mode 100644 .planning/phases/11-filter-engine-results-ui-saved-searches/11-02-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 628d2d8..4ecfe31 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -27,7 +27,7 @@ ### FILTER — Suche & Profile - [x] **FILTER-01**: Nutzer kann per Volltext-Stichwort (Titel/Beschreibung) suchen. -- [ ] **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-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. @@ -82,7 +82,7 @@ | SCHEMA-01 | Phase 10 | Complete | | SCHEMA-02 | Phase 10 | Complete | | FILTER-01 | Phase 11 | Complete | -| FILTER-02 | Phase 11 | Pending | +| FILTER-02 | Phase 11 | Complete | | FILTER-03 | Phase 11 | Pending | | FILTER-04 | Phase 11 | Complete | | FILTER-05 | Phase 11 | Complete | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 2cedc35..742a4c8 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -386,10 +386,10 @@ 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 5. The UI clearly indicates data coverage (Oberschwelle vs. Unterschwelle) so an empty or thin result set isn't mistaken for a bug -**Plans**: 1/6 plans executed +**Plans**: 2/6 plans executed - [x] 11-01-PLAN.md — Trefferliste + Freitext + Sortierung + Coverage-Banner (FILTER-01/04/05, UI-01/05) -- [ ] 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) - [ ] 11-04-PLAN.md — Detailansicht via ?tender= + Quell-Link (UI-02) - [ ] 11-05-PLAN.md — Triage: gelesen/ungelesen + Favorit + Merklisten-Filter (UI-03/04) @@ -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 | | 9. Cert Manager Module | 6/6 | Complete | 2026-07-02 | | 10. Ausschreibungs-Radar Foundation & DÖE Ingestion | 6/6 | Complete | 2026-07-21 | -| 11. Filter Engine, Results UI & Saved Searches | 1/6 | In Progress| | +| 11. Filter Engine, Results UI & Saved Searches | 2/6 | In Progress| | | 12. Tender Notifications | 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 | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index f94cfcd..a38f889 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,15 +5,15 @@ milestone_name: Ausschreibungs-Radar current_phase: 11 current_phase_name: filter-engine-results-ui-saved-searches status: executing -stopped_at: Completed 11-01-PLAN.md -last_updated: "2026-07-21T13:57:16.027Z" +stopped_at: Completed 11-02-PLAN.md +last_updated: "2026-07-21T14:05:18.391Z" last_activity: 2026-07-21 last_activity_desc: Phase 11 execution started progress: total_phases: 11 completed_phases: 9 total_plans: 52 - completed_plans: 46 + completed_plans: 47 --- # Project State @@ -28,11 +28,11 @@ See: .planning/PROJECT.md (updated 2026-07-17) ## Current Position Phase: 11 (filter-engine-results-ui-saved-searches) — EXECUTING -Plan: 2 of 6 +Plan: 3 of 6 Status: Ready to execute Last activity: 2026-07-21 — Phase 11 execution started -Progress: [█████████░] 88% +Progress: [█████████░] 90% ## Performance Metrics @@ -81,6 +81,7 @@ Progress: [█████████░] 88% | Plan | Duration | Tasks | Files | |------|----------|-------|-------| | Phase 11 P01 | 8min | 3 tasks | 11 files | +| Phase 11 P02 | 4min | 3 tasks | 12 files | ## Accumulated Context @@ -167,6 +168,8 @@ Recent decisions affecting current work: - [Phase ?]: estimatedValue kommt als String (Prisma Decimal) im JSON-Response — formatValue() im Frontend prüft explizit auf null/NaN statt zu koerzieren - [Phase ?]: openOnly/includeNullValue Default-Semantik lebt im Builder, nicht im DTO - [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_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. ### Pending Todos @@ -204,7 +207,7 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-07-21T13:57:16.014Z -Stopped at: Completed 11-01-PLAN.md +Last session: 2026-07-21T14:05:18.378Z +Stopped at: Completed 11-02-PLAN.md 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 diff --git a/.planning/phases/11-filter-engine-results-ui-saved-searches/11-02-SUMMARY.md b/.planning/phases/11-filter-engine-results-ui-saved-searches/11-02-SUMMARY.md new file mode 100644 index 0000000..227d8b4 --- /dev/null +++ b/.planning/phases/11-filter-engine-results-ui-saved-searches/11-02-SUMMARY.md @@ -0,0 +1,159 @@ +--- +phase: 11-filter-engine-results-ui-saved-searches +plan: 02 +subsystem: api, ui, database +tags: [nestjs, prisma, class-validator, nuts, 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)" +provides: + - "geo/nuts-bundesland.ts: bundeslandFromRegion()/nutsPrefixFor() — NUTS-1-Präfix (DE1..DEG) -> 16 Bundesland-Namen, null-safe" + - "TenderNormalizerService setzt bundesland aus region ab (statt hartcodiert null) — neue Ingests sofort filterbar" + - "Backfill-Migration 20260721140000_tender_bundesland_backfill: 933/1671 Bestandszeilen mit bundesland befüllt + @@index([bundesland])" + - "buildTenderWhere: plz-startsWith, bundesland-exact, region-startsWith Branches" + - "FilterPanel.tsx: PLZ-Feld + 16-Länder-Bundesland-Dropdown, schreibt in URL-searchParams" +affects: [11-03-cpv-filter, 11-04-detail-view, 11-05-triage, 11-06-saved-searches] + +tech-stack: + added: [] + patterns: + - "Reine, Nest-unabhängige geo/nuts-bundesland.ts-Datei (wie tender-query.builder.ts) — pure Function, isoliert testbar ohne DB" + - "Handgeschriebene idempotente Backfill-Migration (UPDATE ... WHERE col IS NULL) statt destruktivem Rewrite — sicher mehrfach ausführbar" + - "Kleine Konstante im Web gespiegelt (BUNDESLAND_OPTIONS in FilterPanel.tsx) statt Shared-Package für einen stabilen 16-Werte-Katalog" + +key-files: + created: + - apps/api/src/tenders/geo/nuts-bundesland.ts + - apps/api/src/tenders/geo/nuts-bundesland.spec.ts + - apps/api/prisma/migrations/20260721140000_tender_bundesland_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.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: + - "bundesland-Filter matcht exakt gegen die (jetzt befüllte, indexierte) bundesland-Spalte statt region-startsWith — schneller und robuster nach dem Backfill; region bleibt als eigener, unabhängiger Präfixfilter erhalten (auch für Zeilen ohne bundesland-Derivation nützlich)." + - "BUNDESLAND_OPTIONS wird im Web-Package als kleine Konstante gespiegelt (nicht aus @tessera/shared bezogen) — web nutzt @tessera/shared aktuell gar nicht, ein 16-Werte-Katalog rechtfertigt keine neue Cross-Package-Abhängigkeit (RESEARCH-Vorgabe: 'kleine Konstante spiegeln' explizit als Option genannt)." + - "Migration wurde per docker exec psql lokal angewendet und danach via `prisma migrate resolve --applied` in der _prisma_migrations-Historie nachgezogen (DATABASE_URL musste für Host-Prisma-Aufrufe explizit auf die Container-IP gesetzt werden — .env im Container zeigt intern auf den Service-Namen, vom Host aus nicht auflösbar)." + +requirements-completed: [FILTER-02] + +coverage: + - id: D1 + description: "NUTS-1 (DE1..DEG) -> 16 Bundesland-Namen-Map, null-safe für fehlende/unbekannte region, Reverse-Lookup nutsPrefixFor(), gegen echte DB-Region-Stichproben (DE212/DE300/DE600/DE712/DEA22) validiert" + requirement: "FILTER-02" + verification: + - kind: unit + ref: "apps/api/src/tenders/geo/nuts-bundesland.spec.ts (7 tests)" + status: pass + human_judgment: false + - id: D2 + description: "TenderNormalizerService setzt bundesland = bundeslandFromRegion(region) statt hartcodiertem null; region=null -> bundesland=null ohne Crash" + requirement: "FILTER-02" + verification: + - kind: unit + ref: "apps/api/src/tenders/tender-normalizer.service.spec.ts (8 tests, davon 2 neu für bundesland)" + status: pass + human_judgment: false + - id: D3 + description: "Backfill-Migration füllt bundesland für Bestandszeilen aus region-NUTS-1-Präfix; @@index([bundesland]) ergänzt; idempotent (zweiter Lauf: UPDATE 0)" + requirement: "FILTER-02" + verification: + - kind: manual_procedural + ref: "docker exec tessera-ctl-db-1 psql -U tessera -d tessera -c \"SELECT bundesland, count(*) FROM \\\"Tender\\\" GROUP BY bundesland\" -> 933/1671 Zeilen über alle 16 Länder befüllt, 738 NULL wo region selbst NULL ist; zweiter Migrationslauf UPDATE 0 (idempotent bestätigt); prisma migrate status -> up to date" + status: pass + human_judgment: false + - id: D4 + description: "buildTenderWhere: plz-startsWith, bundesland-exact (gegen befüllte Spalte), region-startsWith Branches — nur angehängt wenn gesetzt" + requirement: "FILTER-02" + verification: + - kind: unit + ref: "apps/api/src/tenders/tender-query.builder.spec.ts (17 tests, davon 4 neu für plz/bundesland/region)" + status: pass + human_judgment: false + - id: D5 + description: "FilterPanel erweitert um PLZ-Eingabefeld + 16-Länder-Bundesland-Dropdown, schreibt in URL-searchParams (deep-linkbar, ResultsList/listTenders leiten alle Params bereits transparent weiter)" + requirement: "FILTER-02" + verification: [] + human_judgment: true + rationale: "Visuelle/Interaktions-Verifikation des Dropdowns und der resultierenden Trefferliste erfordert einen Docker-Rebuild + Browser-Check (Docker-Stack läuft noch mit alten Images) — kein Component-Test in diesem Plan angefordert; Unit-Tests decken die Builder-Logik ab, nicht das Rendering." +--- + +# Phase 11 Plan 02: NUTS-Bundesland-Ableitung, Backfill & Region/PLZ/Bundesland-Filter Summary + +**NUTS-1-Präfix-Ableitung macht den bis dato konstant-leeren Bundesland-Filter erstmals funktionsfähig: Normalizer-Fix für neue Ingests + Backfill-Migration füllt 933/1671 Bestandszeilen über alle 16 Länder, verdrahtet in Query-Builder + FilterPanel.** + +## Performance + +- **Duration:** ~4 min +- **Started:** 2026-07-21T15:59:14+02:00 +- **Completed:** 2026-07-21T16:03:21+02:00 +- **Tasks:** 3 (Task 1 war TDD, RED-then-GREEN) +- **Files modified:** 12 (3 created, 9 modified) + +## Accomplishments +- `geo/nuts-bundesland.ts`: feste `NUTS1_BUNDESLAND`-Map (16 Einträge), `bundeslandFromRegion()` (null-safe, case-insensitive Präfix-Match) und `nutsPrefixFor()` (Reverse-Lookup) — gegen 5 echte `region`-Stichproben aus der Live-DB validiert (DE212→Bayern, DE300→Berlin, DE600→Hamburg, DE712→Hessen, DEA22→Nordrhein-Westfalen). +- `TenderNormalizerService.normalize()` setzt `bundesland = bundeslandFromRegion(region)` statt der in Phase 10 hartcodierten `null`-Zuweisung — jede neue Ingestion bekommt sofort ein abgeleitetes Bundesland. +- Handgeschriebene Backfill-Migration `20260721140000_tender_bundesland_backfill`: `CREATE INDEX IF NOT EXISTS "Tender_bundesland_idx"` + idempotenter `UPDATE ... CASE upper(left(region,3)) ... WHERE bundesland IS NULL AND region IS NOT NULL` — lokal angewendet, **933 von 1671 Zeilen** über alle 16 Bundesländer befüllt (738 bleiben NULL, weil `region` selbst NULL ist — kein Datenverlust, kein Crash). +- `schema.prisma`: `@@index([bundesland])` ergänzt für Filter-Performance; `prisma generate` neu ausgeführt. +- `buildTenderWhere` (Query-Builder) um drei konditionale AND-Branches erweitert: `plz` (startsWith), `bundesland` (exakt, gegen die jetzt befüllte indexierte Spalte), `region` (startsWith, unabhängig nutzbar). +- `TenderQueryDto` um validierte `plz`/`region`/`bundesland`-Felder erweitert (`@IsString`, `plz` zusätzlich `@MaxLength(5)`). +- `FilterPanel.tsx`: neues PLZ-Eingabefeld + Bundesland-Dropdown mit den 16 Ländernamen (im Web als kleine Konstante gespiegelt), schreibt in die URL-`searchParams` — `ResultsList`/`listTenders()` leiten sämtliche Params bereits transparent an die API weiter, keine zusätzliche Fetch-Verdrahtung nötig. + +## Task Commits + +1. **Task 1a: NUTS-Bundesland-Map RED** — `3dec35f` (test) — failing `nuts-bundesland.spec.ts` (7 Tests, Modul existiert noch nicht) +2. **Task 1b: NUTS-Bundesland-Map GREEN** — `d12e1c9` (feat) — `nuts-bundesland.ts` implementiert, 7/7 Tests grün +3. **Task 2: Normalizer + Backfill-Migration** — `e4db602` (feat) — Normalizer-Fix, Schema-Index, Backfill-Migration lokal angewendet + `prisma migrate resolve --applied` +4. **Task 3: Query-Builder + DTO + FilterPanel** — `69e2529` (feat) — plz/bundesland/region-Branches, DTO-Felder, FilterPanel-Erweiterung + +_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/geo/nuts-bundesland.ts` - NUTS-1→Bundesland-Map + Ableitungs-/Reverse-Funktionen (neu) +- `apps/api/src/tenders/geo/nuts-bundesland.spec.ts` - 7 Unit-Tests (neu) +- `apps/api/prisma/migrations/20260721140000_tender_bundesland_backfill/migration.sql` - Index + idempotenter Backfill-UPDATE (neu) +- `apps/api/src/tenders/tender-normalizer.service.ts` - `bundesland` aus `region` abgeleitet statt hartcodiert `null` +- `apps/api/src/tenders/tender-normalizer.service.spec.ts` - 2 neue Testfälle (region→bundesland, region=null→bundesland=null) +- `apps/api/src/tenders/tender.types.ts` - Dokumentationskommentar für `bundesland`/`region` aktualisiert (nicht mehr "deferred") +- `apps/api/prisma/schema.prisma` - `@@index([bundesland])` ergänzt +- `apps/api/src/tenders/dto/tender-query.dto.ts` - `plz`/`region`/`bundesland` validierte Felder ergänzt +- `apps/api/src/tenders/tender-query.builder.ts` - drei neue konditionale `AND`-Branches +- `apps/api/src/tenders/tender-query.builder.spec.ts` - 4 neue Testfälle (plz/bundesland/region gesetzt + alle drei omitted wenn leer) +- `apps/web/src/app/(portal)/modules/tender-radar/components/FilterPanel.tsx` - PLZ-Feld + Bundesland-Dropdown (16 Länder) + +## Decisions Made +- `bundesland`-Filter matcht exakt gegen die jetzt befüllte, indexierte `bundesland`-Spalte — schneller als `region`-Präfix-Matching und die vom Plan empfohlene Post-Backfill-Variante; `region`-Präfixfilter bleibt als eigenständiger, von `bundesland` unabhängiger Branch erhalten (nützlich für Zeilen, deren `bundesland`-Ableitung z. B. wegen eines unbekannten Präfixes leer blieb). +- `BUNDESLAND_OPTIONS` wird im Web-Package als kleine, dokumentierte Konstante gespiegelt statt über ein Shared-Package bezogen — `apps/web` nutzt `@tessera/shared` aktuell gar nicht, und ein stabiler 16-Werte-EU-Katalog rechtfertigt keine neue Cross-Package-Abhängigkeit (RESEARCH nennt "kleine Konstante spiegeln" explizit als gleichwertige Option). +- Migration lokal per `docker exec tessera-ctl-db-1 psql -U tessera -d tessera` angewendet (kein Host-Port auf dem DB-Container) und danach via `prisma migrate resolve --applied` in die `_prisma_migrations`-Historie nachgezogen, damit `prisma migrate status` konsistent bleibt — `DATABASE_URL` musste für den Host-Prisma-CLI-Aufruf explizit auf die Container-IP (`172.19.0.2`) gesetzt werden, da der im API-Container gültige `.env`-Hostname vom Host aus nicht auflösbar ist. + +## Deviations from Plan + +None - plan executed exactly as written. Eine kleine Ergänzung: der veraltete "Deferred to Phase 11"-Dokumentationskommentar in `tender.types.ts` wurde konsistent mit der neuen Implementierung aktualisiert (Rule 1 — Bug/Doku-Inkonsistenz, keine funktionale Änderung, im selben Commit wie der Normalizer-Fix). + +## Issues Encountered +- `prisma migrate status`/`prisma migrate resolve` schlugen zunächst mit "Environment variable not found: DATABASE_URL" fehl, weil das Host-Shell-Environment die im Container gültige `.env` nicht automatisch lädt bzw. der dortige Hostname vom Host nicht auflösbar ist — gelöst durch expliziten `DATABASE_URL=postgresql://tessera:tessera_dev@172.19.0.2:5432/tessera`-Präfix (MEMORY-Hinweis "Lokale DB-Migrationen"). + +## User Setup Required + +None - keine externen Service-Konfigurationen erforderlich. Keine neuen npm-Pakete. + +## Next Phase Readiness + +- Bundesland-Filter liefert jetzt echte Treffer (933/1671 Zeilen über alle 16 Länder) statt konstant 0 — Kern-Pitfall des Plans behoben. +- `plz`/`region`/`bundesland` sind als AND-Branches im Builder etabliert; Plan 11-03 (CPV) fügt einen weiteren unabhängigen Branch nach demselben Muster hinzu, ohne bestehende Branches zu ändern. +- **Manuelle UAT ausstehend** (wie in 11-01 SUMMARY dokumentiert): Der Docker-Stack läuft noch mit den alten Images. Für eine Live-Verifikation im Browser (Bundesland-Dropdown wählen → Trefferliste filtert; PLZ-Präfix eintragen → Trefferliste filtert) ist `docker compose build api web && docker compose up -d` erforderlich (User führt den Rebuild selbst aus). Alle automatisierten Tests sind grün: 106/106 API-Tests (`pnpm --filter @tessera/api test`), 114/114 Web-Tests (`pnpm --filter @tessera/web test`); `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 source files verified present on disk; all 4 task commits (3dec35f, d12e1c9, e4db602, 69e2529) verified in git log.