|
@@ -25,7 +25,10 @@ per-cell audit trail.
|
|
|
## 2. Tech stack (non-negotiable)
|
|
## 2. Tech stack (non-negotiable)
|
|
|
|
|
|
|
|
- Runtime: Docker, two-stage build, `node:20-alpine` for CSS + JS-vendor
|
|
- 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.
|
|
- Language: PHP 8.3, strict types, PSR-12.
|
|
|
- Database: SQLite via PDO, file at `/var/www/data/app.sqlite` (mounted volume).
|
|
- Database: SQLite via PDO, file at `/var/www/data/app.sqlite` (mounted volume).
|
|
|
- Front end (Phase 19):
|
|
- Front end (Phase 19):
|
|
@@ -46,7 +49,8 @@ per-cell audit trail.
|
|
|
- Auth: Microsoft Entra ID via OpenID Connect (Authorization Code + PKCE),
|
|
- Auth: Microsoft Entra ID via OpenID Connect (Authorization Code + PKCE),
|
|
|
plus an optional env-configured "local admin" fallback for dev / on-prem.
|
|
plus an optional env-configured "local admin" fallback for dev / on-prem.
|
|
|
- Composer deps: `twig/twig`, `jumbojett/openid-connect-php`,
|
|
- 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`,
|
|
- npm deps (build-time only): `tailwindcss`, `alpinejs`, `@alpinejs/csp`,
|
|
|
`htmx.org`, `sortablejs`.
|
|
`htmx.org`, `sortablejs`.
|
|
|
|
|
|
|
@@ -64,7 +68,8 @@ per-cell audit trail.
|
|
|
├── ACCEPTANCE.md # spec §10 manual checklist walkthrough
|
|
├── ACCEPTANCE.md # spec §10 manual checklist walkthrough
|
|
|
├── SPEC.md # this file
|
|
├── SPEC.md # this file
|
|
|
├── doc/
|
|
├── 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/
|
|
├── assets/
|
|
|
│ └── css/input.css # Tailwind entry, compiled into public/assets/css/app.css
|
|
│ └── css/input.css # Tailwind entry, compiled into public/assets/css/app.css
|
|
|
├── public/
|
|
├── public/
|
|
@@ -85,10 +90,12 @@ per-cell audit trail.
|
|
|
│ ├── Auth/ LocalAdmin, OidcClient, SessionGuard
|
|
│ ├── Auth/ LocalAdmin, OidcClient, SessionGuard
|
|
|
│ ├── Controllers/ AuthController, WorkerController, SprintController,
|
|
│ ├── Controllers/ AuthController, WorkerController, SprintController,
|
|
|
│ │ TaskController, AuditController, UserController,
|
|
│ │ TaskController, AuditController, UserController,
|
|
|
-│ │ SettingsController
|
|
|
|
|
|
|
+│ │ SettingsController, ImportController (Phase 20)
|
|
|
│ ├── Db/ Connection, Migrator
|
|
│ ├── Db/ Connection, Migrator
|
|
|
│ ├── Domain/ User, Worker, Sprint, SprintWeek, SprintWorker,
|
|
│ ├── Domain/ User, Worker, Sprint, SprintWeek, SprintWorker,
|
|
|
│ │ SprintWorkerDay, Task, TaskAssignment
|
|
│ │ SprintWorkerDay, Task, TaskAssignment
|
|
|
|
|
+│ │ └── Import/ (Phase 20) ParsedSheet, ParsedWeek, ParsedWorker,
|
|
|
|
|
+│ │ ParsedTask, ParsedAssignment, ImportResult
|
|
|
│ ├── Http/ Request, Response, Router, View (+ e() helper)
|
|
│ ├── Http/ Request, Response, Router, View (+ e() helper)
|
|
|
│ ├── Repositories/ UserRepository, WorkerRepository, SprintRepository,
|
|
│ ├── Repositories/ UserRepository, WorkerRepository, SprintRepository,
|
|
|
│ │ SprintWeekRepository, SprintWorkerRepository,
|
|
│ │ SprintWeekRepository, SprintWorkerRepository,
|
|
@@ -96,6 +103,8 @@ per-cell audit trail.
|
|
|
│ │ TaskAssignmentRepository, AuditRepository,
|
|
│ │ TaskAssignmentRepository, AuditRepository,
|
|
|
│ │ AppSettingsRepository
|
|
│ │ AppSettingsRepository
|
|
|
│ └── Services/ AuditLogger, CapacityCalculator
|
|
│ └── Services/ AuditLogger, CapacityCalculator
|
|
|
|
|
+│ └── Import/ (Phase 20) XlsxColorClassifier, XlsxSprintImporter,
|
|
|
|
|
+│ SprintImporter
|
|
|
├── migrations/ 001_init.sql (full schema per spec §3)
|
|
├── migrations/ 001_init.sql (full schema per spec §3)
|
|
|
│ 002_sprint_week_active_days.sql (Phase 12 — mask column)
|
|
│ 002_sprint_week_active_days.sql (Phase 12 — mask column)
|
|
|
│ 003_task_status_and_app_settings.sql (Phase 18 — task-cell status + KV)
|
|
│ 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,
|
|
│ users/index.twig, audit/index.twig,
|
|
|
│ settings/index.twig,
|
|
│ settings/index.twig,
|
|
|
│ sprints/{new,show,settings,present}.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/ +
|
|
├── tests/ TestCase + Services/ + Repositories/ + Controllers/ +
|
|
|
│ Cascade/ + Domain/ + Db/ + Http/ (Phase 19 TwigViewTest)
|
|
│ Cascade/ + Domain/ + Db/ + Http/ (Phase 19 TwigViewTest)
|
|
|
└── data/ SQLite + sessions directory + twig-cache/
|
|
└── data/ SQLite + sessions directory + twig-cache/
|
|
@@ -185,6 +196,10 @@ Pages (HTML):
|
|
|
| POST | `/users/{id}` | admin |
|
|
| POST | `/users/{id}` | admin |
|
|
|
| GET | `/sprints/new` | admin |
|
|
| GET | `/sprints/new` | admin |
|
|
|
| POST | `/sprints` | 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}` | signed-in |
|
|
|
| GET | `/sprints/{id}/present` | signed-in |
|
|
| GET | `/sprints/{id}/present` | signed-in |
|
|
|
| GET | `/sprints/{id}/settings` | admin |
|
|
| GET | `/sprints/{id}/settings` | admin |
|
|
@@ -846,6 +861,81 @@ with a `BOOTSTRAP_ADMIN` audit row.
|
|
|
`.assign-status-*` safelist + status-select styling all carry
|
|
`.assign-status-*` safelist + status-select styling all carry
|
|
|
over unchanged.
|
|
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
|
|
### Upcoming
|
|
|
|
|
|
|
|
Nothing scheduled.
|
|
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:
|
|
Run the test suite:
|
|
|
```bash
|
|
```bash
|
|
|
vendor/bin/phpunit
|
|
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
|
|
## 12. How to resume in a fresh Claude session
|
|
@@ -905,13 +1008,15 @@ vendor/bin/phpunit
|
|
|
Tell Claude:
|
|
Tell Claude:
|
|
|
|
|
|
|
|
> Working on `/Users/achiappa/Development/claude_code_private/sprint_planer_web`.
|
|
> 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 —
|
|
> are shipped (see §9; Phase 17's slider popover was removed —
|
|
|
> typed entry is now the only edit path on number inputs; Phase 18
|
|
> typed entry is now the only edit path on number inputs; Phase 18
|
|
|
> added per-cell task-status colours + filter + a new /settings page
|
|
> 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
|
|
> 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
|
|
> 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
|
|
> scheduled. Outstanding items are in §10 (mostly a human-run
|
|
|
> acceptance walkthrough in the running container). If I ask you to
|
|
> acceptance walkthrough in the running container). If I ask you to
|
|
|
> plan or work a new phase, follow the maintenance rule in §14 —
|
|
> 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)
|
|
## 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
|
|
2813019 Sprint view: tabs (Arbeitstage / Tasks) + smart Close on present
|
|
|
10ea4b8 Cell popover: replace per-cell status select with slider + status pills
|
|
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
|
|
1864835 Fix: filter dropdown close — grace timer for transit gap + close on Clear
|