Files
Messe-Lotse/plan/architecture-rewrite-1.0.md
T
2026-09-29 09:40:32 +00:00

628 lines
82 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)