Quellcode durchsuchen

SPEC.md: mark Phase 20 shipped (XLSX import wizard)

Updates §3 (new src/Domain/Import + src/Services/Import directories,
Twig views, ImportController, sample fixture under doc/), §6 (4 new
admin routes), §9 Phase 20 entry pinned at 8876239, §11 expected test
count (143 / 392), §12 resume prompt, §13 git history.

Per the §14 maintenance contract, this is the SPEC-only follow-up to
8876239 ("Phase 20: XLSX import wizard").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chiappa vor 2 Tagen
Ursprung
Commit
62bb8b24ea
1 geänderte Dateien mit 114 neuen und 8 gelöschten Zeilen
  1. 114 8
      SPEC.md

+ 114 - 8
SPEC.md

@@ -25,7 +25,10 @@ per-cell audit trail.
 ## 2. Tech stack (non-negotiable)
 
 - Runtime: Docker, two-stage build, `node:20-alpine` for CSS + JS-vendor
-  copy + `php:8.3-apache` for runtime.
+  copy + `php:8.3-apache` for runtime. The runtime stage installs
+  `pdo_sqlite`, plus `zip` and `gd` (Phase 20 — required by
+  PhpSpreadsheet); `dom`, `xml`, `xmlreader`, `xmlwriter`, `simplexml`,
+  `mbstring`, `fileinfo` ship with the base image.
 - Language: PHP 8.3, strict types, PSR-12.
 - Database: SQLite via PDO, file at `/var/www/data/app.sqlite` (mounted volume).
 - Front end (Phase 19):
@@ -46,7 +49,8 @@ per-cell audit trail.
 - Auth: Microsoft Entra ID via OpenID Connect (Authorization Code + PKCE),
   plus an optional env-configured "local admin" fallback for dev / on-prem.
 - Composer deps: `twig/twig`, `jumbojett/openid-connect-php`,
-  `vlucas/phpdotenv`, `phpunit/phpunit` (dev).
+  `vlucas/phpdotenv`, `phpoffice/phpspreadsheet` (Phase 20 — XLSX import
+  wizard), `phpunit/phpunit` (dev).
 - npm deps (build-time only): `tailwindcss`, `alpinejs`, `@alpinejs/csp`,
   `htmx.org`, `sortablejs`.
 
@@ -64,7 +68,8 @@ per-cell audit trail.
 ├── ACCEPTANCE.md               # spec §10 manual checklist walkthrough
 ├── SPEC.md                     # this file
 ├── doc/
-│   └── admin-manual.md         # operator-facing setup + run guide
+│   ├── admin-manual.md         # operator-facing setup + run guide
+│   └── Tool_Sprint Planning.xlsx  # Phase 20 — sample workbook (parser fixture)
 ├── assets/
 │   └── css/input.css           # Tailwind entry, compiled into public/assets/css/app.css
 ├── public/
@@ -85,10 +90,12 @@ per-cell audit trail.
 │   ├── Auth/            LocalAdmin, OidcClient, SessionGuard
 │   ├── Controllers/     AuthController, WorkerController, SprintController,
 │   │                    TaskController, AuditController, UserController,
-│   │                    SettingsController
+│   │                    SettingsController, ImportController (Phase 20)
 │   ├── Db/              Connection, Migrator
 │   ├── Domain/          User, Worker, Sprint, SprintWeek, SprintWorker,
 │   │                    SprintWorkerDay, Task, TaskAssignment
+│   │   └── Import/      (Phase 20) ParsedSheet, ParsedWeek, ParsedWorker,
+│   │                    ParsedTask, ParsedAssignment, ImportResult
 │   ├── Http/            Request, Response, Router, View (+ e() helper)
 │   ├── Repositories/    UserRepository, WorkerRepository, SprintRepository,
 │   │                    SprintWeekRepository, SprintWorkerRepository,
@@ -96,6 +103,8 @@ per-cell audit trail.
 │   │                    TaskAssignmentRepository, AuditRepository,
 │   │                    AppSettingsRepository
 │   └── Services/        AuditLogger, CapacityCalculator
+│       └── Import/      (Phase 20) XlsxColorClassifier, XlsxSprintImporter,
+│                        SprintImporter
 ├── migrations/          001_init.sql (full schema per spec §3)
 │                        002_sprint_week_active_days.sql (Phase 12 — mask column)
 │                        003_task_status_and_app_settings.sql (Phase 18 — task-cell status + KV)
@@ -104,7 +113,9 @@ per-cell audit trail.
 │                        users/index.twig, audit/index.twig,
 │                        settings/index.twig,
 │                        sprints/{new,show,settings,present}.twig,
-│                        sprints/_task_list.twig (shared partial)
+│                        sprints/_task_list.twig (shared partial),
+│                        sprints/import_upload.twig (Phase 20),
+│                        sprints/import_preview.twig (Phase 20)
 ├── tests/               TestCase + Services/ + Repositories/ + Controllers/ +
 │                        Cascade/ + Domain/ + Db/ + Http/ (Phase 19 TwigViewTest)
 └── data/                SQLite + sessions directory + twig-cache/
@@ -185,6 +196,10 @@ Pages (HTML):
 | POST   | `/users/{id}`               | admin          |
 | GET    | `/sprints/new`              | admin          |
 | POST   | `/sprints`                  | admin          |
+| GET    | `/sprints/import`           | admin          |
+| POST   | `/sprints/import`           | admin (multipart, `_csrf`) |
+| GET    | `/sprints/import/{token}`   | admin          |
+| POST   | `/sprints/import/{token}`   | admin (form `_csrf`) |
 | GET    | `/sprints/{id}`             | signed-in      |
 | GET    | `/sprints/{id}/present`     | signed-in      |
 | GET    | `/sprints/{id}/settings`    | admin          |
@@ -846,6 +861,81 @@ with a `BOOTSTRAP_ADMIN` audit row.
       `.assign-status-*` safelist + status-select styling all carry
       over unchanged.
 
+- [x] **Phase 20 — XLSX import wizard** (`8876239`). Two-step
+      admin-only wizard at `/sprints/import` that ingests the team's
+      historical *Tool_Sprint Planning.xlsx* workbook into the database
+      one tab per sprint. Step 1 is a multipart upload form with strict
+      validation (≤ 5 MB, `.xlsx` extension, `PK\x03\x04`/`PK\x05\x06`
+      ZIP magic-byte check). Step 2 (`/sprints/import/{token}` where
+      `token` is a 32-char hex random session key, TTL 30 min) shows
+      one preview panel per parsed sheet: sprint-name input pre-filled
+      from the tab name, inferred start/end dates pre-filled from
+      `KW`-row + closest-year heuristic (using `setISODate()` over
+      ±1 year and picking the nearest match to `today`), reserve
+      fraction read-only from the workbook, target picker (Create new
+      sprint / Merge into empty existing sprint — non-empty sprints are
+      filtered out of the dropdown server-side), diff summary listing
+      workers-to-create + assignment-cell totals + per-status colour
+      counts, and a per-sheet "skip" toggle. New service trio:
+      `App\Services\Import\XlsxColorClassifier` (pure ARGB → status,
+      hue/saturation thresholds with no PhpSpreadsheet dep — bins green
+      H 80..170/S>0.15, yellow+orange H 20..80, red H 340..20/S>0.20,
+      everything else → `zugewiesen`; near-white L>0.96 and S<0.10
+      banding both fall through to default), `App\Services\Import\
+      XlsxSprintImporter` (parses with the fixed coordinate map locked
+      down from the sample file: `C6` = "Arbeitstage" / `E..` = max
+      working days, `C7` = "Datum", `C8` = "KW", `C9..` worker name +
+      `E..` per-week days + `J` RTB + `K` Σ-formula, `J{r}` = "Reserven"
+      ends the worker block; `M4` = "Tasks", `Q4..` worker-name
+      formulas (=C9, =C10, …) for the authoritative task-column →
+      Arbeitstage worker mapping which skips Arbeitstage gaps, `M9..P9`
+      = To Do/Owner/Prio/Tot header, `M10..` task title with `N`
+      owner, `O` prio, `Q..` per-worker assignments; the parser
+      tolerates up to 2 consecutive empty rows in both blocks so the
+      sample workbook's row-13 visual gap doesn't truncate Sprint 2),
+      and `App\Services\Import\SprintImporter` (transactional commit
+      that creates or merges-into-empty sprints, materialises
+      sprint_weeks with `active_days_mask = (1 << maxDays) − 1` and
+      `max_working_days = popcount(mask)`, auto-creates missing
+      workers by case-folded exact name with audit each, drops
+      task-owner names that don't resolve into the global Workers
+      table into a `missingOwners` warning list and creates the task
+      with `owner_worker_id = NULL`, writes per-week days through the
+      existing `SprintWorkerDayRepository::upsert` and per-cell days
+      + status through `TaskAssignmentRepository::{upsert,upsertStatus}`
+      so the same audit semantics already in production carry over;
+      one `IMPORTED_FROM_XLSX` audit row per sprint anchors the run).
+      Composer dep: `phpoffice/phpspreadsheet ^3.4`. Dockerfile
+      runtime stage adds `zip` + `gd` (PhpSpreadsheet require) on top
+      of the existing `pdo_sqlite`. New `App\Domain\Import\` value
+      objects (ParsedSheet/Week/Worker/Task/Assignment + ImportResult)
+      round-trip through `toArray()/fromArray()` so the parsed result
+      can be JSON-stashed in `$_SESSION['sp_imports'][$token]` between
+      steps without persisting the uploaded file to disk. Hamburger
+      menu gains an "Import" link next to "New sprint" for admins.
+      Strict CSP unchanged — no JS in the wizard, native form posts
+      throughout. Tests: 143/392 (was 108/281) — pure-table-driven
+      `XlsxColorClassifierTest` (20 cases incl. all six fills observed
+      in the sample workbook + edges like fully-transparent, pure
+      white, pure red, lavender), `XlsxSprintImporterTest` (5 smoke
+      tests against `doc/Tool_Sprint Planning.xlsx`: 3 sheets parsed,
+      Sprint 1 shape 5w/15w/>20t/0.2 reserve, Sprint 2 colour-count
+      matches openpyxl audit (27 yellow/orange → gestartet, 5 green
+      → abgeschlossen, 0 red), Sprint 2 16-worker count tolerating
+      the row-13 gap, `toArray/fromArray` round-trip — auto-skipped
+      when host PHP lacks `dom`/`zip`/`xmlreader`/`simplexml`/`gd`
+      so the suite stays portable to thin developer environments),
+      `SprintImporterCommitTest` (5 cases on the in-memory SQLite
+      harness: full new-sprint commit shape + audit row count, refuse
+      to merge into a non-empty existing sprint, case-folded worker
+      reuse with leading/trailing whitespace, `maxDaysToMask` table,
+      `fold` normalisation), `ImportControllerTest` (3 cases on the
+      static helpers via reflection: ZIP magic-byte detection, wrong
+      extension rejection, `UPLOAD_ERR_*` mapping). The classifier
+      test was originally written with the legacy `@dataProvider`
+      doc-block annotation; PHPUnit 11 deprecated metadata in
+      doc-comments so we use the `#[DataProvider]` attribute.
+
 ### Upcoming
 
 Nothing scheduled.
@@ -897,7 +987,20 @@ for f in $(git ls-files '*.php'); do php -l "$f" | tail -1 | sed "s|^|$f: |"; do
 Run the test suite:
 ```bash
 vendor/bin/phpunit
-# → OK (108 tests, 281 assertions)
+# → OK (143 tests, 392 assertions)
+```
+
+The Phase 20 parser tests need `ext-dom`, `ext-zip`, `ext-xmlreader`,
+`ext-simplexml`, and `ext-gd` (PhpSpreadsheet's hard requires); on hosts
+that don't have all of them the parser tests auto-skip via
+`extension_loaded()` in `setUp()`. Run inside the Docker image when
+the host PHP is thin:
+```bash
+docker compose build
+docker run --rm -v "$(pwd):/app" -w /app sprint_planer_web-app:latest \
+    sh -c "git config --global --add safe.directory /app \
+        && composer install --no-interaction --no-progress \
+        && vendor/bin/phpunit --colors=never"
 ```
 
 ## 12. How to resume in a fresh Claude session
@@ -905,13 +1008,15 @@ vendor/bin/phpunit
 Tell Claude:
 
 > Working on `/Users/achiappa/Development/claude_code_private/sprint_planer_web`.
-> Read `SPEC.md`, the git log, and `ACCEPTANCE.md`. Phases 1–19
+> Read `SPEC.md`, the git log, and `ACCEPTANCE.md`. Phases 1–20
 > are shipped (see §9; Phase 17's slider popover was removed —
 > typed entry is now the only edit path on number inputs; Phase 18
 > added per-cell task-status colours + filter + a new /settings page
 > gated by a global flag that's off by default; Phase 19 swapped the
 > stack to Twig 3 + Tailwind 3 + Alpine CSP + htmx + SortableJS and
-> removed jQuery + jQuery UI completely). Nothing is currently
+> removed jQuery + jQuery UI completely; Phase 20 added a two-step
+> XLSX import wizard at `/sprints/import` powered by PhpSpreadsheet,
+> with a colour-coded cell → status mapping). Nothing is currently
 > scheduled. Outstanding items are in §10 (mostly a human-run
 > acceptance walkthrough in the running container). If I ask you to
 > plan or work a new phase, follow the maintenance rule in §14 —
@@ -924,6 +1029,7 @@ before acting — nothing here is load-bearing once it grows stale.
 ## 13. Git history (as of this writing)
 
 ```
+8876239 Phase 20: XLSX import wizard (phpspreadsheet + colour→status)
 2813019 Sprint view: tabs (Arbeitstage / Tasks) + smart Close on present
 10ea4b8 Cell popover: replace per-cell status select with slider + status pills
 1864835 Fix: filter dropdown close — grace timer for transit gap + close on Clear