initial checkin

This commit is contained in:
2026-09-29 09:40:32 +00:00
commit 50b7ccd65f
34 changed files with 11874 additions and 0 deletions
+627
View File
@@ -0,0 +1,627 @@
---
goal: Production-Ready Rewrite of 24HRS Messe-Lotse (Pimcore + React)
version: 1.0
date_created: 2026-06-09
last_updated: 2026-06-09
owner: 24HRS Team
status: Planned
tags: architecture, rewrite, feature, pimcore, react
---
# Introduction
![Status: Planned](https://img.shields.io/badge/status-Planned-blue)
Full production rewrite of the 6,200-line monolithic `index.html` trade fair project management tool ("24HRS Messe-Lotse") into a multi-user web application backed by Pimcore (already in place) with a React + TypeScript frontend.
**Architecture:** React SPA served by Pimcore domain → Pimcore REST API (OAuth2) → MySQL + Gotenberg (PDF)
**Data Model (4 entities):**
```
fairLocation ──1→n── fair ──1→n── job ──n→1── customer
(where) (when) (what) (who)
```
---
## 1. Requirements & Constraints
- **REQ-001**: Multi-user with OAuth2 authentication (Pimcore password grant)
- **REQ-002**: Four roles with scoped visibility — **Admin** (all data, config, user management), **Editor** (all jobs across all customers, resource pool, workflow), **Viewer** (read-only across all jobs), **Customer** (restricted to jobs linked to their `customer` record; sees 4 of 8 tabs; cannot create/delete jobs or manage resources)
- **REQ-002a**: Customer role access rules — editable: Tab 0 booth details (standType/size/height/hall/area/image/notes), Tab 1 briefing documents, Tab 4 checklist check/uncheck, Tab 5+6 stand photos + acceptance; read-only: fair/fairLocation info, workflow state, inherited dates; hidden: Tabs 2 Organisations, Tab 3 Produktion, Tab 7 Weekly; actions: save own editable fields, export PDF of own jobs, no delete/duplicate/create/transition
- **REQ-003**: All file uploads stored in Pimcore DAM (not base64 in localStorage)
- **REQ-004**: PDF export via Gotenberg (already running)
- **REQ-005**: Frontend served from Pimcore domain (no CORS, session cookie reuse)
- **REQ-006**: Feature parity with existing `index.html` — all 8 tabs + dashboard + calendar
- **REQ-007**: Symfony Workflow-driven phase progression on `job` entity — 8-state state machine (Angebot → Planung → Organisation → Produktion → Aufbau → Messe → Abbau → Abgeschlossen) with transition guards
- **REQ-008**: Normalized 4-entity data model — `fairLocation` (venue), `fair` (trade fair event), `job` (booth project), `customer` (company); one `fair` contains many `job`s, one `customer` can have multiple `job`s at the same `fair`
- **REQ-009**: Date inheritance — `job` inherits `setupStart/End`, `eventStart/End`, `teardownStart/End` from its parent `fair`, with per-`job` override capability
- **REQ-010**: "Fair Detail" view — the primary "todo" grouping: open a `fair` → see all its `job`s across all `customer`s, combined calendar, shared deadlines, bulk PDF export
- **REQ-011**: Resource pool management — 11 entities (`WorkerRole`, `Worker`, `PoolVehicle`, `Equipment`, `Accommodation`, `WorkerDemand`, `WorkerAssignment`, `PoolVehicleAssignment`, `EquipmentAssignment`, `AccommodationAssignment`, `Travel`); plan resource needs via `WorkerDemand` (role + quantity), fill with `WorkerAssignment` (specific worker + dates); fair-level resource view with gap analysis (needed − assigned); worker-level travel management (one-way or round-trip per fair); replaces all per-job ad-hoc team/vehicle/hotel lists
- **REQ-012**: AI document processing skills — pluggable AI skills triggered on asset upload to Pimcore DAM; extract information from uploaded documents, compare with stored job data, surface suggestions for auto-fill, validation warnings, and data enrichment; skills include: document classifier, contact extractor, deadline detector, stand data extractor, confirmation parser, checklist generator, brand consistency checker, material list extractor, acceptance report generator, setup progress analyzer, weekly protocol summarizer; all AI suggestions stored as non-destructive metadata (user accepts/ignores); phased rollout Tiers 1→2→3
- **REQ-013**: Tiered document storage — documents stored at the entity level matching their scope (customer/fair/job), not only on job. `customer.brandDoc`, `consultationDocs`, `contractDocs` for company-wide documents. `fair.constructionBriefing`, `floorPlan`, `regulations`, `protocols` for fair-wide documents. Job retains booth-specific documents (confirmations, photos, acceptance, signature). AI `DocumentScopeDetector` identifies scope on upload and suggests relocation if mismatched. Job detail shows merged view of all three levels.
- **CON-001**: Must run in existing Docker Compose environment
- **CON-002**: No legacy data migration needed (greenfield)
- **CON-003**: Pimcore ≥ 11.x, Gotenberg, and OAuth2 already in place
- **SEC-001**: Booking platform passwords stored encrypted (Pimcore encrypted field type)
- **SEC-002**: All API routes protected by OAuth2, role-checked per endpoint
- **GUD-001**: React 18 + TypeScript + Vite + TanStack Query + Zustand + shadcn/ui
---
## 2. Implementation Steps
### Implementation Phase 1 — Pimcore Bundle & Data Model
- GOAL-001: All backend capabilities (4 entities, relations, API, services) exist before any frontend work begins.
#### 1.1 Data Object Classes
| Task | Description |
|------|-------------|
| TASK-001 | Scaffold `MessebauBundle` in `bundles/MessebauBundle/` with Symfony bundle class, `services.yaml`, `routes.yaml` |
| TASK-002 | Create Data Object `FairLocation` (venue) — text fields: name, address, googleMapsUrl; contact fields: contactName, contactPhone, contactEmail; textarea: notes |
| TASK-003 | Create Data Object `Customer` (company) — text fields: company (name), address; contact fields: contactName, contactPhone, contactEmail; asset fields: brandDoc (document, company-wide brandbook/CI guidelines), consultationDocs (document gallery, multi-fair planning/consultation docs), contractDocs (document gallery, framework agreements); textarea: notes |
| TASK-004 | Create Data Object `Fair` (trade fair event) — text fields: name, hallsUsed; relation fields: fairLocation (1→1 to `FairLocation`); datetime fields: setupStart, setupEnd, eventStart, eventEnd, teardownStart, teardownEnd; asset fields: constructionBriefing (document gallery, multi-booth briefings like CFC 2026), floorPlan (document gallery, hall-level plans + venue maps), regulations (document gallery, venue regs + fire safety), protocols (document gallery, fair-wide weekly protocols); textarea: notes |
| TASK-005 | Create Data Object `Job` (booth project) — the core entity, replaces old `MessebauJob`. **Location fields**: standType (select: Systemstand/Reihstand/Eckstand/Kopfstand/Blockstand/Inselstand), standSize, standHeight, hall (e.g. "Hall 3 / Booth C12"), area, project. **Workflow field**: workflowState (input, managed by Symfony Workflow). **Relation fields**: customer (n→1 to `Customer`), fair (n→1 to `Fair`). **Date overrides** (nullable; if null, inherit from `fair`): setupStartOverride, setupEndOverride, eventStartOverride, eventEndOverride, teardownStartOverride, teardownEndOverride. **Notes**: textarea |
| TASK-006 | Create field collections for `Job`: `BookingPlatformInfo` (portalLink, email, password[encrypted], deadline), `PowerInfo` (kw, distributor, deadline), `WlanInfo` (type, deadline), `CleaningInfo` (deadline), `RiggingInfo` (supplierFirstname, supplierLastname, supplierEmail, supplierPhone, messeFirstname, messeLastname, messeEmail, messePhone, deadline), `DruckproduktionInfo` (qm, blockout, forexplatten, deadline), `CateringInfo` (baristaEnabled, baristaPackage, days, drinksEnabled, drinksDetails, foodEnabled, foodDetails), `CostEstimate` (standConstruction, activation, furniture, logistics — all integer, currency EUR; used by BudgetExtractor AI skill) |
| TASK-007 | Create related Data Objects (all with 1:n relation to `Job`): `Material` (type[flooring/standsystem/hardware/furniture], name, quantity, notes), `ChecklistItem` (text, checked, order). Note: per-job team, vehicles, and hotels are replaced by the resource pool (see 1.4) |
| TASK-008 | Add asset/image/document fields to `Job`: standImage (image), standGallery (image gallery), briefingCustomerDoc (document, optional: per-job override; if null, shows `customer.consultationDocs`), briefingPlanDoc (document, optional: per-job override; if null, shows `fair.floorPlan`), briefingAdditionalDoc (document), powerConfirmation (document), wlanConfirmation (document), cleaningConfirmation (document), riggingQuote (document), riggingDrawing (document), riggingMesseConfirmation (document), standApprovalDoc (document), acceptancePhotos (image gallery), acceptanceSignature (image), weeklyProtocols (document gallery). Note: `briefingBrandDoc` moved to `customer.brandDoc` — brandbook is company-wide, not per-job |
#### 1.2 Workflow
| Task | Description |
|------|-------------|
| TASK-009 | Create Symfony Workflow config in `bundles/MessebauBundle/Resources/config/workflows/job.yml` — 8 places (angebot, planung, organisation, produktion, aufbau, messe, abbau, abgeschlossen); initial marking: `angebot`; transitions with guards: `angebot→planung` requires `customer` and `fair` relations set, `planung→organisation` requires briefingCustomerDoc uploaded, `organisation→produktion` requires power/wlan/cleaning deadlines set if their checkboxes enabled, `produktion→aufbau` requires all checklist items checked, `abbau→abgeschlossen` requires acceptanceSignature+acceptanceDate set; date-based transitions (aufbau→messe, messe→abbau) auto-fire via cron checking effective dates (override or inherited from `fair`) |
| TASK-010 | Create `WorkflowController` — `GET /api/messebau/jobs/{id}/workflow` returns current place, available transitions (with guard status: allowed/blocked/reason), transition history; `POST /api/messebau/jobs/{id}/workflow/{transition}` applies a manual transition |
| TASK-011 | Create `WorkflowTransitionListener` — on `workflow.enter`: log timestamp to job's transitionHistory (JSON array); on `workflow.enter.aufbau`: auto-generate PDF export for setup team; on `workflow.enter.messe`: send customer email with booth details; on `workflow.enter.abgeschlossen`: mark job as read-only |
#### 1.3 API Controllers
| Task | Description |
|------|-------------|
| TASK-012 | Create `FairLocationController` — `GET /api/messebau/fairlocations` (list), `GET/POST/PUT/DELETE /api/messebau/fairlocations/{id}` |
| TASK-013 | Create `CustomerController` — `GET /api/messebau/customers` (list, searchable by company name), `GET/POST/PUT/DELETE /api/messebau/customers/{id}`; `GET /api/messebau/customers/{id}/jobs` lists all jobs for this customer |
| TASK-014 | Create `FairController` — `GET /api/messebau/fairs` (list, filterable by fairLocation, date range), `GET/POST/PUT/DELETE /api/messebau/fairs/{id}`; `GET /api/messebau/fairs/{id}/jobs` lists all jobs for this fair (the "todo" view); fair serialization includes fairLocation name inline |
| TASK-015 | Create `JobController` — `GET /api/messebau/jobs` (paginated, filterable by workflowState/fair/customer/search), `GET /api/messebau/jobs/{id}` (full serialization with relations + asset URLs + resolved effectiveDates), `POST /api/messebau/jobs` (create — requires `customer` and `fair` relation IDs, initializes default checklist items), `PUT /api/messebau/jobs/{id}` (full update), `PATCH /api/messebau/jobs/{id}` (partial update), `DELETE /api/messebau/jobs/{id}` |
| TASK-016 | Create `AssetController` — `POST /api/messebau/jobs/{id}/upload/{fieldName}` (validate file type+size, create Pimcore Asset, link to field), `DELETE /api/messebau/jobs/{id}/asset/{fieldName}`. Also handles `customer` and `fair` entity uploads (brandDoc, consultationDocs, contractDocs, constructionBriefing, floorPlan, regulations, protocols) via generalized endpoint `POST /api/messebau/{entity}/{id}/upload/{fieldName}` |
| TASK-017 | Create `CalendarController` — `GET /api/messebau/calendar?year=&month=&fairId=` — aggregate effective dates (override or inherited from `fair`) plus all deadline fields across jobs; return flat event list with type+color+jobId+fairId; filterable by `fairId` |
| TASK-018 | Create `JobDuplicateService` — deep clone `Job` data object (reset workflowState to angebot, clear acceptance data), copy all related objects (materials, checklist), copy all assets in DAM, append "(Kopie)" to project field, re-link to same `customer` and `fair`, generate new ID |
| TASK-019 | Create `JobExportService` — accepts job ID, loads full data with joined `customer`/`fair`/`fairLocation`, resolves effective dates, renders Twig template (`templates/messebau/pdf/export.html.twig`), POSTs HTML to Gotenberg, returns PDF binary with filename `Jobzettel-{customer.company}-{fair.name}.pdf` |
| TASK-020 | Create `JobNotificationListener` — listen to `dataobject.postUpdate`, check changed deadline fields, dispatch async messages for email reminders at T-7, T-3, T-1 days |
| TASK-021 | Create `FairExportService` — accepts fair ID, loads all jobs, renders aggregated Twig template (all jobs for that fair), POSTs to Gotenberg, returns combined PDF. Endpoint: `GET /api/messebau/fairs/{id}/export-pdf` |
| TASK-022 | Configure OAuth2 firewall — protect `/api/messebau/*` with `pimcore.api` firewall |
| TASK-023 | Define roles and scoping — `ROLE_MESSEBAU_CUSTOMER` (own jobs only, limited fields, 4/8 tabs, no delete/duplicate/create/transition), `ROLE_MESSEBAU_VIEWER` (GET only across all jobs), `ROLE_MESSEBAU_EDITOR` (CRUD across all jobs + resource pool), `ROLE_MESSEBAU_ADMIN` (full access + user management + delete). Enforce in controller method annotations. Add API scoping base class: when `ROLE_MESSEBAU_CUSTOMER`, read `user.messebauCustomer` relation and auto-filter all `job` queries by `customer = user.messebauCustomer`; 403 on cross-company access; 403 on all resource/pool endpoints; fair/fairLocation read-only GET |
| TASK-023a | Add `messebauCustomer` field to Pimcore User class (n→1 relation to `Customer` DO, nullable). Admin sets this during user creation to link a login to a customer company. Customer users without this relation get 403 on all endpoints. |
| TASK-024 | Write PHPUnit tests: `FairLocationControllerTest`, `CustomerControllerTest`, `FairControllerTest`, `JobControllerTest` (including customer scoping: verify 403 on other company's jobs, fields limited per role), `JobDuplicateServiceTest` (clone integrity with preserved relations), `JobExportServiceTest`, `FairExportServiceTest`, `CalendarControllerTest`, `WorkflowTest`, `WorkflowTransitionListenerTest` |
| TASK-025 | Register all API routes in `bundles/MessebauBundle/Resources/config/routes.yaml` with `messebau_api` prefix |
#### 1.4 Resource Pool Entities
| Task | Description |
|------|-------------|
| TASK-026 | Create Data Object `WorkerRole` (role category) — text field: name (e.g. "Barista", "Rigger", "LorryDriver", "FairWorker", "Electrician", "Cleaner") |
| TASK-027 | Create Data Object `Worker` (person pool) — text fields: firstName, lastName, email, phone; relation: role (n→1 to `WorkerRole`); select field: state (internal/hired/toHire); textarea: notes |
| TASK-028 | Create Data Object `PoolVehicle` (truck pool) — text fields: name, type, licensePlate; select field: state (owned/hired/toHire); textarea: notes |
| TASK-029 | Create Data Object `Equipment` (special items pool) — text fields: name, description; select field: state (owned/rented/toRent); textarea: notes |
| TASK-030 | Create Data Object `WorkerDemand` (planning: "we need X people of this role") — relation fields: fair→Fair (required), job→Job (nullable), workerRole→WorkerRole (required); integer field: quantity; textarea: notes |
| TASK-031 | Create Data Object `WorkerAssignment` (execution: "Anna fills that slot") — relation fields: fair→Fair (required), job→Job (nullable), worker→Worker (required); datetime fields: startDate, endDate; textarea: notes |
| TASK-032 | Create Data Object `PoolVehicleAssignment` — relation fields: fair→Fair (required), job→Job (nullable), poolVehicle→PoolVehicle (required); datetime fields: startDate, endDate; textarea: notes |
| TASK-033 | Create Data Object `EquipmentAssignment` — relation fields: fair→Fair (required), job→Job (nullable), equipment→Equipment (required); datetime fields: startDate, endDate; textarea: notes |
| TASK-033a | Create Data Object `Accommodation` (hotel/apartment pool) — text fields: name, address, bookingReference; select field: type (hotel/apartment/other); textarea: notes |
| TASK-033b | Create Data Object `AccommodationAssignment` — relation fields: fair→Fair (required), accommodation→Accommodation (required), worker→Worker (nullable); date fields: checkIn, checkOut; textarea: notes. Multiple workers can share the same accommodation at a fair |
| TASK-033c | Create Data Object `Travel` (per worker per fair) — relation fields: worker→Worker (required), fair→Fair (required); select field: type (oneWay/roundTrip); text fields: origin, destination, transportMethod, bookingReference; datetime fields: departureDateTime, arrivalDateTime; datetime fields: returnDepartureDateTime (nullable), returnArrivalDateTime (nullable); textarea: notes |
| TASK-034 | Create `WorkerRoleController` — `GET /api/messebau/workerroles` (list), `GET/POST/PUT/DELETE /api/messebau/workerroles/{id}` |
| TASK-035 | Create `WorkerController` — `GET /api/messebau/workers` (list, filterable by `?roleId=&state=`), `GET/POST/PUT/DELETE /api/messebau/workers/{id}` |
| TASK-036 | Create `PoolVehicleController` — `GET /api/messebau/poolvehicles` (list, filterable by `?state=`), `GET/POST/PUT/DELETE /api/messebau/poolvehicles/{id}` |
| TASK-037 | Create `EquipmentController` — `GET /api/messebau/equipment` (list, filterable by `?state=`), `GET/POST/PUT/DELETE /api/messebau/equipment/{id}` |
| TASK-038 | Create `FairResourceController` — `GET /api/messebau/fairs/{id}/resources` returns all demands+assignments grouped by type with gap computed (needed − assigned); `POST/PUT/DELETE /api/messebau/fairs/{id}/demands` manages demand slots; `POST/PUT/DELETE /api/messebau/fairs/{id}/assignments` manages worker/poolVehicle/equipment assignments |
| TASK-039 | Create `JobResourceController` — `GET /api/messebau/jobs/{id}/resources` returns demands+assignments scoped to this job |
| TASK-039a | Create `AccommodationController` — `GET /api/messebau/accommodations` (list), `GET/POST/PUT/DELETE /api/messebau/accommodations/{id}` |
| TASK-039b | Create `FairAccommodationController` — `GET /api/messebau/fairs/{id}/accommodations` returns accommodations + assignments + occupancy for this fair; `POST/PUT/DELETE /api/messebau/fairs/{id}/accommodations/{accId}/assign` manages check-in/check-out |
| TASK-039c | Create `WorkerTravelController` — `GET /api/messebau/workers/{id}/travel` lists all travel for this worker (filterable by `?fairId=`); `POST/PUT/DELETE /api/messebau/workers/{id}/travel` manages travel records |
| TASK-040 | Write PHPUnit tests: `WorkerRoleControllerTest`, `WorkerControllerTest`, `PoolVehicleControllerTest`, `EquipmentControllerTest`, `AccommodationControllerTest`, `FairAccommodationControllerTest`, `WorkerTravelControllerTest`, `FairResourceControllerTest` (demand fill calculation, gap computation, assignment scoping), `JobResourceControllerTest` |
### Workflow State Machine Diagram
```
┌──────────┐ Progress Color
│ Angebot │ ← Job created 12% gray
└────┬─────┘
│ Guard: customer + fair relations set
┌────▼─────┐
│ Planung │ ← Briefing uploads 25% blue
└────┬─────┘
│ Guard: briefingCustomerDoc uploaded
┌────▼─────┐
│ Organisation│ ← Services booked 37% teal
└────┬─────┘
│ Guard: power/wlan/cleaning deadlines set (if enabled)
┌────▼─────┐
│ Produktion│ ← Materials+printing 50% indigo
└────┬─────┘
│ Guard: all checklist items checked
┌────▼─────┐
│ Aufbau │ ← effective setup dates 62% orange
└────┬─────┘
│ Auto: effective setupEnd reached (cron)
┌────▼─────┐
│ Messe │ ← effective event dates 75% yellow
└────┬─────┘
│ Auto: effective eventEnd reached (cron)
┌────▼─────┐
│ Abbau │ ← effective teardown dates 87% red
└────┬─────┘
│ Guard: acceptanceSignature + acceptanceDate set
┌────▼─────┐
│ Abgeschl. │ ← Archive, read-only 100% green
└──────────┘
```
**Effective dates**: Each `job` resolves dates as `job.xxxOverride ?? fair.xxx`. The workflow, calendar, and PDF export all use resolved effective dates.
**Transition types:**
- **Manual** (user clicks button in UI): angebot→planung, planung→organisation, organisation→produktion, produktion→aufbau, abbau→abgeschlossen
- **Automatic** (cron checks effective dates): aufbau→messe (when effective setupEnd < now), messe→abbau (when effective eventEnd < now)
- **Optional rollback**: Abgeschlossen jobs can be reopened to Abbau by admin only
**Transition history** stored as JSON array on the job object: `[{from: "planung", to: "organisation", timestamp: "2026-06-09T14:30:00Z", user: "max.mustermann"}]`
### Implementation Phase 2 — React Skeleton & Auth
- GOAL-002: Authenticated React app served from Pimcore domain with routing in place.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-016 | Scaffold Vite + React + TypeScript project in `frontend/` with Tailwind CSS v3 and shadcn/ui initialized | | |
| TASK-017 | Configure `vite.config.ts` — build output to `../public/build/messebau/`, base path `/build/messebau/`, proxy `/api` to Pimcore in dev mode | | |
| TASK-018 | Create `src/api/client.ts` — Axios instance with `baseURL: '/api/messebau'`, request interceptor to attach `Authorization: Bearer {accessToken}`, response interceptor for 401 → attempt refresh token → retry original request; implement refresh token queue to prevent concurrent refresh race conditions | | |
| TASK-019 | Create `src/stores/authStore.ts` — Zustand store: `accessToken`, `refreshToken`, `user` (id, username, roles), `login(username, password)` (POST to `/oauth2/token` with password grant), `logout()`, `refreshAccessToken()`, persist tokens to sessionStorage (not localStorage for security) | | |
| TASK-020 | Create `src/stores/uiStore.ts` — Zustand store: `currentView` (dashboard/calendar/detail), `currentJobId`, `toastQueue[]`, `sidebarCollapsed` | | |
| TASK-021 | Create `src/components/auth/LoginPage.tsx` — username + password form, calls `authStore.login()`, error display, redirects to dashboard on success; route at `/login` | | |
| TASK-022 | Create `src/components/auth/ProtectedRoute.tsx` — checks auth store for token, redirects to `/login` if missing, renders `<Outlet />` if authenticated | | |
| TASK-023 | Create `src/components/layout/AppShell.tsx` — flex layout: `<Sidebar>` (260px fixed) + `<main>` (flex-1, 32px padding); wraps all authenticated routes | | |
| TASK-024 | Create `src/components/layout/Sidebar.tsx` — "24HRS" logo + "Messe-Lotse" subtitle (yellow on black); nav items (role-dependent): Dashboard, Kalender always shown; "Ressourcen" (submenu: Worker Roles, Worker Pool, Truck Pool, Equipment Pool, Accommodations) hidden for customer; "Admin" hidden for customer/viewer; active state via `useLocation()`; logout button at bottom | | |
| TASK-024a | Create `src/hooks/usePermissions.ts` — reads role from `authStore.user.roles`; returns boolean flags: `canEdit`, `canDelete`, `canCreateJob`, `canDuplicateJob`, `canTransitionWorkflow`, `canManageResources`, `canSeeOrgTab`, `canSeeProduktionTab`, `canSeeWeeklyTab`, `canSeeCustomerTab`, `canAddChecklistItems`, `isCustomer`; all components import this hook to conditionally render UI elements | | |
| TASK-025 | Set up `react-router-dom` in `src/App.tsx` — routes: `/login` → LoginPage, `/` → ProtectedRoute → AppShell → Dashboard, `/fair/:id` → FairDetail (the "todo" view), `/customer/:id` → CustomerDetail, `/jobs/:id` → JobDetail, `/calendar` → Calendar, `/calendar?fairId=:id` → Calendar scoped to fair; 404 catch-all | | |
| TASK-026 | Create Pimcore controller `MessebauController::appAction()` — serves `public/build/messebau/index.html` with Twig; configure route `/messebau/{react}` with `react` as optional catch-all parameter for SPA client-side routing | | |
| TASK-027 | Write Vitest tests: `authStore.test.ts` (login, logout, token refresh flow), `client.test.ts` (token attachment, 401 refresh, request retry) | | |
### Implementation Phase 3 — Dashboard, Hierarchy & Job CRUD
- GOAL-003: Dashboard with hierarchical navigation (fair → jobs), full job lifecycle, and entity creation flow.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-028 | Create `src/types/job.ts` — TypeScript interfaces: `FairLocation`, `Customer`, `Fair`, `Job` (with `effectiveDates` resolved object), `Vehicle`, `Hotel`, `TeamMember`, `Material`, `ChecklistItem`, `BookingPlatformInfo`, `PowerInfo`, `WlanInfo`, `CleaningInfo`, `RiggingInfo`, `DruckproduktionInfo`, `CateringInfo`, `Acceptance`, `Briefing`, `LeadMagnets`, `JobStats`, `WorkflowState` (place, availableTransitions, history) | | |
| TASK-029 | Create `src/api/fairLocations.ts` + `src/api/customers.ts` + `src/api/fairs.ts` + `src/api/jobs.ts` — typed API functions for all 4 entity CRUD operations; jobs API supports filters `?fairId=&customerId=&workflowState=&search=` | | |
| TASK-030 | Create `src/hooks/useFairs.ts` + `src/hooks/useJobs.ts` + `src/hooks/useJob.ts` — TanStack Query hooks; `useFairs` is the primary list on the dashboard; `useJobs(fairId?)` for fair-detail view | | |
| TASK-031 | Create `src/components/dashboard/StatsGrid.tsx` — stat cards: Gesamt (total jobs), Laufende Messen (fairs with active jobs), Diese Woche (jobs in setup/messe/teardown this week) | | |
| TASK-032 | Create `src/components/dashboard/FairCard.tsx` — card per fair showing fair name, fairLocation name, date range, job count, combined progress; clicking navigates to `/fair/{id}` (the "todo" view) | | |
| TASK-033 | Create `src/components/dashboard/FairCardGrid.tsx` — responsive grid of FairCards; search bar filters by fair name or fairLocation name | | |
| TASK-034 | Create `src/components/dashboard/EmptyState.tsx` — for internal roles: "Keine Messen vorhanden" + "Neue Messe anlegen" button; for customer role: "Keine Projekte vorhanden — 24HRS wird Ihre Messeaufträge hier bereitstellen" (no create button) | | |
| TASK-035 | Create `src/components/fair/FairDetailView.tsx` — fair header (name, location, dates), stats for this fair (total jobs, open jobs), action buttons (edit fair, bulk PDF export), then `<JobCardGrid>` filtered to this fair; "Neuen Job hinzufügen" button | | |
| TASK-036 | Create `src/components/job/CreateJobFlow.tsx` — wizard: Step 1: select or create `Customer` (combobox with search + "Neuen Kunden anlegen" option), Step 2: select or create `Fair` (combobox grouped by fairLocation + "Neue Messe anlegen" option), Step 3: enter booth details (project, standType, standSize, hall). After creation, navigate to job detail | | |
| TASK-037 | Create `src/components/job/JobCard.tsx` — card with stand image, customer.company, fair.name, hall, workflow progress bar (12%→100%) with phase label; `onClick` navigates to `/jobs/{id}` | | |
| TASK-038 | Create `src/components/job/JobCardGrid.tsx` — responsive grid, maps jobs to `<JobCard>` | | |
| TASK-039 | Create `src/components/job-detail/DetailHeader.tsx` — black bar: breadcrumb (fair.name → customer.company → job.project), back button, action buttons gated by `usePermissions`: save (all except viewer), duplicate (editor/admin only), export job PDF (all), delete (admin only); customer sees save + export only, no delete/duplicate/transition; delete shows confirm dialog | | |
| TASK-040 | Create `src/components/job-detail/TabContainer.tsx` — dynamic tab list based on role: internal sees 8 tabs; customer sees 4 tabs (Tab 0 Stammdaten, Tab 1 Briefing, Tab 4 Dokumente, Tab 5+6 Standfotos+Standabnahme); tabs 2/3/7 hidden for customer; tab visibility controlled by `usePermissions` hook; active tab stored in search params (`?tab=0`) | | |
| TASK-041 | Implement save — `updateJob()` mutation collects all form data, shows toast | | |
| TASK-042 | Implement delete — confirm dialog, `deleteJob()` mutation, navigate to parent fair detail | | |
| TASK-043 | Implement duplicate — `duplicateJob()` mutation, creates new job linked to same `customer` and `fair`, navigate to new job | | |
| TASK-044 | Write Playwright test: `create-fair-and-jobs.spec.ts` — create fairLocation → create customer → create fair → create two jobs for different customers at same fair → verify both appear in fair detail | | |
| TASK-045 | Write Playwright test: `duplicate-delete.spec.ts` — open job, duplicate, verify "(Kopie)" in project, navigate to fair detail, delete original, verify gone | | |
### Implementation Phase 4 — Tab 0: Master Data & Scheduling
- GOAL-004: Stammdaten & Termine tab with relation display, date inheritance, and stand image upload.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-046 | Create `src/components/job-detail/tabs/MasterDataTab.tsx` — two-column form grid using `react-hook-form` + `zod` validation | | |
| TASK-047 | Implement entity selectors section — **Customer** + **Fair** + **FairLocation** shown read-only (for customer role: fixed display, no "Ändern" button; for internal: editable combo boxes); all roles see fair/fairLocation info + Google Maps link | | |
| TASK-048 | Implement Stammdaten section: Projekt (text, editable by all), Standart (select, editable by all), Standgröße (text, editable by all), Standhöhe (number, editable by all), Halle/Stand (text, editable by all), Standfläche m² (number, editable by all) | | |
| TASK-049 | Implement inherited Zeitplanung section: show `fair`'s dates as read-only reference values; for internal roles: per-job override fields (nullable datetime-local); for customer role: no overrides, only read-only display of effective dates | | |
| TASK-050 | Implement workflow state section — read-only badge showing current phase with color; for internal roles: transition buttons (grayed out if guard blocks, tooltip explains why blocked); for customer role: no transition buttons | | |
| TASK-051 | Implement Messestand-Bild section: file input, image preview, download/remove buttons | | |
| TASK-052 | Implement Notizen textarea (full-width, min-height 100px) | | |
| TASK-053 | Write Playwright test: `masterdata-tab.spec.ts` — verify customer/fair/fairLocation display correctly; override setup date, save, reload, verify override persisted; verify inherited dates shown when override cleared | | |
### Implementation Phase 5 — Tab 1: Briefing (Merged View)
- GOAL-005: Briefing tab shows documents from all three scope levels (customer → fair → job), with scope badges and inline AI scope detection.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-051 | Create `src/components/job-detail/tabs/BriefingTab.tsx` — 3 collapsible sections with scope badges: "☰ Kunde", "🌐 Messe", "📋 Job"; each section shows relevant documents from that entity level, merged into a single view | | |
| TASK-052 | Implement **Customer Documents** section — shows `customer.brandDoc` (with download/delete), `customer.consultationDocs` gallery, `customer.contractDocs` gallery; each with [Add Document] button (uploads to customer entity, not job); AI scope badge if detected scope differs from current section | | |
| TASK-053 | Implement **Fair Documents** section — shows `fair.constructionBriefing` gallery, `fair.floorPlan` gallery, `fair.regulations` gallery, `fair.protocols` gallery; each with [Add Document] button (uploads to fair entity); multi-booth briefings are visible to all jobs of this fair | | |
| TASK-054 | Implement **Job Documents** section — shows `job.briefingCustomerDoc` (override, with "inherited from customer" indicator if null), `job.briefingPlanDoc` (override, with "inherited from fair" indicator if null), `job.briefingAdditionalDoc`; [Upload] buttons for job-level overrides | | |
| TASK-055 | Implement scope notification on upload — when a job-level briefing upload is classified as multi-fair or multi-booth by `DocumentScopeDetector`, show: "⚠️ This document references X fairs / Y booths. Move to customer/fair level?" with [Move to customer] | [Move to fair] | [Keep here] | | |
| TASK-056 | Add info banner at bottom — "Hinweis: Dokumente können auf Kunden-, Messe- oder Job-Ebene gespeichert werden. Scoped-AI erkennt automatisch die richtige Ebene." | | |
| TASK-057 | Write Playwright test: `briefing-tab.spec.ts` — upload brandbook to customer level → verify visible in all jobs for that customer; upload multi-booth briefing to a job → verify scope mismatch detected → move to fair → verify all jobs now see document; upload job-specific briefing → verify shows only on that job | | |
### Implementation Phase 6 — Tab 2: Organisation
- GOAL-006: Organisation tab with booking platform, services, rigging, hotels, and resource assignment summary (replaces old per-job ad-hoc team/vehicle lists — now managed via the resource pool at fair level).
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-058 | Create `src/components/job-detail/tabs/OrganisationTab.tsx` — multiple collapsible/section blocks | | |
| TASK-059 | Implement Buchungsplattform section — checkbox "Buchungsplattform vorhanden" toggles fields: Portal-Link (url), E-Mail (email), Passwort (password), Bestelldeadline (date) | | |
| TASK-060 | Implement Strombuchung section — checkbox "Strom gebucht" toggles: Leistung kW (number), Stromverteiler (text), Bestelldeadline (date), Buchungsbestätigung PDF upload+preview | | |
| TASK-061 | Implement WLAN section — checkbox "WLAN gebucht" toggles: Art (text), Bestelldeadline (date), Buchungsbestätigung PDF upload+preview | | |
| TASK-062 | Implement Reinigungsservice section — checkbox "Reinigungsservice gebucht" toggles: Bestelldeadline (date), Buchungsbestätigung PDF upload+preview | | |
| TASK-063 | Implement Rigging/Truss section — checkbox "Rigging/Truss vorhanden" toggles: Lieferant Kontaktdaten (Vorname, Nachname, E-Mail, Telefon), Bestelldeadline (date), Dokumente (Angebot PDF, Technische Zeichnung PDF, Auftragsbestätigung Messe PDF — each with upload+preview), Messe Kontaktdaten (Vorname, Nachname, E-Mail, Telefon) | | |
| TASK-064 | Implement Standfreigabe section — checkbox "Standfreigabe vorhanden" toggles: Bestelldeadline (date), Freigabedokument PDF upload+preview | | |
| TASK-065 | Implement resource assignment summary — read-only display of workers/vehicles/equipment assigned to this job (fetched via `GET /api/messebau/jobs/{id}/resources`), grouped by type; each item shows: name, role/type, state badge, dates; "[Manage all resources for this fair →]" link to fair Resources tab | | |
| TASK-066 | Implement accommodation + travel summary — read-only display of accommodations assigned to workers at this fair (from `FairAccommodationController`) and travel records for workers on this job (from `WorkerTravelController`); "[Manage accommodation & travel →]" link to fair Resources tab | | |
### Implementation Phase 7 — Tab 3: Produktion
- GOAL-007: Production tab with materials, printing, catering, lead magnets.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-068 | Create `src/components/job-detail/tabs/ProduktionTab.tsx` | | |
| TASK-069 | Implement material lists — 4 sections: Bodenbeläge, Standsystem/Messebau-Elemente, Hardware/Technik, Möbel & Ausstattung; each with add button and list of items (name, quantity, notes); addMaterial(type) creates new Material in the list | | |
| TASK-070 | Implement Druckproduktion section — checkbox toggles: Quadratmeter m² (number), Blockout (text), Anzahl Forexplatten (number), Bestelldeadline (date) | | |
| TASK-071 | Implement Catering section — Barista-Service checkbox toggles: Service-Paket (select: 1 PAX 150/300, 2 PAX 150/300), Anzahl Messetage (number); Catering-Personal: displays workers assigned to this job with role=Barista (from `GET /api/messebau/jobs/{id}/resources`), with button to create `WorkerDemand` for missing roles; Getränke checkbox toggles textarea; Food checkbox toggles textarea | | |
| TASK-072 | Implement Leadmagneten section — 9 checkboxes in 2-column grid: Flyer/Broschüren, Giveaways, Tablet, Badge-Scanner, Gewinnspiel, Produkt-Demos, KI Fotobox, Vending Machine, Greif-Arm | | |
### Implementation Phase 8 — Tab 4, 5, 6, 7: Documents, Photos, Acceptance, Weekly
- GOAL-008: Remaining tabs — checklist, photo gallery, acceptance with signature, weekly protocols.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-073 | Create `src/components/job-detail/tabs/DokumenteTab.tsx` — checklist with 8 default items; each item: checkbox + label with strikethrough when checked; **role-gated**: internal roles can add/remove checklist items; customer role can only check/uncheck existing items (no add/delete via `usePermissions.canAddChecklistItems`); TanStack Query optimistic update on toggle | | |
| TASK-074 | Create `src/components/job-detail/tabs/StandfotosTab.tsx` — upload zone (dashed border, drag-and-drop support, accepts image/* multiple, max 10 files 2 MB each); gallery grid (thumbnails 200px height, object-fit cover); each item has download + delete buttons; click opens `<Lightbox>` for full-size view with prev/next navigation | | |
| TASK-075 | Create `src/components/shared/Lightbox.tsx` — portal-based (ReactDOM.createPortal), dark overlay, centered image, close button (top-right), prev/next arrow buttons, keyboard navigation (arrow keys, escape), click-outside-to-close, swipe gesture on mobile | | |
| TASK-076 | Create `src/components/job-detail/tabs/StandabnahmeTab.tsx` — acceptance photo gallery (same pattern as Standfotos but separate asset field), signature pad using `react-signature-canvas` (600x200 canvas, clear + save buttons, "Unterschrift gespeichert" indicator), remarks textarea, Abnahmedatum (datetime-local) | | |
| TASK-077 | Create `src/components/job-detail/tabs/WeeklyTab.tsx` — multi-file upload for PDFs (accept .pdf, multiple), list of uploaded protocols with filename, size, download button, delete button; empty state with PDF icon | | |
### Implementation Phase 9 — Calendar View
- GOAL-009: Month-view calendar with color-coded job events, fair-scoped filtering, effective date resolution.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-078 | Create `src/hooks/useCalendar.ts` — TanStack Query hook fetching `/api/messebau/calendar?year=&month=&fairId=` for current month/year; store current month/year/fairId in local state; if viewing from fair detail, fairId is set; from global calendar, fairId is null (show all) | | |
| TASK-079 | Create `src/components/calendar/CalendarToolbar.tsx` — prev/next month buttons, month-year heading, "Heute" button, fair selector dropdown (filter calendar to one fair), color legend (Aufbau=blue, Messe=yellow, Abbau=red, Deadline=orange) | | |
| TASK-080 | Create `src/components/calendar/CalendarGrid.tsx` — 7-column grid, day headers (Mo-So), correct day cells, leading/trailing days from adjacent months grayed out | | |
| TASK-081 | Create `src/components/calendar/CalendarDay.tsx` — day number (bold+today border if today), event pills: setup=#3498db, event=#FFE000+black text+bold, teardown=#e74c3c, deadline=#FF6B35+bold+border-left; each pill shows truncated text with customer+fair name, click navigates to `/jobs/{jobId}` | | |
| TASK-082 | Wire calendar events — each job produces up to 4 event types using effective dates (override or inherited from fair); events spanning multiple days render on each day in the span | | |
### Implementation Phase 10 — PDF Export
- GOAL-010: Server-side PDF generation via Gotenberg.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-083 | Add job PDF export endpoint — `GET /api/messebau/jobs/{id}/export-pdf` invokes `JobExportService`, returns PDF with `Content-Disposition: attachment` | | |
| TASK-083a | Add fair bulk PDF export — button on FairDetailView triggers `GET /api/messebau/fairs/{id}/export-pdf` via `FairExportService`, returns combined PDF of all jobs for that fair | | |
| TASK-084 | Create `templates/messebau/pdf/export.html.twig` — print-optimized HTML: 24HRS header, customer.company + fair.name + fairLocation.name, info grid (stand type, size, hall, effective dates), material tables, checklist with checkmarks, acceptance info, signature image; CSS with `@page { margin: 20mm }`, yellow accents, Inter font, `page-break-inside: avoid` | | |
| TASK-085 | Add "PDF Export" button handler in `DetailHeader.tsx` — calls API, receives blob, creates download link, triggers click, shows toast | | |
### Implementation Phase 10a — Fair Resources Tab & Worker Management
- GOAL-010a: Fair-level resource pool with demand planning, worker/vehicle/equipment/accommodation assignment, travel management, and fill-gap visualization.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-R01 | Create `src/types/resources.ts` — interfaces: `WorkerRole`, `Worker`, `PoolVehicle`, `Equipment`, `Accommodation`, `WorkerDemand`, `WorkerAssignment`, `PoolVehicleAssignment`, `EquipmentAssignment`, `AccommodationAssignment`, `Travel`, `FairResources` (all demands + assignments grouped by type with gap) | | |
| TASK-R02 | Create `src/api/workerroles.ts` + `src/api/workers.ts` + `src/api/poolvehicles.ts` + `src/api/equipment.ts` + `src/api/accommodations.ts` + `src/api/travel.ts` + `src/api/fairResources.ts` — typed API functions for all 11 resource entities | | |
| TASK-R03 | Create `src/hooks/useFairResources.ts` — TanStack Query hook fetching `GET /api/messebau/fairs/{id}/resources`, returns grouped resource view with gap calculation | | |
| TASK-R04 | Create `src/hooks/useJobResources.ts` — TanStack Query hook fetching `GET /api/messebau/jobs/{id}/resources` for job-scoped resource display | | |
| TASK-R05 | Create `src/components/fair/FairResourcesTab.tsx` — four grids: **Workers by Role** table (role, needed qty, assigned count, gap, assigned workers list with job labels, "Add Demand" / "Assign Worker" buttons); **Trucks** table (vehicle name, state badge, assigned to job/dates); **Equipment** table (item name, state badge, assigned to job/dates); **Accommodation** table (name, type badge, occupancy, assigned workers with check-in/out dates, "Assign Worker" button); add-demand modal, add-assignment modals per type | | |
| TASK-R06 | Create `src/components/resources/WorkerPoolView.tsx` — worker pool management page (reachable from sidebar): list all workers filterable by role+state, CRUD actions, "Add Worker" form; each worker row shows assigned fairs + travel count | | |
| TASK-R07 | Create `src/components/resources/PoolVehicleView.tsx` — truck pool management page, same pattern | | |
| TASK-R08 | Create `src/components/resources/EquipmentView.tsx` — equipment pool management page, same pattern | | |
| TASK-R08a | Create `src/components/resources/AccommodationPoolView.tsx` — accommodation pool management page, same pattern | | |
| TASK-R08b | Create `src/components/resources/WorkerDetailView.tsx` — worker detail page: shows worker info + role + state, travel records (list of fairs with outbound/return info, transport method, booking reference, "Add Travel" button, one-way toggle hides return fields), accommodation assignments, fair assignments | | |
| TASK-R09 | Update `Sidebar` — add "Ressourcen" nav item (collapsible sub-items: Worker Roles, Worker Pool, Truck Pool, Equipment Pool, Accommodations) | | |
| TASK-R10 | Write Playwright test: `fair-resources.spec.ts` — open fair detail → Resources tab → add demand "3 Baristas" → verify gap=3 → create worker Anna (Barista, internal) → assign Anna → verify assigned=1, gap=2 → assign Anna to a specific job → verify job Resources tab shows Anna | | |
| TASK-R11 | Write Playwright test: `accommodation-travel.spec.ts` — create accommodation "Hotel Central" → assign worker Anna (check-in 14.6., check-out 18.6.) → verify occupancy=1 → add travel for Anna: one-way, Berlin→Hannover, Bahn, 14.6. 08:00→10:30 → verify worker detail shows travel + accommodation | | |
### Implementation Phase 11 — Cross-Cutting & Polish
- GOAL-011: Production-quality UX, responsive design, PWA, CI/CD.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-086 | Install `sonner` for toast notifications; create `<Toaster>` in `App.tsx`; add `showToast(message, type)` utility in `uiStore` | | |
| TASK-086a | Create `src/components/shared/WorkflowProgress.tsx` — horizontal stepper showing all 8 phases with current/active/complete states, phase icon + label; below: "Nächster Schritt: [Phase]" button that triggers workflow transition via API; guard violations shown as tooltip with reason; transition history shown as expandable timeline | | |
| TASK-087 | Add loading skeletons — `<Skeleton>` components (shadcn/ui) for: stats cards, job cards grid, detail header, form fields, gallery items; shown during TanStack Query `isLoading` state | | |
| TASK-088 | Add error boundaries — `<ErrorBoundary>` wrapping each major route/view, shows error message + retry button calling `queryClient.invalidateQueries()` | | |
| TASK-089 | Responsive design — sidebar collapses to bottom tab bar or hamburger menu on screens < 768px; form grids stack to single column; calendar days reduce height; gallery grid reduces columns | | |
| TASK-090 | Install `vite-plugin-pwa` — configure service worker with `workbox` for static asset caching; add offline indicator component that shows when `navigator.onLine` is false; register PWA manifest with app name "24HRS Messe-Lotse", yellow+black theme color | | |
| TASK-091 | Create `frontend/Dockerfile` — multi-stage: Node.js build stage (`npm ci && npm run build`), nginx stage serving built assets (if serving separately) OR configure Vite build output to Pimcore's `public/build/messebau/` directory (if served by Pimcore) | | |
| TASK-092 | Create `.github/workflows/ci.yml` — on push/PR: checkout, setup Node 20, `npm ci`, `npm run lint` (ESLint), `npm run typecheck` (tsc --noEmit), `npm run test` (Vitest), `npm run test:e2e` (Playwright with Pimcore container as service), `npm run build`; on main branch push: build and deploy | | |
| TASK-093 | Run full manual QA — create job, fill all 8 tabs, upload files, save, reload, verify persistence; test calendar month navigation; test PDF export opens correctly; test duplicate creates independent copy; test delete removes all assets; test on mobile viewport; test with viewer role (cannot edit); test with editor role (can edit but not delete); test with admin role (full access) | | |
### Implementation Phase 12 — AI Document Processing Skills
- GOAL-012: Pluggable AI skills triggered on asset upload to Pimcore DAM that extract, validate, and enrich job data from uploaded documents. All suggestions are non-destructive metadata — the user decides to apply or ignore. Skills are **field-aware**: each upload field maps to a specific skill subset via a routing matrix (see `plan/ai-document-skills.md` § "Field → Skill Routing Matrix"). The common chain (Classifier → MismatchDetector → VersionDetector) runs on every upload; the field-specific skill chain runs only for fields with applicable skills.
**Architecture:** `asset.postAdd` → `DocumentClassifier` (identifies document type) → type-specific extractor chain → `SuggestionService` (stores as JSON on job) → frontend renders inline suggestions.
#### Phase 12a — Infrastructure & Tier 1 (High Impact)
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-AI01 | Create `SuggestionService` in `bundles/MessebauBundle/Service/` — stores AI suggestions as a JSON field `aiSuggestions` on `Job` DO (array of `{id, skill, field, suggestedValue, confidence, sourceAssetId, status(pending/applied/ignored), createdAt}`); provides methods to add, apply (writes to actual field), ignore; API endpoint `POST /api/messebau/jobs/{id}/suggestions/{suggestionId}/apply|ignore` | | |
| TASK-AI02 | Create `src/components/shared/SuggestionBadge.tsx` — yellow notification banner shown inline near affected form field; displays suggested value with confidence percentage, source document name, and [Apply]/[Ignore] buttons; appears alongside existing form fields in Tab 0/1/3/4 | | |
| TASK-AI03 | Create `DocumentClassifier` — Symfony service triggered on `asset.postAdd`; analyzes file extension, content type, and filename patterns to classify: briefing, brandbook, floorplan, power-confirmation, rigging-quote, cleaning-confirmation, stand-photo, acceptance-photo, weekly-protocol, consultation-summary, other; routes to correct extraction chain | | |
| TASK-AI03a | Create `DocumentScopeDetector` skill — runs during classification (Step 3); determines whether a document is customer-scoped (mentions multiple fairs, brandbook), fair-scoped (mentions "alle Stände", booth counts, hall numbers), or job-scoped (single booth + one company); outputs `suggestedScope` + `suggestedEntityId`; triggers relocation suggestion if scope mismatches upload field; example: CFC briefing (112 booths) uploaded to job → suggests move to `fair.constructionBriefing` | | |
| TASK-AI03b | Refactor skill chain to be scope-aware — all skills accept a `scope` parameter (`customer`\|`fair`\|`job`) and an `affectedEntityId`; skills output `scope` + `affectedEntityIds[]` in their result; `SuggestionService` stores scope level on each suggestion; frontend tags suggestions with scope badge; propagation: fair-scoped suggestion shows "Apply to all X jobs?"; customer-scoped suggestion shows "Apply to all jobs for this customer?" | | |
| TASK-AI04 | Create `ContactExtractor` skill — parses briefing docs (PDF, DOCX) via LLM/text extraction for name/email/phone patterns; returns ISO format contacts; compares with `customer` contact fields; suggests updates if different | | |
| TASK-AI05 | Create `DeadlineDetector` skill (enhanced for German) — scans any document for date patterns including German ranges ("01.–02.07.2026", "Aufbau: DD.–DD.MM.YYYY", "Abbau ab DD.MM. HH:MM Uhr"); maps recognized dates to setup/event/teardown fields using context keywords; compares with stored values; flags mismatches | | |
| TASK-AI05a | Create `MultiFairDetector` skill — parses consultation summaries listing multiple fairs; extracts fair name, booth size, stand type, location hints, budgets; suggests bulk creation of `Fair` records + `Job` placeholders for the referenced customer; deduplicates against existing fairs | | |
| TASK-AI05b | Create `BudgetExtractor` skill — detects currency amounts (€, EUR, k€) with context words (Standbau, Aktivierung, Möbel, Transport); maps to new `Job.costEstimate` field collection; calculates expected totals for multi-booth briefings | | |
| TASK-AI05c | Create `PainPointSummarizer` skill — extracts structured pain points and expectations from consultation/briefing docs; generates a concise structured notes summary; suggests appending to `job.notes` with [Preview/Append/Replace] options | | |
| TASK-AI06 | Create `ChecklistGenerator` skill — extracts requirements/action items from customer briefing text (LLM); generates checklist item suggestions with confidence scores; suggests auto-creation on Tab 4 | | |
| TASK-AI07 | Write PHPUnit tests: `DocumentClassifierTest` (correct classification per document type), `ContactExtractorTest` (name/email/phone extraction from sample PDFs), `DeadlineDetectorTest` (German date ranges, field mapping), `MultiFairDetectorTest` (multi-fair extraction + dedup), `BudgetExtractorTest` (currency parsing, context mapping), `PainPointSummarizerTest` (structured output), `ChecklistGeneratorTest` (requirement extraction) | | |
| TASK-AI08 | Write Playwright test: `ai-suggestions-tier1.spec.ts` — upload Brevo consultation PDF → verify 5 fairs suggested for creation → verify contact Lisa Reinhardt extracted → verify budget amounts detected → verify pain points summarized → apply to notes → verify notes populated; upload CFC briefing → verify German dates parsed (01.–02.07.2026) → verify material checklist items generated | | |
#### Phase 12b — Tier 2 (Validation)
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-AI09 | Create `StandDataExtractor` skill — parses DWG/DXF/PDF floor plans for dimensions, stand type designators, hall/stand numbers; compares with Tab 0 Stammdaten values; flags discrepancies | | |
| TASK-AI10 | Create `ConfirmationParser` skill — parses booking confirmations (power/WLAN/cleaning/rigging PDFs) for service details, quantities, confirmation numbers, dates; compares with booked fields in Organisation tab; sets "confirmed" flag if match; warns if mismatch | | |
| TASK-AI11 | Create `BrandConsistencyChecker` skill — extracts brand colors and logo specifications from brandbook (AI/PDF); when stand photos are uploaded, analyzes images for color proximity to brand palette, logo visibility; flags deviations | | |
| TASK-AI12 | Create `DocumentMismatchDetector` skill — triggers on `DocumentClassifier` mismatch; e.g. user uploads floor plan into "Briefing Kunde" area → suggests moving to "Standplan" section; provides one-click reassign to correct asset field | | |
| TASK-AI12a | Create `DocumentVersionDetector` skill — when a new asset is uploaded to a field with an existing asset, compares filenames (Levenshtein) + content (dimensions, budgets, dates extraction); flags changes between versions with severity (info/warning/critical); example: OMR 2026 dimensions changed from 8.65×5.8m to 7.50×6.70m (same area, different layout) | | |
| TASK-AI13 | Write PHPUnit tests: `StandDataExtractorTest`, `ConfirmationParserTest`, `BrandConsistencyCheckerTest`, `DocumentMismatchDetectorTest`, `DocumentVersionDetectorTest` | | |
| TASK-AI14 | Write Playwright test: `ai-suggestions-tier2.spec.ts` — upload floor plan → verify dimension mismatch warning on Tab 0; upload power confirmation → verify "confirmed" badge + kW mismatch flag; upload brandbook + stand photo → verify color consistency check; upload newer version of OMR briefing over existing one → verify version change detected → compare dimensions → verify warning shown | | |
#### Phase 12c — Tier 3 (Enhancement)
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-AI15 | Create `MaterialListExtractor` skill — parses briefing text and floor plan legends for material names, furniture items, equipment references; suggests auto-population of Tab 3 Produktion material sections | | |
| TASK-AI15a | Create `LeadMagnetIdeator` skill — when briefing requests activation/lead magnet ideas, generates creative suggestions based on company description, product, budget, event type, and previous activations from the document; outputs 3 ideas with title, description, relevance score, budget estimate, and difficulty; displayed as suggestion cards on Tab 3 Leadmagneten section | | |
| TASK-AI16 | Create `AcceptanceReportGenerator` skill — combines acceptance photos, signature, remarks from Tab 6; generates structured acceptance PDF with annotated photos, issue list, and sign-off block via Gotenberg | | |
| TASK-AI17 | Create `SetupProgressAnalyzer` skill — when stand photos are uploaded at different timestamps, compares images for structural changes (walls up, graphics mounted, furniture placed); auto-checks corresponding checklist items | | |
| TASK-AI18 | Create `WeeklyProtocolSummarizer` skill — extracts status updates, open items, decisions from uploaded protocol PDFs; generates a structured summary JSON; renders as a timeline card on the Weekly tab | | |
| TASK-AI19 | Write PHPUnit tests: `MaterialListExtractorTest`, `LeadMagnetIdeatorTest`, `AcceptanceReportGeneratorTest`, `SetupProgressAnalyzerTest`, `WeeklyProtocolSummarizerTest` | | |
| TASK-AI20 | Write Playwright test: `ai-suggestions-tier3.spec.ts` — upload briefing → verify material suggestions on Tab 3; upload Brevo OMR briefing with activation request → verify 3 lead magnet ideas generated → select one → verify added to lead magnets; upload two stand photos 2 days apart → verify checklist item auto-checked; upload acceptance photos + remarks → export acceptance report → verify PDF content | | |
---
## 3. Alternatives
- **ALT-001: Incremental refactor of existing `index.html`** — Rejected because the current file is 6,200 lines of mixed HTML/CSS/JS with no framework, `var` usage, localStorage for files, and inline event handlers. Incremental refactoring would be slower and produce lower-quality results than a fresh start with proper tooling.
- **ALT-002: Vue or Svelte instead of React** — React chosen for largest ecosystem, shadcn/ui component library support, and broader developer availability. No technical reason against Vue/Svelte; either would work equally well.
- **ALT-003: Separate backend (NestJS/FastAPI) instead of Pimcore** — Pimcore is already in the stack and provides DAM, data modeling, auth, and workflow capabilities out of the box. A separate backend would duplicate auth and file management that Pimcore already handles.
- **ALT-004: Client-side PDF generation (jsPDF/html2canvas)** — Rejected in favor of Gotenberg (already running) for more reliable rendering, smaller client bundle, and consistency across browsers.
---
## 4. Dependencies
| ID | Dependency | Status | Notes |
|----|-----------|--------|-------|
| DEP-001 | Pimcore ≥ 11.x with Docker Compose | In place | Already running |
| DEP-002 | Gotenberg container (port 3000) | In place | PDF engine, already configured |
| DEP-003 | OAuth2 configured on Pimcore | In place | Password grant flow |
| DEP-004 | Node.js ≥ 20 and npm ≥ 9 | Required | For frontend development and build |
| DEP-005 | Symfony Messenger + cron | Pimcore default | For async email notifications |
| DEP-005a | `symfony/workflow` 6.x | Pimcore default | Symfony Workflow component (bundled with Pimcore, just needs config) |
| DEP-006 | MySQL (Pimcore's database) | In place | No changes needed |
| DEP-007 | `react` 18.x, `react-dom` 18.x | Package | Core UI framework |
| DEP-008 | `@tanstack/react-query` 5.x | Package | Server state management |
| DEP-009 | `zustand` 4.x | Package | Client state management |
| DEP-010 | `react-hook-form` 7.x + `zod` 3.x | Package | Form handling + validation |
| DEP-011 | `shadcn/ui` latest | Package | Component library (Tailwind-based) |
| DEP-012 | `react-router-dom` 6.x | Package | Client-side routing |
| DEP-013 | `react-signature-canvas` 1.x | Package | Signature pad for acceptance tab |
| DEP-014 | `sonner` 1.x | Package | Toast notifications |
| DEP-015 | `vite-plugin-pwa` | Package | PWA service worker generation |
| DEP-016 | `axios` 1.x | Package | HTTP client with interceptors |
| DEP-017 | LLM API (OpenAI / Anthropic / self-hosted) | External | AI extraction requiring NLU; gated behind config — skills use structured regex/parsing as fallback when LLM is unavailable |
| DEP-018 | `smalot/pdfparser` or `spatie/pdf-to-text` (PHP) | Package | PDF text extraction for AI skills; DWG/DXF parsing for floor plans |
---
## 5. Files
| ID | File | Type | Purpose |
|----|------|------|---------|
| FILE-001 | `bundles/MessebauBundle/MessebauBundle.php` | New | Symfony bundle registration |
| FILE-002 | `bundles/MessebauBundle/Resources/config/services.yaml` | New | Service definitions |
| FILE-003 | `bundles/MessebauBundle/Resources/config/routes.yaml` | New | API route definitions |
| FILE-004 | `bundles/MessebauBundle/Controller/Api/FairLocationController.php` | New | FairLocation CRUD endpoints |
| FILE-005 | `bundles/MessebauBundle/Controller/Api/CustomerController.php` | New | Customer CRUD endpoints |
| FILE-006 | `bundles/MessebauBundle/Controller/Api/FairController.php` | New | Fair CRUD + list jobs for fair + bulk PDF |
| FILE-007 | `bundles/MessebauBundle/Controller/Api/JobController.php` | New | Job CRUD endpoints |
| FILE-008 | `bundles/MessebauBundle/Controller/Api/AssetController.php` | New | File upload/delete endpoints |
| FILE-009 | `bundles/MessebauBundle/Controller/Api/CalendarController.php` | New | Calendar aggregation with effective dates |
| FILE-010 | `bundles/MessebauBundle/Controller/MessebauController.php` | New | Serves React SPA index.html |
| FILE-011 | `bundles/MessebauBundle/Service/JobDuplicateService.php` | New | Deep clone logic |
| FILE-012 | `bundles/MessebauBundle/Service/JobExportService.php` | New | Job PDF via Gotenberg |
| FILE-013 | `bundles/MessebauBundle/Service/FairExportService.php` | New | Fair bulk PDF (all jobs) via Gotenberg |
| FILE-014 | `bundles/MessebauBundle/EventListener/JobNotificationListener.php` | New | Deadline email reminders |
| FILE-015 | `bundles/MessebauBundle/Resources/config/workflows/job.yml` | New | Workflow state machine (8 places, transitions, guards) |
| FILE-016 | `bundles/MessebauBundle/Controller/Api/WorkflowController.php` | New | Workflow state + transitions endpoint |
| FILE-017 | `bundles/MessebauBundle/EventListener/WorkflowTransitionListener.php` | New | Workflow event hooks |
| FILE-018 | `bundles/MessebauBundle/Controller/Api/WorkerRoleController.php` | New | WorkerRole CRUD |
| FILE-019 | `bundles/MessebauBundle/Controller/Api/WorkerController.php` | New | Worker pool CRUD |
| FILE-020 | `bundles/MessebauBundle/Controller/Api/PoolVehicleController.php` | New | PoolVehicle CRUD |
| FILE-021 | `bundles/MessebauBundle/Controller/Api/EquipmentController.php` | New | Equipment CRUD |
| FILE-022 | `bundles/MessebauBundle/Controller/Api/FairResourceController.php` | New | Fair demand + assignment endpoints |
| FILE-023 | `bundles/MessebauBundle/Controller/Api/JobResourceController.php` | New | Job-scoped resource view |
| FILE-024 | `bundles/MessebauBundle/Controller/Api/AccommodationController.php` | New | Accommodation CRUD |
| FILE-025 | `bundles/MessebauBundle/Controller/Api/FairAccommodationController.php` | New | Fair-level accommodation assignments |
| FILE-026 | `bundles/MessebauBundle/Controller/Api/WorkerTravelController.php` | New | Worker travel CRUD |
| FILE-026a | `bundles/MessebauBundle/Service/SuggestionService.php` | New | AI suggestion persistence (store/apply/ignore) |
| FILE-026b | `bundles/MessebauBundle/Service/AiSkill/DocumentClassifier.php` | New | Document type detection |
| FILE-026ba | `bundles/MessebauBundle/Service/AiSkill/DocumentScopeDetector.php` | New | Scope detection (customer/fair/job) + relocation suggestion |
| FILE-026c | `bundles/MessebauBundle/Service/AiSkill/ContactExtractor.php` | New | Tier 1: contact info extraction |
| FILE-026d | `bundles/MessebauBundle/Service/AiSkill/DeadlineDetector.php` | New | Tier 1: date detection + field mapping |
| FILE-026e | `bundles/MessebauBundle/Service/AiSkill/ChecklistGenerator.php` | New | Tier 1: requirement → checklist items |
| FILE-026f | `bundles/MessebauBundle/Service/AiSkill/StandDataExtractor.php` | New | Tier 2: dimensions + stand info from floor plans |
| FILE-026g | `bundles/MessebauBundle/Service/AiSkill/ConfirmationParser.php` | New | Tier 2: booking confirmation validation |
| FILE-026h | `bundles/MessebauBundle/Service/AiSkill/BrandConsistencyChecker.php` | New | Tier 2: brandbook vs photos comparison |
| FILE-026i | `bundles/MessebauBundle/Service/AiSkill/DocumentMismatchDetector.php` | New | Tier 2: wrong-section upload detection |
| FILE-026j | `bundles/MessebauBundle/Service/AiSkill/MaterialListExtractor.php` | New | Tier 3: material/furniture from briefing |
| FILE-026k | `bundles/MessebauBundle/Service/AiSkill/AcceptanceReportGenerator.php` | New | Tier 3: structured acceptance PDF via Gotenberg |
| FILE-026l | `bundles/MessebauBundle/Service/AiSkill/SetupProgressAnalyzer.php` | New | Tier 3: photo timeline → checklist auto-check |
| FILE-026m | `bundles/MessebauBundle/Service/AiSkill/WeeklyProtocolSummarizer.php` | New | Tier 3: protocol → structured summary |
| FILE-026n | `bundles/MessebauBundle/EventListener/AiSkillTriggerListener.php` | New | Event listener on `asset.postAdd` → classifier → extraction chain |
| FILE-026o | `bundles/MessebauBundle/Service/AiSkill/MultiFairDetector.php` | New | Tier 1: multiple fair detection + bulk creation |
| FILE-026p | `bundles/MessebauBundle/Service/AiSkill/BudgetExtractor.php` | New | Tier 1: currency/context extraction → costEstimate |
| FILE-026q | `bundles/MessebauBundle/Service/AiSkill/PainPointSummarizer.php` | New | Tier 1: pain points → structured notes |
| FILE-026r | `bundles/MessebauBundle/Service/AiSkill/DocumentVersionDetector.php` | New | Tier 2: version diffing (Levenshtein + content comparison) |
| FILE-026s | `bundles/MessebauBundle/Service/AiSkill/LeadMagnetIdeator.php` | New | Tier 3: creative activation idea generation |
| FILE-026t | `frontend/src/components/shared/SuggestionBadge.tsx` | New | Inline AI suggestion display with Apply/Ignore |
| FILE-011 | `templates/messebau/pdf/export.html.twig` | New | PDF export HTML template |
| FILE-012 | `templates/messebau/app.html.twig` | New | React SPA shell template |
| FILE-013 | `frontend/package.json` | New | Frontend dependencies |
| FILE-014 | `frontend/vite.config.ts` | New | Vite build configuration |
| FILE-015 | `frontend/tsconfig.json` | New | TypeScript configuration |
| FILE-016 | `frontend/tailwind.config.ts` | New | Tailwind CSS configuration |
| FILE-017 | `frontend/src/main.tsx` | New | React entry point |
| FILE-018 | `frontend/src/App.tsx` | New | Router + providers |
| FILE-019 | `frontend/src/api/client.ts` | New | Axios + OAuth2 interceptor |
| FILE-020 | `frontend/src/api/fairLocations.ts` | New | FairLocation API functions |
| FILE-020a | `frontend/src/api/customers.ts` | New | Customer API functions |
| FILE-020b | `frontend/src/api/fairs.ts` | New | Fair API functions |
| FILE-020c | `frontend/src/api/jobs.ts` | New | Job API functions |
| FILE-020d | `frontend/src/api/calendar.ts` | New | Calendar API functions |
| FILE-022 | `frontend/src/stores/authStore.ts` | New | OAuth2 token + user state |
| FILE-023 | `frontend/src/stores/uiStore.ts` | New | UI state (views, toasts) |
| FILE-025 | `frontend/src/hooks/useFairs.ts` | New | Fair list query hook |
| FILE-025a | `frontend/src/hooks/useJobs.ts` | New | Job list query hook (filterable by fairId/customerId) |
| FILE-025b | `frontend/src/hooks/useJob.ts` | New | Single job query + mutations |
| FILE-026 | `frontend/src/hooks/useCalendar.ts` | New | Calendar data hook |
| FILE-027 | `frontend/src/types/job.ts` | New | TypeScript interfaces |
| FILE-028 | `frontend/src/components/layout/AppShell.tsx` | New | Main layout wrapper |
| FILE-028a | `frontend/src/hooks/usePermissions.ts` | New | Role-based permission flags (canEdit, canDelete, canCreateJob, canTransitionWorkflow, canManageResources, canSeeOrgTab, canSeeProduktionTab, canSeeWeeklyTab, canAddChecklistItems, isCustomer) |
| FILE-029 | `frontend/src/components/layout/Sidebar.tsx` | New | Navigation sidebar |
| FILE-030 | `frontend/src/components/auth/LoginPage.tsx` | New | OAuth2 login form |
| FILE-031 | `frontend/src/components/auth/ProtectedRoute.tsx` | New | Auth guard component |
| FILE-035 | `frontend/src/components/dashboard/FairCard.tsx` | New | Fair card in dashboard grid |
| FILE-035a | `frontend/src/components/dashboard/FairCardGrid.tsx` | New | Fair cards layout |
| FILE-035b | `frontend/src/components/dashboard/SearchBar.tsx` | New | Fair/job search input |
| FILE-035c | `frontend/src/components/dashboard/EmptyState.tsx` | New | Empty state view |
| FILE-035d | `frontend/src/components/fair/FairDetailView.tsx` | New | Fair detail "todo" view (all jobs for a fair) |
| FILE-035e | `frontend/src/components/job/CreateJobFlow.tsx` | New | Multi-step job creation wizard |
| FILE-035f | `frontend/src/components/job/JobCard.tsx` | New | Individual job card |
| FILE-035g | `frontend/src/components/job/JobCardGrid.tsx` | New | Job cards layout |
| FILE-035h | `frontend/src/components/fair/FairResourcesTab.tsx` | New | Fair-level resource demand + assignment grids |
| FILE-035i | `frontend/src/components/resources/WorkerPoolView.tsx` | New | Worker pool management page |
| FILE-035j | `frontend/src/components/resources/PoolVehicleView.tsx` | New | Truck pool management page |
| FILE-035k | `frontend/src/components/resources/EquipmentView.tsx` | New | Equipment pool management page |
| FILE-035l | `frontend/src/api/workerroles.ts` | New | WorkerRole API functions |
| FILE-035m | `frontend/src/api/workers.ts` | New | Worker API functions |
| FILE-035n | `frontend/src/api/poolvehicles.ts` | New | PoolVehicle API functions |
| FILE-035o | `frontend/src/api/equipment.ts` | New | Equipment API functions |
| FILE-035p | `frontend/src/api/fairResources.ts` | New | Fair resource demand + assignment API |
| FILE-035q | `frontend/src/types/resources.ts` | New | Resource TypeScript interfaces |
| FILE-035r | `frontend/src/hooks/useFairResources.ts` | New | Fair resources query hook |
| FILE-035s | `frontend/src/hooks/useJobResources.ts` | New | Job resources query hook |
| FILE-035t | `frontend/src/components/resources/AccommodationPoolView.tsx` | New | Accommodation pool management page |
| FILE-035u | `frontend/src/components/resources/WorkerDetailView.tsx` | New | Worker detail page (travel + accommodation + assignments) |
| FILE-035v | `frontend/src/api/accommodations.ts` | New | Accommodation API functions |
| FILE-035w | `frontend/src/api/travel.ts` | New | Travel API functions |
| FILE-036 | `frontend/src/components/job-detail/DetailHeader.tsx` | New | Job detail toolbar with breadcrumb |
| FILE-038 | `frontend/src/components/job-detail/TabContainer.tsx` | New | Tab navigation wrapper |
| FILE-039 | `frontend/src/components/job-detail/tabs/MasterDataTab.tsx` | New | Tab 0: Stammdaten |
| FILE-040 | `frontend/src/components/job-detail/tabs/BriefingTab.tsx` | New | Tab 1: Briefing |
| FILE-041 | `frontend/src/components/job-detail/tabs/OrganisationTab.tsx` | New | Tab 2: Organisation |
| FILE-042 | `frontend/src/components/job-detail/tabs/ProduktionTab.tsx` | New | Tab 3: Produktion |
| FILE-043 | `frontend/src/components/job-detail/tabs/DokumenteTab.tsx` | New | Tab 4: Dokumente |
| FILE-044 | `frontend/src/components/job-detail/tabs/StandfotosTab.tsx` | New | Tab 5: Standfotos |
| FILE-045 | `frontend/src/components/job-detail/tabs/StandabnahmeTab.tsx` | New | Tab 6: Standabnahme |
| FILE-046 | `frontend/src/components/job-detail/tabs/WeeklyTab.tsx` | New | Tab 7: Weekly |
| FILE-047 | `frontend/src/components/calendar/CalendarGrid.tsx` | New | Calendar month grid |
| FILE-048 | `frontend/src/components/calendar/CalendarDay.tsx` | New | Single calendar day |
| FILE-049 | `frontend/src/components/calendar/CalendarToolbar.tsx` | New | Month navigation |
| FILE-050 | `frontend/src/components/shared/Lightbox.tsx` | New | Image lightbox overlay |
| FILE-050a | `frontend/src/components/shared/WorkflowProgress.tsx` | New | Workflow phase stepper + transition buttons |
| FILE-051 | `frontend/Dockerfile` | New | Production frontend build |
| FILE-052 | `.github/workflows/ci.yml` | New | CI/CD pipeline |
| FILE-053 | `frontend/tests/` | New | Vitest + Playwright tests |
| FILE-054 | `index.html` | Archive | No longer needed after deploy |
---
## 6. Testing
| ID | Test | Type | Description |
|----|------|------|-------------|
| TEST-001 | `FairLocationControllerTest` | PHPUnit | CRUD with auth/roles |
| TEST-002 | `CustomerControllerTest` | PHPUnit | CRUD with auth/roles |
| TEST-003 | `FairControllerTest` | PHPUnit | CRUD + `/fairs/{id}/jobs` listing + bulk PDF |
| TEST-004 | `JobControllerTest` | PHPUnit | CRUD with filters (fairId, customerId, workflowState), effective date resolution |
| TEST-005 | `JobDuplicateServiceTest` | PHPUnit | Deep clone with preserved fair/customer relations, copied assets |
| TEST-006 | `JobExportServiceTest` | PHPUnit | PDF via Gotenberg, effective date resolution in template |
| TEST-007 | `FairExportServiceTest` | PHPUnit | Combined PDF for all jobs in a fair |
| TEST-008 | `CalendarControllerTest` | PHPUnit | Event aggregation with effective dates, fairId filter |
| TEST-009 | `WorkerRoleControllerTest` | PHPUnit | Role CRUD |
| TEST-010 | `WorkerControllerTest` | PHPUnit | Worker CRUD with role+state filters |
| TEST-011 | `FairResourceControllerTest` | PHPUnit | Demand CRUD, assignment CRUD, gap computation (needed − assigned), assignment scoping by fair/job |
| TEST-012 | `DocumentClassifierTest` | PHPUnit | Correct classification per document type (briefing/brandbook/floorplan/confirmation/photo/protocol) |
| TEST-013 | `ContactExtractorTest` | PHPUnit | Name/email/phone extraction from sample PDFs, ISO formatting, comparison with stored values |
| TEST-014 | `DeadlineDetectorTest` | PHPUnit | Date parsing (multiple formats), context-based field mapping, mismatch flagging |
| TEST-015 | `ChecklistGeneratorTest` | PHPUnit | Requirement extraction from text, checklist item generation with confidence scores |
| TEST-016 | `StandDataExtractorTest` | PHPUnit | Dimension + stand type extraction from floor plan samples |
| TEST-017 | `ConfirmationParserTest` | PHPUnit | Service detail extraction from booking confirmation PDFs |
| TEST-018 | `SuggestionServiceTest` | PHPUnit | Store/apply/ignore flow, apply writes to actual DO field, ignore removes from pending list |
| TEST-004a | `WorkflowTest` | PHPUnit | Assert initial state is `angebot`; test each transition: valid (applied), blocked-by-guard (rejected with reason); test date-based auto-transitions; test `abgeschlossen` applies read-only flag |
| TEST-004b | `WorkflowTransitionListenerTest` | PHPUnit | Assert audit log written on every transition; assert PDF generation triggered on `aufbau` enter; assert email dispatched on `messe` enter |
| TEST-005 | `authStore.test.ts` | Vitest | Login flow, token refresh, logout, sessionStorage persistence |
| TEST-006 | `client.test.ts` | Vitest | Token attachment, 401 refresh + retry, concurrent refresh queue |
| TEST-007 | `useJobs.test.ts` | Vitest | Query caching, search debounce, pagination |
| TEST-012 | `create-fair-and-jobs.spec.ts` | Playwright | Create fairLocation → create customer → create fair at location → create two jobs for different customers at same fair → verify both appear in fair detail → verify fair card on dashboard shows correct job count |
| TEST-013 | `edit-job.spec.ts` | Playwright | Open job → edit fields across tabs → override dates → save → reload → verify overrides persisted, inherited dates shown when override cleared |
| TEST-014 | `upload-files.spec.ts` | Playwright | Upload images and PDFs to various tabs, verify previews, download, delete |
| TEST-015 | `duplicate-delete.spec.ts` | Playwright | Duplicate job → verify "(Kopie)" + same customer/fair → delete original → verify gone from fair detail |
| TEST-016 | `calendar-navigation.spec.ts` | Playwright | Navigate months, filter by fair, click event → navigate to job detail |
| TEST-017 | `pdf-export.spec.ts` | Playwright | Export single job PDF + bulk fair PDF → verify downloads |
| TEST-018 | `roles-access.spec.ts` | Playwright | Viewer (cannot edit/delete, read-only), editor (can edit, cannot delete), admin (full); customer: login, verify only own jobs visible, verify 4/8 tabs shown, verify cannot see org/produktion/weekly tabs, verify cannot create/delete/duplicate, verify can check/uncheck checklist items but not add/remove, verify can export own job PDF |
| TEST-019 | `workflow-progression.spec.ts` | Playwright | Create job (verify "Angebot"), assign customer+fair → transition "Planung", upload briefing → transition "Organisation", verify guard blocks skip |
| TEST-020 | `responsive.spec.ts` | Playwright | 375px viewport: sidebar collapses, forms stack, gallery adjusts |
| TEST-021 | `offline.spec.ts` | Playwright | Go offline, verify indicator, verify cached static assets load |
| TEST-022 | `fair-resources.spec.ts` | Playwright | Open fair → Resources tab → add demand "3 Baristas" → verify gap=3 → create worker Anna (Barista, internal) → assign Anna to fair → verify assigned=1, gap=2 → assign Anna to a specific job → verify job Resources tab shows Anna |
| TEST-023 | `accommodation-travel.spec.ts` | Playwright | Create accommodation → assign worker → verify occupancy → add one-way travel → verify worker detail shows travel |
| TEST-024 | `ai-suggestions-tier1.spec.ts` | Playwright | Upload briefing PDF → verify contact suggestion appears → apply → verify field populated; verify deadline suggestion with mismatch warning → ignore → dismissed; verify checklist suggestion → apply → items created on Tab 4 |
| TEST-025 | `ai-suggestions-tier2.spec.ts` | Playwright | Upload floor plan → verify dimension mismatch warning on Tab 0; upload confirmation → verify "confirmed" badge + kW mismatch flag; upload brandbook + stand photo → verify color consistency check |
| TEST-026 | `ai-suggestions-tier3.spec.ts` | Playwright | Upload briefing → verify material suggestions on Tab 3; upload 2 stand photos 2 days apart → verify checklist auto-checked; upload acceptance photos → export acceptance report → verify PDF content |
---
## 7. Risks & Assumptions
| ID | Description |
|----|-------------|
| RISK-001 | **Pimcore Data Object serialization performance** — deeply nested field collections + relation objects may cause slow API responses. Mitigation: lazy-load relations, use Pimcore's `getBy` with `force` parameter selectively, paginate related items, consider denormalizing some fields into `MessebauJob` directly if performance degrades. |
| RISK-002 | **File upload timeout** — large files uploaded over slow connections may exceed PHP `max_execution_time` or proxy timeout. Mitigation: set reasonable file size limits (10 MB), implement chunked upload for future if needed. |
| RISK-003 | **OAuth2 token refresh race condition** — multiple concurrent API calls on 401 could trigger multiple refresh attempts. Mitigation: Axios interceptor with a refresh promise queue — only one refresh fires, all other 401s wait for its result. |
| RISK-004 | **Booking platform password stored in plaintext** — the current app stores the booking platform password in localStorage. Mitigation: use Pimcore's encrypted field type for the password field, or at minimum add a prominent notice in the UI. |
| RISK-005 | **Gotenberg availability** — PDF export depends on Gotenberg being reachable. Mitigation: implement health check on the export endpoint, return clear error if Gotenberg is down, consider client-side print fallback. |
| RISK-006 | **Workflow guard fragility** — guard logic embedded in YAML expressions may become complex and hard to test. Mitigation: keep guards simple (field-not-null checks), defer complex validation to custom guard classes (`GuardInterface`) for the `organisation→produktion` and `produktion→aufbau` transitions. |
| RISK-007 | **N+1 query problem with 4 entities** — serializing a `Job` with joined `Customer` + `Fair` + `FairLocation` could trigger excessive queries on list endpoints. Mitigation: use Pimcore's `getBy` API with eager-loading, flatten frequently-needed fields into the `Job` response directly (e.g. `customerName`, `fairName`, `fairLocationName` as denormalized cache), add response caching headers. |
| RISK-008 | **Resource gap calculation performance** — computing `count(WorkerAssignment)` grouped by role for a fair with 100+ jobs could be slow. Mitigation: denormalize `assignedCount` and `gap` into the API response in the controller (single aggregation query per role), not computed client-side. |
| RISK-009 | **AI hallucination in document extraction** — LLM-based extraction may produce plausible but incorrect data. Mitigation: all AI output is non-destructive suggestions only; user must explicitly Apply; confidence scores shown; structured regex/parsing used as fallback when LLM confidence is below threshold; Phase 1 skills use minimal LLM (structured extraction preferred). |
| RISK-010 | **LLM cost at scale** — per-asset LLM calls could become expensive with high document volume. Mitigation: skills triggered only on `asset.postAdd` (once per upload, not re-analyzed); results cached on the suggestion record; configurable LLM provider (self-hosted option); Tiers 2-3 are deployed only after Tier 1 validates the ROI. |
| ASSUMPTION-001 | Pimcore's REST API / Data Hub is suitable for the frontend's needs. If response times are too slow, a thin caching layer (Symfony cache or API Gateway) can be added. |
| ASSUMPTION-002 | The existing Docker Compose setup has sufficient resources to add the frontend build container. |
| ASSUMPTION-003 | Team is comfortable with React + TypeScript. If not, Vue or Svelte can be substituted with minimal plan changes. |
| ASSUMPTION-004 | No regulatory requirements (GDPR article 32, etc.) mandate encryption at rest beyond what Pimcore provides. If so, encrypted field type must be used for all contact/personal data. |
---
## 8. Related Specifications / Further Reading
- [Pimcore Data Objects Documentation](https://pimcore.com/docs/platform/Pimcore/Data_Objects/)
- [Pimcore REST API / Data Hub](https://pimcore.com/docs/platform/Pimcore/Web_Services/Data_Hub/)
- [Pimcore Asset/DAM Documentation](https://pimcore.com/docs/platform/Pimcore/Assets/)
- [Symfony Workflow Component](https://symfony.com/doc/current/components/workflow.html)
- [Gotenberg Chromium Conversion](https://gotenberg.dev/docs/routes#chromium-conversions)
- [TanStack Query Documentation](https://tanstack.com/query/latest)
- [shadcn/ui Components](https://ui.shadcn.com/)
- [React Signature Canvas](https://www.npmjs.com/package/react-signature-canvas)
- Original `index.html` reference: `/srv/dockerBuilds/messeLotse/index.html` (6,204 lines)