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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+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)
Binary file not shown.
+368
View File
@@ -0,0 +1,368 @@
---
goal: Vertical Slice Implementation Plan for 24HRS Messe-Lotse (AI-Optimized)
version: 1.0
date_created: 2026-06-10
last_updated: 2026-06-10
owner: 24HRS Team
status: Planned
tags: architecture, vertical-slices, ai-optimized, migration
---
# Introduction
![Status: Planned](https://img.shields.io/badge/status-Planned-blue)
This plan supersedes `architecture-rewrite-1.0.md` by re-scoping the 12 sequential phases into 6 AI-sized vertical slices. Each slice delivers a complete, independently shippable user capability across all layers (Pimcore data model → API → React UI → auth). No slice requires a later slice to function. Context: zero developers, AI-only execution. Scale: ~300 jobs/year, peaks at 113 jobs per fair.
## 1. Requirements & Constraints
### Inherited from architecture-rewrite-1.0.md
- **REQ-001**: Multi-user with OAuth2 authentication (Pimcore password grant)
- **REQ-002**: Four roles — Admin, Editor, Viewer, Customer — with scoped visibility
- **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)
- **REQ-006**: Feature parity with `index.html` mockup (8 tabs + dashboard + calendar)
- **REQ-007**: Symfony Workflow-driven phase progression (8-state state machine)
- **REQ-008**: Normalized 4-entity data model (fairLocation → fair → job ← customer)
- **REQ-009**: Date inheritance (job overrides nullable, fall back to fair dates)
- **REQ-010**: Fair Detail view — primary grouping of jobs
- **REQ-011**: Resource pool management (11 entities, deferred to Slice 5)
- **REQ-012**: AI document processing skills (deferred to Slice 4)
- **REQ-013**: Tiered document storage (customer/fair/job levels)
- **CON-001**: Must run in existing Docker Compose environment
- **CON-002**: No legacy data migration needed (greenfield)
- **CON-003**: Pimcore ≥ 11.x, Gotenberg, 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
### Slice-Specific Constraints
- **CON-SLICE-001**: Each slice must produce working, testable, demonstrable software
- **CON-SLICE-002**: No slice may depend on a later slice for core functionality
- **CON-SLICE-003**: Slices 1-3 deliver minimal viable product; Slices 4-6 add intelligence/automation
- **CON-SLICE-004**: All AI-generated code must be verifiable via Playwright E2E tests per slice
- **CON-SLICE-005**: Timestamped slice completion tracking per attached date column
### Decisions from Architecture Review (2026-06-10)
- **DEC-001**: SketchUp 3D bridge is async (message queue worker), not synchronous HTTP call from Pimcore controller. Job-level mutual exclusion prevents duplicate generations. Health monitoring required for SketchUp + MCP availability.
- **DEC-002**: Workflow must support admin "force transition" with justification log. Rigid guards alone are insufficient for real-world trade fair prep entropy. Card created: **INVESTIGATE workflow guard flexibility**.
- **DEC-003**: Tiered document model must degrade gracefully when no brandbooks or multi-fair docs exist. If a customer has no `brandDoc`, the UI shows nothing collapsed — no empty inheritance indicators.
- **DEC-004**: LLM operational cost and accuracy risk require investigation before Slice 4 begins. Card created: **INVESTIGATE monthly LLM cost at full scale (10,000+ calls/year)** and **INVESTIGATE SetupProgressAnalyzer auto-check confidence floor**.
## 2. Implementation Steps
---
### Slice 1 — Create and manage a trade fair
- GOAL-001: Full-stack capability to authenticate, create venues/customers/fairs, and view them on a dashboard. Proves the entire technology stack works end-to-end: Pimcore data objects, OAuth2, REST API, React SPA, role-based access.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-001 | Scaffold `MessebauBundle` in `bundles/MessebauBundle/` with Symfony bundle class, `services.yaml`, `routes.yaml` | | |
| TASK-002 | Create Data Object `FairLocation` (venue) — text: name, address, googleMapsUrl; contact: contactName, contactPhone, contactEmail; textarea: notes | | |
| TASK-003 | Create Data Object `Customer` (company) — text: company, address; contact: contactName, contactPhone, contactEmail; asset: brandDoc (document, nullable), consultationDocs (gallery, nullable), contractDocs (gallery, nullable); textarea: notes | | |
| TASK-004 | Create Data Object `Fair` (trade fair) — text: name, hallsUsed; relation: fairLocation (1→1 to `FairLocation`); datetime: setupStart, setupEnd, eventStart, eventEnd, teardownStart, teardownEnd; asset: constructionBriefing (gallery), floorPlan (gallery), regulations (gallery), protocols (gallery); textarea: notes | | |
| TASK-005 | Create `FairLocationController` — `GET /api/messebau/fairlocations` (list), `GET/POST/PUT/DELETE /api/messebau/fairlocations/{id}` | | |
| TASK-006 | Create `CustomerController` — `GET /api/messebau/customers` (list, searchable by company name), `GET/POST/PUT/DELETE /api/messebau/customers/{id}` | | |
| TASK-007 | Create `FairController` — `GET /api/messebau/fairs` (list, filterable by fairLocation, date range), `GET/POST/PUT/DELETE /api/messebau/fairs/{id}` | | |
| TASK-008 | Create `AssetController` (generalized) — `POST /api/messebau/{entity}/{id}/upload/{fieldName}` handles `customer` and `fair` entity uploads; `DELETE /api/messebau/{entity}/{id}/asset/{fieldName}` | | |
| TASK-009 | Configure OAuth2 firewall — protect `/api/messebau/*` with `pimcore.api` firewall; define 4 roles: `ROLE_MESSEBAU_CUSTOMER`, `ROLE_MESSEBAU_VIEWER`, `ROLE_MESSEBAU_EDITOR`, `ROLE_MESSEBAU_ADMIN`; Customer role auto-filters by `user.messebauCustomer` relation; add `messebauCustomer` field to Pimcore User class | | |
| TASK-010 | Scaffold Vite + React + TypeScript in `frontend/` with Tailwind CSS v3, shadcn/ui, react-router-dom, TanStack Query, Zustand, Axios | | |
| TASK-011 | Configure `vite.config.ts` — build output to `../public/build/messebau/`, base path `/build/messebau/`, proxy `/api` to Pimcore in dev mode | | |
| TASK-012 | Create `src/api/client.ts` — Axios instance, request interceptor for Bearer token, response interceptor for 401 → refresh token → retry; implement refresh token queue | | |
| TASK-013 | Create `src/stores/authStore.ts` — Zustand: accessToken, refreshToken, user (id, username, roles), login/logout/refresh, persist to sessionStorage | | |
| TASK-014 | Create `src/stores/uiStore.ts` — Zustand: currentView, sidebarCollapsed, toastQueue | | |
| TASK-015 | Create `src/hooks/usePermissions.ts` — reads role from authStore, returns boolean flags: canEdit, canDelete, canCreateJob, canManageResources, isCustomer, etc. | | |
| TASK-016 | Create `src/components/auth/LoginPage.tsx` — username + password form, calls authStore.login(), error display, redirect on success | | |
| TASK-017 | Create `src/components/auth/ProtectedRoute.tsx` — checks auth token, redirects to /login if missing | | |
| TASK-018 | Create `src/components/layout/AppShell.tsx` — flex: Sidebar (260px) + main (flex-1, 32px padding) | | |
| TASK-019 | Create `src/components/layout/Sidebar.tsx` — "24HRS" logo + "Messe-Lotse" subtitle (yellow on black); nav items: Dashboard, Kalender (all roles); logout at bottom; role-gated nav items reserved for later slices | | |
| TASK-020 | Create `src/components/dashboard/FairCard.tsx` — card per fair: name, location name, date range, job count placeholder (0); click navigates to /fair/{id} | | |
| TASK-021 | Create `src/components/dashboard/FairCardGrid.tsx` — responsive grid of FairCards; search bar filters by name or location | | |
| TASK-022 | Create `src/components/dashboard/EmptyState.tsx` — internal roles: "Keine Messen vorhanden" + create button; customer: "Keine Projekte vorhanden" (no create button) | | |
| TASK-023 | Create `src/components/fair/CreateFairFlow.tsx` — wizard: Step 1: select or create FairLocation (combobox), Step 2: enter fair details (name, dates, halls), Step 3: confirm | | |
| TASK-024 | Create `src/components/fair/FairDetailView.tsx` — fair header (name, location, dates), stats placeholder (0 jobs), "Neuen Job hinzufügen" button (gated by usePermissions) | | |
| TASK-025 | Create `src/App.tsx` — routes: /login → LoginPage, / → ProtectedRoute → AppShell → Dashboard, /fair/:id → FairDetailView, /fair/new → CreateFairFlow, 404 | | |
| TASK-026 | Create Pimcore controller `MessebauController::appAction()` — serves `public/build/messebau/index.html` via Twig; catch-all route for SPA routing | | |
| TASK-027 | Write Playwright test `slice-1-auth-create-fair.spec.ts` — login as editor, create fairLocation, create fair, verify fair appears on dashboard, logout; login as customer, verify dashboard loads (empty), deny access to /fair/new | | |
| TASK-028 | Write Vitest tests: `authStore.test.ts` (login, logout, token refresh), `usePermissions.test.ts` (role → flag mapping) | | |
---
### Slice 2 — Add jobs to a fair
- GOAL-002: Create and view booth projects (jobs) linked to customers and fairs. File upload works via Pimcore DAM. Basic master data editing with role scoping. Proves entity relations, asset management, and role-gated UI.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-101 | Create Data Object `Job` (booth project) — **core entity**: standType (select: Systemstand/Reihstand/Eckstand/Kopfstand/Blockstand/Inselstand), standSize, standHeight, hall, area, project; relations: customer (n→1 to Customer), fair (n→1 to Fair); date overrides: setupStartOverride, setupEndOverride, eventStartOverride, eventEndOverride, teardownStartOverride, teardownEndOverride (all nullable — if null, inherit from fair); workflowState (input, managed by Symfony Workflow); textarea: notes | | |
| TASK-102 | Create Data Object `ChecklistItem` — text, checked (boolean), order (integer); 1:n relation to Job | | |
| TASK-103 | Create Data Object `Material` — type (select: flooring/standsystem/hardware/furniture), name, quantity (integer), notes; 1:n relation to Job | | |
| TASK-104 | 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 EUR) | | |
| TASK-105 | Add asset fields to Job: standImage (image), standGallery (image gallery), briefingCustomerDoc (document, nullable — if null, inherits from customer), briefingPlanDoc (document, nullable — if null, inherits from fair), briefingAdditionalDoc (document), powerConfirmation (document), wlanConfirmation (document), cleaningConfirmation (document), riggingQuote (document), riggingDrawing (document), riggingMesseConfirmation (document), standApprovalDoc (document), acceptancePhotos (image gallery), acceptanceSignature (image), weeklyProtocols (document gallery) | | |
| TASK-106 | Create `JobController` — `GET /api/messebau/jobs` (paginated, filterable by fairId/customerId/workflowState/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}`, `PATCH /api/messebau/jobs/{id}`, `DELETE /api/messebau/jobs/{id}`. Customer role auto-filtered to own company's jobs; 403 on DELETE | | |
| TASK-107 | Extend `AssetController` to handle job entity uploads via existing generalized endpoint | | |
| TASK-108 | Extend `FairController` — `GET /api/messebau/fairs/{id}/jobs` lists all jobs for this fair (the "todo" view); fair serialization includes job count | | |
| TASK-109 | Create `src/types/job.ts` — TypeScript interfaces for all entities and field collections from TASK-101 through TASK-105 | | |
| TASK-110 | Create `src/api/fairLocations.ts` + `src/api/customers.ts` + `src/api/fairs.ts` + `src/api/jobs.ts` — typed API functions for all CRUD operations | | |
| TASK-111 | Create `src/hooks/useFairs.ts` + `src/hooks/useJobs.ts` + `src/hooks/useJob.ts` + `src/hooks/useFair.ts` — TanStack Query hooks | | |
| TASK-112 | Create `src/components/job/CreateJobFlow.tsx` — wizard: Step 1: select or create Customer (combobox with search), Step 2: select Fair (combobox grouped by fairLocation), Step 3: enter booth details (project, standType, standSize, hall). Navigate to job detail on creation | | |
| TASK-113 | Create `src/components/job/JobCard.tsx` — stand image, customer company name, fair name, hall, workflow progress placeholder; click navigates to /jobs/{id} | | |
| TASK-114 | Create `src/components/job/JobCardGrid.tsx` — responsive grid, maps jobs to JobCards | | |
| TASK-115 | Update `FairDetailView` — show JobCardGrid filtered to fair; "Neuen Job hinzufügen" button triggers CreateJobFlow (gated by usePermissions: true for editor/admin, false for customer/viewer); customer sees their own jobs only | | |
| TASK-116 | Create `src/components/job-detail/DetailHeader.tsx` — black bar: breadcrumb (fair.name → customer.company → job.project), back button; action buttons gated by usePermissions: save (editor/admin), duplicate (editor/admin), export PDF (all), delete (admin); customer: save + export only | | |
| TASK-117 | Create `src/components/job-detail/TabContainer.tsx` — dynamic tab list based on role: internal sees all 8 tabs; customer sees 4 tabs (Tab 0, 1, 4, 5+6); active tab in search params (?tab=0); inactive tabs hidden, not just disabled | | |
| TASK-118 | Create `src/components/job-detail/tabs/MasterDataTab.tsx` — two-column form using react-hook-form + zod: **entity selectors** (Customer + Fair read-only, with Google Maps link for fairLocation), **Stammdaten** (Projekt, Standart, Standgröße, Standhöhe, Halle/Stand, Standfläche — all editable by editor/admin, read-only for viewer, select fields editable by customer), **Zeitplanung** (fair dates shown read-only as reference; editor/admin sees override fields with inherited defaults; customer sees resolved effective dates read-only), **Status** (read-only badge), **Messestand-Bild** (file input, image preview, download/remove), **Notizen** (full-width textarea) | | |
| TASK-119 | Create `src/components/job-detail/tabs/DokumenteTab.tsx` — checklist with 8 default items from index.html prototype; checkbox + strikethrough; editor/admin can add/remove items; customer can only check/uncheck existing items (usePermissions.canAddChecklistItems) | | |
| TASK-120 | Write Playwright test `slice-2-jobs-crud.spec.ts` — login as editor, open fair detail, create job for customer A at that fair, verify job appears in fair detail grid, open job detail, edit master data fields, save, reload, verify persistence; login as customer A, verify only own jobs visible, verify edit fields limited to select fields, verify delete button absent | | |
| TASK-121 | Write Vitest tests: `useJobs.test.ts` (fetch, filter, pagination), `MasterDataTab.test.tsx` (form validation, inherited dates display) | | |
---
### Slice 3 — Complete a job's documents and workflow
- GOAL-003: Full job lifecycle with all 8 tabs operational, Symfony Workflow with manual transitions, file uploads for all tab fields, and PDF export via Gotenberg. At this point the system is a complete replacement for the index.html prototype.
**Workflow note per DEC-002:** Guards are implemented as described in TASK-202, but an admin-only `POST /api/messebau/jobs/{id}/workflow/{transition}?force=true&reason=...` endpoint is provided. Force transitions log the admin user + reason to transition history. This is a safety valve for the gap between ideal workflow and real-world fair prep.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-201 | 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+fair relations, `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 | | |
| TASK-202 | 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 manual transition; `POST ...?force=true&reason=...` (admin only) bypasses guards, logs justification to transition history | | |
| TASK-203 | Create `WorkflowTransitionListener` — on `workflow.enter`: log timestamp to transition history (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 | | |
| TASK-204 | Create `src/components/job-detail/tabs/BriefingTab.tsx` — 3 collapsible sections with scope badges: "☰ Kunde" (customer.brandDoc, customer.consultationDocs, customer.contractDocs), "🌐 Messe" (fair.constructionBriefing, fair.floorPlan, fair.regulations, fair.protocols), "📋 Job" (job.briefingCustomerDoc override with "inherited from customer" indicator if null, job.briefingPlanDoc override with "inherited from fair" indicator if null, job.briefingAdditionalDoc). Each section has upload area. Follows DEC-003: empty levels show nothing collapsed — no empty inheritance indicators if parent has no docs | | |
| TASK-205 | Create `src/components/job-detail/tabs/OrganisationTab.tsx` — collapsible sections for Buchungsplattform, Strombuchung, WLAN, Reinigungsservice, Rigging/Truss, Standfreigabe; each section has enable checkbox + fields + file upload with preview; resource summary (read-only placeholder, filled in Slice 5) | | |
| TASK-206 | Create `src/components/job-detail/tabs/ProduktionTab.tsx` — material lists (4 sections: Bodenbeläge, Standsystem, Hardware, Möbel) with add/remove; Druckproduktion section (qm, blockout, forexplatten, deadline); Catering section (barista, drinks, food); Leadmagneten section (9 checkboxes) | | |
| TASK-207 | Create `src/components/job-detail/tabs/StandfotosTab.tsx` — upload zone (dashed border, drag-and-drop, image/*, multiple, max 10), gallery grid with download + delete, click opens Lightbox | | |
| TASK-208 | Create `src/components/shared/Lightbox.tsx` — portal-based (ReactDOM.createPortal), dark overlay, centered image, close button, prev/next arrows, keyboard navigation (arrow keys, escape), click-outside-to-close | | |
| TASK-209 | Create `src/components/job-detail/tabs/StandabnahmeTab.tsx` — acceptance photo gallery (separate from Standfotos), signature pad (react-signature-canvas, 600x200, clear + save), remarks textarea, Abnahmedatum (datetime-local) | | |
| TASK-210 | Create `src/components/job-detail/tabs/WeeklyTab.tsx` — multi-file upload for PDFs, list of uploaded protocols with filename/size/download/delete, empty state with PDF icon | | |
| TASK-211 | Create `src/components/shared/WorkflowProgress.tsx` — horizontal stepper: 8 phases with current/active/complete states, phase icon + label; "Nächster Schritt: [Phase]" button triggers workflow transition; blocked transitions show tooltip with guard reason; force transition button (admin only) with reason text input modal; transition history expandable timeline | | |
| TASK-212 | 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 | | |
| TASK-213 | Create job PDF export endpoint — `GET /api/messebau/jobs/{id}/export-pdf` invokes JobExportService, returns PDF with Content-Disposition: attachment | | |
| TASK-214 | Create `templates/messebau/pdf/export.html.twig` — print-optimized HTML: 24HRS header, customer + fair + location info grid, stand details, material tables, checklist with checkmarks, acceptance info + signature; CSS @page, yellow accents, Inter font, page-break-inside: avoid | | |
| TASK-215 | 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 | | |
| TASK-216 | Create `src/hooks/useCalendar.ts` — TanStack Query hook fetching calendar endpoint; store current month/year/fairId in local state | | |
| TASK-217 | Create `src/components/calendar/CalendarToolbar.tsx` — prev/next month buttons, month-year heading, "Heute" button, fair selector dropdown, color legend | | |
| TASK-218 | Create `src/components/calendar/CalendarGrid.tsx` — 7-column grid, day headers (Mo-So), correct day cells, leading/trailing grayed out | | |
| TASK-219 | Create `src/components/calendar/CalendarDay.tsx` — day number (bold + today border), event pills with color-coded types: setup=#3498db, event=#FFE000, teardown=#e74c3c, deadline=#FF6B35; truncated text with customer+fair name, click navigates to /jobs/{jobId}; span events render on each day in range | | |
| TASK-220 | Write Playwright test `slice-3-full-workflow.spec.ts` — create fair+job, upload briefing doc, transition angebot→planung→organisation→produktion, verify guards block (try to skip to aufbau), fill required fields, complete checklist, transition to aufbau, add acceptance photo+signature, transition to abgeschlossen, verify read-only; force-transition from abgeschlossen to abbau (admin override), verify reason logged in history; export PDF, verify download | | |
| TASK-221 | Write Playwright test `slice-3-calendar.spec.ts` — create 2 jobs at same fair with different dates, open calendar scoped to that fair, verify events render on correct days, click event pill navigates to job detail | | |
---
### Slice 4 — AI document intelligence
- GOAL-004: Pluggable AI skills triggered on asset upload to Pimcore DAM. Common chain (Classifier → MismatchDetector → VersionDetector → DocumentScopeDetector) runs on every upload. Field-specific skills extract data and surface non-destructive suggestions. User explicitly applies or ignores each suggestion.
**Precondition:** INVESTIGATE cards from DEC-004 must be resolved before Slice 4 work begins:
1. Monthly LLM cost estimate at 10,000+ calls/year
2. SetupProgressAnalyzer auto-check confidence threshold and false-positive safety measures
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-301 | **INVESTIGATE:** Estimate monthly LLM cost — audit expected call volume per skill type × model pricing per token (gpt-4o-mini for most skills, gpt-4o for LeadMagnetIdeator, gpt-4o vision for SetupProgressAnalyzer + BrandConsistencyChecker); produce budget range; if >500€/month at full scale, recommend cost optimization strategy | | |
| TASK-302 | **INVESTIGATE:** Define confidence floor for auto-actions — what confidence threshold prevents SetupProgressAnalyzer from auto-checking checklist items that weren't actually done; propose: auto-check only if confidence ≥ 0.90 AND the corresponding checklist item text semantically matches the detected change by cosine similarity ≥ 0.80; all lower-confidence matches surface as suggestions only | | |
| TASK-303 | Create `SuggestionService` in `bundles/MessebauBundle/Service/` — stores AI suggestions as JSON field `aiSuggestions` on Job DO (array of `{id, skill, field, suggestedValue, confidence, sourceAssetId, status(pending/applied/ignored), createdAt}`); methods: add(), apply() writes to actual field, ignore(); API endpoints: `GET /api/messebau/jobs/{id}/suggestions`, `POST .../suggestions/{suggestionId}/apply`, `POST .../suggestions/{suggestionId}/ignore` | | |
| TASK-304 | Create `DocumentClassifier` (SKILL-001) — Symfony service triggered on `asset.postAdd`; analyzes file extension, MIME type, filename patterns, first 4KB of content; classifies into: briefing, brandbook, floorplan, confirmation, confirmation-quote, stand-photo, acceptance-photo, weekly-protocol, consultation-summary, other. Pure regex + metadata — no LLM | | |
| TASK-305 | Create `DocumentMismatchDetector` (SKILL-008) — triggered by common chain; compares classified type against upload field context; if mismatch (e.g. floorplan uploaded to briefing area), returns `{suggestedField, reason}`; blocks skill chain execution until user resolves (Move / Keep / Cancel) | | |
| TASK-306 | Create `DocumentVersionDetector` (SKILL-008a) — triggered if field has ≥1 prior asset; filename Levenshtein distance + content comparison; if newer version detected: diff extracted dimensions/budgets/dates, surface warning with severity (info/warning/critical); user chooses Replace or Keep both | | |
| TASK-307 | Create `DocumentScopeDetector` (SKILL-001a) — triggered on briefing-type uploads; detects customer/fair/job scope via heuristics (multi-fair mentions → customer, "alle Stände" + booth counts → fair, single booth + company → job); if scope mismatches upload field, suggests relocation with target entity preview; if user moves: asset relocated, skills re-run on correct entity | | |
| TASK-308 | Create `AiSkillTriggerListener` — Symfony event listener on `asset.postAdd`; runs Classifier + MismatchDetector inline (sync, <5ms); dispatches one `AnalyzeDocumentMessage` per applicable skill to Symfony Messenger queue `ai_skills` (Redis or Doctrine transport) | | |
| TASK-309 | Configure Symfony Messenger — `ai_skills` transport, multiple `messenger:consume` workers for parallel skill execution | | |
| TASK-310 | Create `ContactExtractor` (SKILL-002) — triggered on briefing/confirmation docs; regex for email/phone → LLM fallback for name/role; output: `{name, email, phone, role}[]` → targets customer.contactName/Phone/Email | | |
| TASK-311 | Create `DeadlineDetector` (SKILL-003 + SKILL-004a) — triggered on any document; regex for date patterns (DD.MM.YYYY, German relative dates, date ranges like "01.–02.07.2026"); field mapping heuristics (Aufbau→setupStart, Strom→power.deadline, etc.); conflict detection if document date differs from stored deadline | | |
| TASK-312 | Create `MultiFairDetector` (SKILL-004b) — triggered on briefing/consultation-summary; LLM extraction of fair name, size, standType, location, budget; deduplication against existing fairs; suggestion: "Create N new fair records for customer X?" | | |
| TASK-313 | Create `BudgetExtractor` (SKILL-004c) — triggered on any doc with currency amounts; regex for monetary patterns (€/EUR/k€) with context mapping (Standbau→costEstimate.standConstruction, Aktivierung→activation, etc.); fallback to regex-only if no LLM available | | |
| TASK-314 | Create `PainPointSummarizer` (SKILL-004d) — triggered on consultation-summary/briefing; LLM extraction of painPoints[], expectations[], structuredNotes; suggestion: append/replace job.notes | | |
| TASK-315 | Create `ChecklistGenerator` (SKILL-004) — triggered on briefing; LLM extraction of actionable tasks in German; deduplication against existing checklist items via cosine similarity of text embeddings | | |
| TASK-316 | Create `StandDataExtractor` (SKILL-005) — triggered on floorplan; PDF/DWG text layer parsing for dimensions, standType, hall, standNumber; image-based fallback: LLM vision; comparison >10% discrepancy flags warning | | |
| TASK-317 | Create `ConfirmationParser` (SKILL-006) — triggered on confirmation; LLM extraction of serviceType, details, confirmationNumber, bookingDate, status; auto-confirm if confirmation number found in text; discrepancy flag if booked values differ from stored | | |
| TASK-318 | Create `BrandConsistencyChecker` (SKILL-007) — triggered on 2nd+ stand photo AND brandbook exists; extract brand colors from brandbook, analyze photo color histogram, Delta-E comparison; LLM vision: "Is logo visible? Position? Deviations from guidelines?"; result: green checkmark or amber warning overlay in photo gallery | | |
| TASK-319 | Create `AcceptanceReportGenerator` (SKILL-010) — manual trigger only (button on Tab 6); LLM vision: analyze acceptance photos + remarks → structured report in German → Gotenberg PDF; includes photo gallery with annotations, issue summary, sign-off block | | |
| TASK-320 | Create `SetupProgressAnalyzer` (SKILL-011) — triggered on 2nd+ stand photo; LLM vision: compare two most recent photos chronologically; detect structural changes (walls erected, flooring laid, graphics mounted, furniture placed, equipment installed, cleaning completed); auto-check mapping per DEC-302 confidence floor; all lower-confidence detections → suggestion only | | |
| TASK-321 | Create `WeeklyProtocolSummarizer` (SKILL-012) — triggered on weekly-protocol PDF upload; LLM extract: date, status update, open items, decisions, next steps; renders as structured summary card on Tab 7 above file list | | |
| TASK-322 | Create `MaterialListExtractor` (SKILL-009) — triggered on briefing/floorplan; LLM extraction of materials categorized as flooring/standsystem/hardware/furniture with name + suggested quantity | | |
| TASK-323 | Create `LeadMagnetIdeator` (SKILL-009a) — triggered on briefing/consultation-summary with activation requirements; LLM (gpt-4o, T=0.7): "Generate 3 creative lead magnet ideas tied to company product, fitting budget, for event type"; context consumed: company description, product info, previous activations, budget constraints, event atmosphere | | |
| TASK-324 | Create `src/components/shared/SuggestionBadge.tsx` — yellow notification banner near affected form field; shows suggested value, confidence %, source document name; [Apply] / [Ignore] buttons; per-tab [Apply All]; tab headers show pending suggestion count badge; low-confidence suggestions (< configurable threshold) hidden by default with toggle | | |
| TASK-325 | Integrate SuggestionBadge into all affected tabs (Tab 0 MasterData, Tab 1 Briefing, Tab 3 Produktion, Tab 4 Checklist, Tab 5 Standfotos, Tab 7 Weekly) | | |
| TASK-326 | Write Playwright test `slice-4-ai-pipeline.spec.ts` — upload Brevo consultation doc to briefingCustomerDoc; verify DocumentClassifier identifies it; verify MismatchDetector passes (correct field); verify ContactExtractor surfaces Lisa Reinhardt suggestion; apply it; verify job.contactName updated; upload floor plan to briefingCustomerDoc; verify MismatchDetector suggests move to briefingPlanDoc; move it; verify asset relocated | | |
| TASK-327 | Write Playwright test `slice-4-setup-progress.spec.ts` — upload first stand photo; verify no analysis; upload second (different angle, more construction); verify SetupProgressAnalyzer compares images; verify low-confidence matches (<0.90) become suggestions, not auto-checks; verify high-confidence match (>0.90) auto-checks corresponding checklist item | | |
---
### Slice 5 — Fair-level resource planning
- GOAL-005: Resource pool management with demand planning, worker/vehicle/equipment/accommodation assignment, travel management, and fill-gap visualization at fair level. Replaces per-job ad-hoc team/vehicle/hotel lists from the prototype.
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-401 | Create Data Object `WorkerRole` — name (e.g. Barista, Rigger, LorryDriver, FairWorker, Electrician, Cleaner) | | |
| TASK-402 | Create Data Object `Worker` — firstName, lastName, email, phone; role (n→1 to WorkerRole); state (internal/hired/toHire); notes | | |
| TASK-403 | Create Data Object `PoolVehicle` — name, type, licensePlate; state (owned/hired/toHire); notes | | |
| TASK-404 | Create Data Object `Equipment` — name, description; state (owned/rented/toRent); notes | | |
| TASK-405 | Create Data Object `WorkerDemand` — fair (n→1 to Fair, required), job (n→1 to Job, nullable), workerRole (n→1 to WorkerRole, required); quantity (integer); notes | | |
| TASK-406 | Create Data Object `WorkerAssignment` — fair (n→1 to Fair, required), job (n→1 to Job, nullable), worker (n→1 to Worker, required); startDate, endDate (datetime); notes | | |
| TASK-407 | Create Data Object `PoolVehicleAssignment` — fair, job (nullable), poolVehicle; startDate, endDate; notes | | |
| TASK-408 | Create Data Object `EquipmentAssignment` — fair, job (nullable), equipment; startDate, endDate; notes | | |
| TASK-409 | Create Data Object `Accommodation` — name, address, bookingReference; type (hotel/apartment/other); notes | | |
| TASK-410 | Create Data Object `AccommodationAssignment` — fair, accommodation, worker (nullable); checkIn, checkOut (date); notes. Multiple workers can share same accommodation | | |
| TASK-411 | Create Data Object `Travel` — worker (n→1 to Worker, required), fair (n→1 to Fair, required); type (oneWay/roundTrip); origin, destination, transportMethod, bookingReference; departureDateTime, arrivalDateTime; returnDepartureDateTime, returnArrivalDateTime (nullable); notes | | |
| TASK-412 | Create controllers: `WorkerRoleController`, `WorkerController`, `PoolVehicleController`, `EquipmentController`, `AccommodationController` — full CRUD for each pool entity | | |
| TASK-413 | 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/vehicle/equipment assignments | | |
| TASK-414 | Create `JobResourceController` — `GET /api/messebau/jobs/{id}/resources` returns demands+assignments scoped to this job | | |
| TASK-415 | Create `FairAccommodationController` — `GET /api/messebau/fairs/{id}/accommodations` returns accommodations + assignments + occupancy; `POST/PUT/DELETE /api/messebau/fairs/{id}/accommodations/{accId}/assign` | | |
| TASK-416 | Create `WorkerTravelController` — `GET /api/messebau/workers/{id}/travel` (filterable by fairId); `POST/PUT/DELETE /api/messebau/workers/{id}/travel` | | |
| TASK-417 | Create `src/types/resources.ts` — TypeScript interfaces for all 11 resource pool entities | | |
| TASK-418 | Create `src/api/` files for all resource API endpoints (workerroles, workers, poolvehicles, equipment, accommodations, fairResources, jobResources, travel) | | |
| TASK-419 | Create `src/hooks/useFairResources.ts` + `src/hooks/useJobResources.ts` — TanStack Query hooks | | |
| TASK-420 | Create `src/components/fair/FairResourcesTab.tsx` — four grids: **Workers by Role** (role, needed qty, assigned count, gap, assigned workers with job labels, Add Demand / Assign Worker buttons), **Trucks** (vehicle name, state badge, assigned to job/dates), **Equipment** (item name, state badge, assigned to job/dates), **Accommodation** (name, type badge, occupancy, assigned workers with check-in/out, Assign Worker button); modals for add-demand and add-assignment | | |
| TASK-421 | Create `src/components/resources/WorkerPoolView.tsx` — list all workers filterable by role+state, CRUD, "Add Worker" form; each worker row shows assigned fairs + travel count | | |
| TASK-422 | Create `src/components/resources/PoolVehicleView.tsx` + `src/components/resources/EquipmentView.tsx` + `src/components/resources/AccommodationPoolView.tsx` — same CRUD pattern | | |
| TASK-423 | Create `src/components/resources/WorkerDetailView.tsx` — worker info + role + state; travel records list (outbound/return info, transport method, booking reference, Add Travel button, one-way toggle); accommodation assignments at each fair; fair assignments | | |
| TASK-424 | Update Sidebar — add "Ressourcen" nav item (collapsible sub-items: Worker Rollen, Worker Pool, Fahrzeug Pool, Equipment Pool, Unterkünfte); hidden for customer role | | |
| TASK-425 | Update OrganisationTab (from Slice 3) — replace placeholder resource summary with live data from `GET /api/messebau/jobs/{id}/resources`; each item shows name + role/type + state badge + dates; "[Manage all resources for this fair →]" links to FairResourcesTab | | |
| TASK-426 | Write Playwright test `slice-5-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 specific job → verify job Resources tab shows Anna; remove Anna → verify gap returns to 3 | | |
| TASK-427 | Write Playwright test `slice-5-accommodation-travel.spec.ts` — create accommodation "Hotel Central" → assign worker Anna (check-in Jun 14, check-out Jun 18) → verify occupancy=1 → add travel for Anna: one-way Berlin→Hannover, Bahn, Jun 14 08:00→10:30 → verify worker detail shows travel + accommodation | | |
---
### Slice 6 — 3D booth generation
- GOAL-006: Automated 3D booth model generation via SketchUp + Aluvision + MCP bridge. Driven by job data from the 24HRS application. Delivers .skp model, PNG preview, and BOM back to the job.
**Architecture note per DEC-001:** Pimcore dispatches an async message to generate-3d queue. An MCP server worker pulls the message, invokes the SketchUp Ruby bridge via HTTP, stores results in Pimcore DAM, and updates job fields. Job-level lock prevents duplicate generations. Health endpoint on MCP server enables monitoring.
**Precondition:** SketchUp Pro with Aluvision 3D Library installed on a dedicated machine. WEBrick bridge extension loaded. MCP server accessible to both Pimcore (for dispatching) and SketchUp bridge (localhost:9494).
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-501 | **AUDIT:** Examine Aluvision 3D Library in SketchUp — catalog component naming conventions, folder structure, parameter patterns; document findings in `graphify-out/aluvision-audit.json` | | |
| TASK-502 | Build SketchUp Ruby bridge — `aluvision_bridge.rb` extension entry point (WEBrick server on port 9494, auto-starts on SketchUp launch via UI.start_timer); implement `/health`, `/component/list`, `/booth/generate`, `/booth/export`, `/booth/bom` endpoints | | |
| TASK-503 | Build `AluvisionRegistry` — Ruby module mapping logical component names → SketchUp library paths; includes all profile types, connectors, accessories, furniture, flooring as documented in 3d-booth-generation.md § Aluvision Registry Pattern | | |
| TASK-504 | Build `BoothBuilder` — Ruby module: LAYOUT_WALLS mapping (6 stand types → wall topology), WALL_POSITIONS (position calculation per side), WALL_SIZES (dimension calculation); clears model, places walls, connectors, flooring, furniture, lead magnets; commits operation, zooms to fit | | |
| TASK-505 | Build `WallBuilder`, `ConnectorBuilder`, `FlooringBuilder`, `FurniturePlacer`, `StorageBuilder` — individual component placement Ruby modules loaded by BoothBuilder | | |
| TASK-506 | Build `BomExtractor` — enumerates placed components in active model, maps to Aluvision part numbers, computes totals (component count, profile length, connector count, flooring area); outputs JSON | | |
| TASK-507 | Build `ModelExporter` — exports .skp (native save), renders PNG (SketchUp viewport capture), optional .glb/.dae export | | |
| TASK-508 | Scaffold `aluvision-mcp-server/` — Node.js/TypeScript project with `@modelcontextprotocol/sdk`; implement `server.ts` entry point with StdioServerTransport | | |
| TASK-509 | Implement `SketchUpClient` — HTTP client (fetch wrapper) targeting `http://localhost:9494`; GET for /health, /component/list, /booth/bom; POST for /booth/generate, /booth/export | | |
| TASK-510 | Implement `LayoutEngine` — STAND_TYPE_TO_LAYOUT mapping (6 German standType strings → layout type); parseStandSize ("6x4" → {width:6000, depth:4000}); resolve() returns BoothSpec with layout, width, depth, height, finish | | |
| TASK-511 | Implement all 12 MCP tool definitions (`aluvision_info`, `aluvision_build_booth`, `aluvision_create_wall`, `aluvision_create_corner`, `aluvision_create_storage`, `aluvision_create_door`, `aluvision_add_backlit`, `aluvision_arrange_layout`, `aluvision_export_bom`, `aluvision_export_model`, `aluvision_export_drawing`, `aluvision_get_component_info`) with parameter schemas in `aluvision_tools.ts` | | |
| TASK-512 | Implement tool call handlers — Map each tool name to SketchUpClient method; aluvision_build_booth: resolve layout via LayoutEngine, POST to /booth/generate; aluvision_export_bom: GET /booth/bom; etc. | | |
| TASK-513 | Create `Booth3dService` in Pimcore — buildParams(job) collects standType, size, height, flooring, furniture, lead magnets, storage rooms from job; mapStandType (6 German→English layout mapping); getFlooring/getFurniture/getLeadMagnets read from job Materials and field collections | | |
| TASK-514 | Create `GenerateBooth3dMessage` — Symfony Messenger message class with jobId, params (serialized BoothSpec); dispatched by Pimcore controller | | |
| TASK-515 | Create `GenerateBooth3dHandler` — Symfony Messenger handler: receives message, checks job-level lock (Redis key `booth3d:lock:{jobId}` with TTL), calls MCP server (spawns node process with stdio), receives result, stores .skp + PNG in Pimcore DAM, updates job.3dModelPath + job.3dPreviewPath + job.3dBom, releases lock | | |
| TASK-516 | Create `Booth3dController` — `POST /api/messebau/jobs/{id}/generate-3d` dispatches GenerateBooth3dMessage; `GET /api/messebau/jobs/{id}/3d-status` returns generation status (queued/generating/complete/failed) + result paths if complete; `GET /api/messebau/jobs/{id}/3d-preview` returns PNG; `GET /api/messebau/jobs/{id}/3d-model` download link for .skp | | |
| TASK-517 | Add 3D fields to Job DO — `3dModelPath`, `3dPreviewPath`, `3dBom` (JSON); `3dGenerationStatus` (queued/generating/complete/failed); `3dLastGenerated` (datetime) | | |
| TASK-518 | Create `src/components/job-detail/Generate3dButton.tsx` — "3D Booth generieren" button on Tab 0 / DetailHeader; disabled if generation in progress; polling 3d-status every 5s while queued/generating; shows PNG preview in modal on complete; "Regenerieren" button for parameter tweaks; download .skp link; BOM summary alongside preview | | |
| TASK-519 | Configure MCP server in Docker Compose — `aluvision-mcp` service linking to SketchUp bridge URL; environment variables for bridge URL and Aluvision library path | | |
| TASK-520 | Implement MCP server health endpoint — `GET /health` returns MCP status, SketchUp bridge connectivity, last generation timestamp, queue depth | | |
| TASK-521 | Configure Prometheus metrics or simple health check script — `curl http://localhost:9494/health` + `curl http://mcp-server/health` monitored via existing Docker health checks; alert if SketchUp bridge unreachable > 5 minutes | | |
| TASK-522 | Write integration test `slice-6-booth-generation.spec.ts` — create job with Eckstand, 6x4, height 2.5; click "3D Booth generieren"; verify status transitions: queued → generating → complete; verify PNG preview displays in modal; verify BOM shows component count + profile lengths; download .skp file; regenerate with different standType (Inselstand), verify new model created | | |
---
### Cross-Cutting — Polish & Deployment (after core slices)
| Task | Description | Completed | Date |
|------|-------------|-----------|------|
| TASK-901 | Install `sonner` for toast notifications; create `<Toaster>` in App.tsx | | |
| TASK-902 | Add loading skeletons (shadcn/ui) for stats cards, job card grid, detail header, form fields, gallery items | | |
| TASK-903 | Add error boundaries wrapping each major route/view; error message + retry button (queryClient.invalidateQueries) | | |
| TASK-904 | Responsive design — sidebar collapses to hamburger on <768px; form grids stack single column; calendar days reduce height; gallery grid reduces columns | | |
| TASK-905 | Install `vite-plugin-pwa` — service worker for static asset caching; offline indicator; PWA manifest (24HRS Messe-Lotse, yellow+black theme) | | |
| TASK-906 | Create `frontend/Dockerfile` — multi-stage: Node.js build → nginx or Pimcore-served assets | | |
| TASK-907 | Create `.github/workflows/ci.yml` — checkout, setup Node 20, npm ci, lint, typecheck, Vitest, Playwright (Pimcore container as service), build | | |
| TASK-908 | Full manual QA — create job, fill all 8 tabs, upload files, save, reload, verify persistence; test calendar; test PDF export; test duplicate/delete; test mobile viewport; test all 4 roles (viewer, editor, admin, customer) | | |
## 3. Alternatives
- **ALT-001**: The original `architecture-rewrite-1.0.md` plan with 12 sequential phases. Rejected because sequential phasing (backend-complete-before-frontend) is unsuitable for AI-only execution where each task must produce a verifiable working increment.
- **ALT-002**: Monorepo with separate backend/frontend deployment. Rejected in favor of Pimcore-served React SPA (REQ-005) for simplicity — no CORS, session cookie reuse, single domain.
- **ALT-003**: Direct SketchUp bridge calls from Pimcore (synchronous). Rejected per DEC-001 — requires async message queue for reliability, concurrent job locking, and health monitoring.
## 4. Dependencies
- **DEP-001**: Pimcore ≥ 11.x with OAuth2, Gotenberg already running (CON-003)
- **DEP-002**: Symfony Messenger with Redis or Doctrine transport (bundled with Pimcore)
- **DEP-003**: Docker Compose environment (CON-001) — Slice 6 adds MCP server service
- **DEP-004**: SketchUp Pro + Aluvision 3D Library (Slice 6 only)
- **DEP-005**: OpenAI API key for gpt-4o-mini / gpt-4o / gpt-4o vision (Slice 4 only)
- **DEP-006**: `@modelcontextprotocol/sdk` npm package (Slice 6 only)
- **DEP-007**: `react-signature-canvas` npm package (Slice 3 only)
## 5. Files
### Plan Documents (this directory)
- **FILE-001**: `plan/architecture-vertical-slices-1.0.md` — this file
- **FILE-002**: `plan/architecture-rewrite-1.0.md` — original plan (superseded, retained for reference)
- **FILE-003**: `plan/ai-document-skills.md` — AI skill specifications (referenced by Slice 4)
- **FILE-004**: `plan/3d-booth-generation.md` — 3D booth generation specification (referenced by Slice 6)
### Backend (Pimcore)
- **FILE-005**: `bundles/MessebauBundle/` — Symfony bundle directory (created in Slice 1)
- **FILE-006**: `bundles/MessebauBundle/Resources/config/workflows/job.yml` — workflow configuration (created in Slice 3)
- **FILE-007**: `bundles/MessebauBundle/Service/SuggestionService.php` — AI suggestion storage (created in Slice 4)
- **FILE-008**: `bundles/MessebauBundle/EventListener/AiSkillTriggerListener.php` — AI skill trigger (created in Slice 4)
- **FILE-009**: `bundles/MessebauBundle/Service/Booth3dService.php` — 3D parameter builder (created in Slice 6)
- **FILE-010**: `templates/messebau/pdf/export.html.twig` — PDF export template (created in Slice 3)
### Frontend (React)
- **FILE-011**: `frontend/` — Vite + React project directory (created in Slice 1)
- **FILE-012**: `frontend/src/App.tsx` — application routes
- **FILE-013**: `frontend/src/api/client.ts` — Axios client with auth interceptors
- **FILE-014**: `frontend/src/stores/authStore.ts` — authentication state
- **FILE-015**: `frontend/src/stores/uiStore.ts` — UI state
- **FILE-016**: `frontend/src/hooks/usePermissions.ts` — role-based permission flags
- **FILE-017**: `frontend/src/components/` — all React components per slice
### 3D Pipeline (Slice 6 only)
- **FILE-018**: `sketchup-aluvision-bridge/` — SketchUp Ruby extension
- **FILE-019**: `aluvision-mcp-server/` — Node.js MCP server
## 6. Testing
### E2E (Playwright)
- **TEST-001**: `slice-1-auth-create-fair.spec.ts` — auth flow, entity creation, role gating
- **TEST-002**: `slice-2-jobs-crud.spec.ts` — job CRUD, relations, file upload, role scoping
- **TEST-003**: `slice-3-full-workflow.spec.ts` — 8-state workflow, all tabs, PDF export, calendar
- **TEST-004**: `slice-3-calendar.spec.ts` — calendar event rendering, navigation
- **TEST-005**: `slice-4-ai-pipeline.spec.ts` — document upload → classification → extraction → suggestion → apply/ignore
- **TEST-006**: `slice-4-setup-progress.spec.ts` — photo comparison, confidence threshold, auto-check safety
- **TEST-007**: `slice-5-fair-resources.spec.ts` — demand/assignment/gap flow, job-scoped resources
- **TEST-008**: `slice-5-accommodation-travel.spec.ts` — accommodation + travel assignment
- **TEST-009**: `slice-6-booth-generation.spec.ts` — async 3D generation, preview, BOM, regeneration
### Unit (Vitest)
- **TEST-010**: `authStore.test.ts` — login, logout, token refresh
- **TEST-011**: `usePermissions.test.ts` — role → flag mapping
- **TEST-012**: `MasterDataTab.test.tsx` — form validation, date inheritance display
- **TEST-013**: `useJobs.test.ts` — fetch, filter, pagination
### Backend (PHPUnit)
- **TEST-014**: FairLocationControllerTest, CustomerControllerTest, FairControllerTest, JobControllerTest
- **TEST-015**: WorkflowTest, WorkflowTransitionListenerTest (force transition guard bypass)
- **TEST-016**: SuggestionServiceTest (apply, ignore, deduplication)
## 7. Risks & Assumptions
- **RISK-001**: AI-only execution without human developers — all code must be independently verifiable by Playwright tests. Each slice ships with E2E tests that prove functionality. Slice quality gates on test pass.
- **RISK-002**: LLM operational cost may exceed budget (DEC-004). Mitigation: resolve INVESTIGATE card before Slice 4 begins. If cost is prohibitive, defer non-critical vision skills (BrandConsistencyChecker, LeadMagnetIdeator) to a later release.
- **RISK-003**: SketchUp must be running on a dedicated machine for 3D generation (Slice 6). Mitigation: health monitoring + alerting (TASK-521); generation is a nice-to-have, not blocking earlier slices.
- **RISK-004**: Aluvision 3D Library component naming may not match expected patterns. Mitigation: TASK-501 audits the library before any implementation begins; AluvisionRegistry acts as adapter layer.
- **ASSUMPTION-001**: Pimcore, Gotenberg, and OAuth2 are already operational in the Docker Compose environment (CON-003).
- **ASSUMPTION-002**: The index.html prototype accurately represents the required feature set for Slices 1-3. Any deviations discovered during implementation are documented as changelog entries against the prototype.
- **ASSUMPTION-003**: All 12 AI skills defined in `plan/ai-document-skills.md` are in scope for Slice 4. If INVESTIGATE card reveals cost concerns, Tier 3 skills may be deferred.
## 8. Related Specifications / Further Reading
- [Architecture Rewrite 1.0 (Original Plan)](architecture-rewrite-1.0.md) — superseded by this document, retained for detailed backend task breakdowns
- [AI Document Processing Skills](ai-document-skills.md) — full skill catalog with extraction strategies, prompts, and routing matrix
- [3D Booth Generation Plan](3d-booth-generation.md) — SketchUp + Aluvision + MCP architecture, tool catalog, layout engine spec
- [CFC2026 Briefing Documents](documents/) — source material for AI extraction testing
- [AI Skill Prompts](prompts/) — LLM prompt templates for each skill
+350
View File
@@ -0,0 +1,350 @@
---
goal: DataObject Structure Summary — Reuse Strategy for 24HRS Messe-Lotse
version: 1.2
date_created: 2026-06-10
last_updated: 2026-06-16
owner: 24HRS Team
status: Planned
tags: architecture, data-model, pimcore, reuse
---
# Introduction
![Status: Planned](https://img.shields.io/badge/status-Planned-blue)
Consolidated DataObject structures for the MesseLotse rewrite. Defines which existing Pimcore DataObjects are reused from `rkcFairBundle` and `dgmMediaBaseBundle`, what fields are added, and what new entities are created in `MessebauBundle`.
**Reuse strategy:** Extend `fair` and `fairLocation` in `rkcFairBundle` with MesseLotse-specific fields. Reuse `Person` from `dgmMediaBaseBundle` as the contact/worker entity. All trade-fair-management logic lives in the new `MessebauBundle`.
---
## 1. fairLocation — `rkcFairBundle` (extend existing)
| Field | Type | Notes |
|------------------|----------------------------|----------------------------------|
| active | bool | existing |
| swidentifier | input | existing |
| omitSync | bool | existing |
| modified | datetime | existing |
| address | input | existing |
| imagesKey | input | existing |
| vaAssetId | input | existing |
| image1–5 | image | existing |
| boxOptionsTree | input | existing |
| boxes | relation | existing |
| **+ name** | **input** | **new — locale name, e.g. "Messe Frankfurt"** |
| **+ googleMapsUrl** | **input** | **new** |
| **+ notes** | **textarea** | **new** |
| **+ contacts** | **ContactAssignment[] (FC)** | **new — see §1a** |
### 1a. FieldCollection: ContactAssignment
| Field | Type | Notes |
|-----------|--------------------|------------------------------------|
| person | many-to-one → Person | reuses dgmMediaBaseBundle Person |
| role | select | technical / billing / operations / general |
| validFrom | date | |
| validTo | date | nullable (null = indefinite) |
| notes | textarea | |
*Same person+role+timeframe pattern as WorkerAssignment (§6c), scoped to fairLocation.*
---
## 2. fair — `rkcFairBundle` (extend existing)
| Field | Type | Notes |
|--------------------|----------------------------|----------------------------------|
| name | input | existing |
| swidentifier | input | existing |
| active | bool | existing |
| omitSync | bool | existing |
| modified | datetime | existing |
| location | many-to-one → fairLocation | existing |
| setupStartDate | datetime | existing |
| fairStartDate | datetime | existing |
| fairEndDate | datetime | existing |
| destructEndDate | datetime | existing |
| imagesKey | input | existing |
| vaAssetId | input | existing |
| image1–5 | image | existing |
| boxOptionsTree | input | existing |
| boxes | relation | existing |
| assignedArticles | relation | existing |
| **+ setupEndDate** | **datetime** | **new** |
| **+ destructStartDate** | **datetime** | **new** |
| **+ hallsUsed** | **input** | **new — free text, e.g. "Halle 5.1, 6, 8"** |
| **+ notes** | **textarea** | **new** |
| **+ regulations** | **many-to-many → Asset** | **new — simple relation, no metadata** |
| **+ documents** | **FairDocument[] (FieldCollection)** | **new — see §2a** |
### 2a. FieldCollection: FairDocument
| Field | Type | Notes |
|--------------|------------------------|----------------------------------------------------|
| asset | many-to-one → Asset | the file |
| documentType | select | `constructionBriefing` / `floorPlan` / `protocol` |
| date | datetime | protocol date or revision date |
| hallName | input | hall reference for floor plans |
| remarks | textarea | optional |
---
## 3. Person — `dgmMediaBaseBundle` (reuse as-is)
| Field | Type | Notes |
|---------------------|-------------------------|------------------------------------|
| firstname | input | existing |
| lastname | input | existing |
| email | input | existing |
| company | input | existing |
| salutation | select | existing |
| gender | select | existing |
| additionalAddresses | mediaBaseAddress[] (FC) | existing — contains telefonNumber, eMailAddress, faxNumber, full address |
| ... +28 other fields | | existing |
Used as relation target for:
- `fairLocation.contactPerson`
- `Customer.contactPerson` (future)
- `WorkerAssignment.person` (future, Slice 5 of `architecture-vertical-slices-1.0.md`)
---
## 4. Customer — `MessebauBundle` (new DO)
*Deferred — an existing Customer DO lives in a bundle not currently accessible. This section will be updated when that bundle is available.*
| Field | Type | Notes |
|-------------------|-------------------------|------------------------------------|
| company | input | |
| address | input | |
| contactPerson | many-to-one → Person | reuses dgmMediaBaseBundle Person |
| brandDoc | document | company-wide brand guidelines |
| consultationDocs | document[] (gallery) | multi-fair planning docs |
| contractDocs | document[] (gallery) | framework agreements |
| notes | textarea | |
---
## 5. Job — `MessebauBundle` (new DO)
### 5a. Master data
| Field | Type | Notes |
|--------------|-----------------------------|----------------------------------------------------|
| project | input | display name |
| customer | many-to-one → Customer | |
| fair | many-to-one → fair | rkcFairBundle |
| standType | select | `Systemstand` / `Reihstand` / `Eckstand` / `Kopfstand` / `Blockstand` / `Inselstand` |
| standSize | input | e.g. "6x4" |
| standHeight | numeric | meters |
| hall | input | e.g. "Halle 5.1 / Stand C12" |
| area | numeric | m² |
| workflowState| input | managed by Symfony Workflow |
| notes | textarea | |
### 5b. Date overrides (all nullable — fallback to `fair`)
| Field | Type |
|------------------------|----------|
| setupStartOverride | datetime |
| setupEndOverride | datetime |
| eventStartOverride | datetime |
| eventEndOverride | datetime |
| teardownStartOverride | datetime |
| teardownEndOverride | datetime |
### 5c. Asset fields
| Field | Type | Notes |
|-------------------------|--------------|--------------------------------------------|
| standImage | image | single booth photo |
| standGallery | image[] | gallery |
| briefingCustomerDoc | document | per-job override; null → inherit from customer |
| briefingPlanDoc | document | per-job override; null → inherit from fair floorPlan |
| briefingAdditionalDoc | document | |
| powerConfirmation | document | |
| wlanConfirmation | document | |
| cleaningConfirmation | document | |
| riggingQuote | document | |
| riggingDrawing | document | |
| riggingMesseConfirmation| document | |
| standApprovalDoc | document | |
| acceptancePhotos | image[] | |
| acceptanceSignature | image | |
| weeklyProtocols | document[] | |
### 5d. FieldCollections
| FieldCollection | Fields |
|----------------------|--------------------------------------------------------------------------------------------|
| 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 (int, EUR), activation (int, EUR), furniture (int, EUR), logistics (int, EUR) |
| Material | type (flooring/standsystem/hardware/furniture), name, quantity, notes — **multi** |
| ChecklistItem | text, checked, order — **multi** |
---
## 6. Resource Pool Entities — `MessebauBundle` (Slice 5)
*All person references reuse `Person` from dgmMediaBaseBundle.*
### 6a. Tree-Based Assignment Pattern
Assignment and booking entities are standalone DataObjects placed in the Pimcore tree under a `fair` or `Job` instance. Tree location expresses scope — no nullable `job` relation field is needed.
```
/Fairs/OMR2026/
├── ResourceAssignments/
│ ├── WA-pool-01 (fair-pool worker)
│ ├── PVA-pool-01 (fair-pool vehicle)
│ └── ...
├── AccommodationBookings/
│ ├── Hotel-Central/
│ │ └── Occupants/
│ │ ├── Anna-Müller
│ │ └── Max-Schmidt
│ └── Airbnb-Loft/
│ └── Occupants/
│ └── Lisa-Weber
├── Travel/
│ ├── Bahn-HH-Berlin/
│ │ └── Passengers/
│ │ ├── Anna-Müller
│ │ └── Max-Schmidt
│ └── Mietwagen-Frankfurt/
│ └── Passengers/
│ └── Lisa-Weber
├── Jobs/
│ ├── BoothBrevo/
│ │ └── ResourceAssignments/
│ │ ├── WA-001 (job-dedicated worker)
│ │ └── PVA-001 (job-dedicated vehicle)
│ └── BoothDmexco/
│ └── ResourceAssignments/
│ └── WA-002 (job-dedicated worker)
```
**Scope**: parent folder is `.../ResourceAssignments/` under `fair` → fair-pool (available for all jobs). Parent folder is `.../Jobs/{job}/ResourceAssignments/` → dedicated to that specific job. Bookings live directly under fair.
**Aggregation queries** (fair-level "how many Baristas?"):
```php
$listing = new WorkerAssignment\Listing();
$listing->setCondition("o_path LIKE '/Fairs/OMR2026/%'");
$allAssignments = $listing->load();
```
This returns both fair-pool and all job-dedicated assignments in one query. For scoped queries:
```php
// Fair-pool only
"o_path LIKE '/Fairs/OMR2026/ResourceAssignments/%'"
// Specific job only
"o_path LIKE '/Fairs/OMR2026/Jobs/BoothBrevo/ResourceAssignments/%'"
```
### 6b. Pool Entities (shared resource catalog)
| DO | Fields | Tree location |
|---|---|---|
| WorkerRole | name (Barista / Rigger / LorryDriver / FairWorker / Electrician / Cleaner) | `/ResourcePool/WorkerRoles/` |
| PoolVehicle | name, type, licensePlate, state (owned/hired/toHire), notes | `/ResourcePool/Vehicles/` |
| Equipment | name, description, state (owned/rented/toRent), notes | `/ResourcePool/Equipment/` |
| Accommodation | name, address, bookingReference, type (hotel/apartment/other), notes | `/ResourcePool/Accommodations/` |
### 6c. Worker + Vehicle + Equipment Assignments (standalone DOs, tree-scoped)
| DO | Fields | Tree location |
|---|---|---|
| WorkerAssignment | person → Person, role → WorkerRole, state (select: internal / hired / toHire, default from WorkerRole), startDate (datetime), endDate (datetime), offerDoc (document, nullable, external only), contractDoc (document, nullable, external only), billingDoc (document, nullable, external only), notes (textarea) | `/Fairs/{fair}/ResourceAssignments/` or `/Fairs/{fair}/Jobs/{job}/ResourceAssignments/` |
| PoolVehicleAssignment | poolVehicle → PoolVehicle, startDate (datetime), endDate (datetime), notes | same pattern |
| EquipmentAssignment | equipment → Equipment, startDate (datetime), endDate (datetime), notes | same pattern |
### 6d. Accommodation — Booking + Occupants Pattern
One real-world booking covers N persons. Documents (offer/contract/billing) live on the booking parent. Occupants are tree children.
**AccommodationBooking** (parent)
| Field | Type | Notes |
|---|---|---|
| accommodation | → Accommodation | |
| checkIn | date | |
| checkOut | date | |
| confirmationNumber | input | |
| offerDoc | document | nullable |
| contractDoc | document | nullable |
| billingDoc | document | nullable |
| totalCost | numeric (EUR) | nullable, for split billing |
| notes | textarea | |
Tree: `/Fairs/{fair}/AccommodationBookings/`
**AccommodationOccupant** (tree child of booking)
| Field | Type | Notes |
|---|---|---|
| person | → Person | |
| costShare | numeric (EUR) | nullable, for split billing |
Tree: `/Fairs/{fair}/AccommodationBookings/{booking}/Occupants/`
### 6e. Travel — Booking + Passengers Pattern
Same pattern as accommodation: one booking covers N travelers. Documents on the parent, passengers as children.
**TravelBooking** (parent)
| Field | Type | Notes |
|---|---|---|
| transportMethod | select | train / plane / rentalVan / ownCar |
| origin | input | |
| destination | input | |
| departureDateTime | datetime | |
| arrivalDateTime | datetime | |
| returnDepartureDateTime | datetime | nullable |
| returnArrivalDateTime | datetime | nullable |
| bookingReference | input | |
| offerDoc | document | nullable |
| contractDoc | document | nullable |
| billingDoc | document | nullable |
| totalCost | numeric (EUR) | nullable |
| notes | textarea | |
Tree: `/Fairs/{fair}/Travel/`
**TravelPassenger** (tree child of booking)
| Field | Type | Notes |
|---|---|---|
| person | → Person | |
| costShare | numeric (EUR) | nullable |
Tree: `/Fairs/{fair}/Travel/{booking}/Passengers/`
---
## 7. Bundle Responsibility Split
| Bundle | Owns | Reuses |
|-----------------------|-----------------------------------------------------|-----------------------------------|
| `rkcFairBundle` | fair + fairLocation (extended) | — |
| `dgmMediaBaseBundle` | Person | — |
| `MessebauBundle` | Customer, Job, ContactAssignment FC, FairDocument FC, all Job FCs, resource pool entities | Person, fair, fairLocation |
---
## 8. Related Documents
- [architecture-vertical-slices-1.0.md](architecture-vertical-slices-1.0.md) — implementation plan with 6 AI-sized vertical slices
- [architecture-rewrite-1.0.md](architecture-rewrite-1.0.md) — original plan (superseded, retained for reference)
- [ai-document-skills.md](ai-document-skills.md) — AI skill catalog and field routing matrix
- [3d-booth-generation.md](3d-booth-generation.md) — SketchUp + Aluvision + MCP pipeline
Binary file not shown.
@@ -0,0 +1,245 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/PageMode /UseNone /Pages 16 0 R /Type /Catalog
>>
endobj
15 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20251208211515+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20251208211515+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
16 0 obj
<<
/Count 10 /Kids [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R ] /Type /Pages
>>
endobj
17 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 682
>>
stream
GasJN>Aoub'Z],&.F'0-)UC<p5i6T*CS`B.)Q$))?6C4#>%0DEUhW*lNlc.-3_]ljcLpB#3<D0'UAM*;!94bWble:YBaGtk!Mg6fUqNB`EJb/N$Y(%^)#==]i\75lEq@SY2a+/`/k'=Wl&I5c&<X"AV5lNlZ8`Cm6ih!&hE4pnPJo9t/Z/L:dsX!`?[RTnUtj(*GrL(pr:U0>B1P6R/6EGm:2,p4`@Q:,(6\kHQf0s%07oPpP.8(W@fJd@Hi0E6Q[`tm-UDu)aM?b]G?J2rd-ZZF*5W\T6($n1]p,Y-B97Ue%\r79q>C:2rJ("n&U-c9O=StO*kZ3@"q=7qQZe/,!km?R*sB!*U-dKkF`"39W1r"=!fQBHWm#dIl39.-/!"=V<<7BrelDH';IuU2e?*aOjiSYL)@BF[d5g5BP2(ib'teP;Js/_IV)e9;b0ViabdOUB=-thE7#br@h>A+;DiG.3#M0Z1,S5r8:3SB4qg@L\@:!"UL=nM3PN>jKhR.f%=P?F`p?`*$E9HMgI"1?^W?RV6)re]F[U`LC252k\`Yo%+2q*%a;-+Y.:p>#M,^_XQ"A`1C5hE-mk>$5>/5DN&,eX*W[B7U:5b+0Hn-i?THF+u4H4*t3\)1FG5%O*P2j!B?;WRA/T&AlOjgihrLSGGLGjBGG]%-/N9nV:UTnii&~>endstream
endobj
18 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 621
>>
stream
GasbX9lldX&;KZOMEQ*Nb_56pcnXcARATMJ%7ad@OU25E.j1D^q[+L^ng't?"*f)umB^Kr@R0e*k.3VUgnjN%*WmaNmK'YU62C`Adbo>"X](%;?kNLl5Hors?Dc7,+Yi!o0(5eGRW2J^J;PaK3+?iZ'2T>8f)[We[DuY]?t?L[6M8fq8pH`-ObTVJdNa/=Oe'X$elRqS+MGL.$QL??BY:ji2m6B?]4UZ+*s/DI@qpNfQk\2.ZG%oe/=.iib?Y8jflaUdZf\+MePPpkH9]SFUY'*#5+;.-O1G/&-b+j^d9U0b8n%il*sPZMSGFqj[p\VN$m.4ZM\4aGAfci;/Wd.L"Jc!l`)D`-LLP*2.bg<?8[U4_+(a2OP4PZROXI]ID6dKJ195?O';T[Cg6C,lX,8Hp4q=(Y)1+?o[b9(Wd[un+6W:cd_Rt(6$CTV9GUV-d47)QT(uOGS/]t4L/)Os,A\toX;t?fIH/!Q3r%Z3"o;Ku&&9=RZN#j>_pKnG^*_h-MflC>qb8Ah"Ca;Mi+/:XPRH9&h3$OBY_aS%YO`\*(qBJ1jFLXjiE?lRMP&RE4CD!gM6bJ`#SI5-q!-S)/%.DVWGf"<U`Y+SFhM@fdJD_M.2Ibh~>endstream
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 712
>>
stream
Gat%_9lldX&;KZQ'mg>Q"H)u]S!-"aVOb)Lef;5.Zj%6gVaA=mr;*QqE9rV>R1=j(B_Qf0?*+rsF><MopAgZahL8kJ+t30#Os#>81V>@V58J'mbb?4a74SRf`WETk<g6+tTj-*mJdC]GV=^@0&YU1`BlP1%TT%oALO(=:h8'mI"rp=5pgEaHR8Mj.nK!KG<r"7O=4G\]5#]3-AU.s#=_bo\25(bW*J4#W_8f/?KJ[(eap\JY<$9];i)'(NS@l2q1-J,*QqWfcOsl>h`6@p')7W-:SZFYeg^>E))koO[1<qYT2lE!>EHjUE(OVsRG7J**[.ftnampF;94FSk#hrJr8hVXR.$(%9K-g/$PXX^$+',PLU(DWZN'r(c9._a'Zmbk`>QTmK\;DnP/3Mn>n/jW:HY_C9fBaMQRo#(H%!?ouoF&@7jk*KZa52;6N`oM$O6R4b5[+d:r+Uh[\./&=:R+g_O*-4m*ZmTT65l2$"iV'9d]bG1Ni4t<!5=_<T%)S92F.[KcdeGkak.;D/Y9Z2?Yf"'8E'^nFkt>D6>&Tk'Y%0Kc./a2E817Fd`bQkX\3TZ&m4Alre%BIU>&?<]l*B>>1]BDn#[Bu"8elMjf#L:(#<90X%7E:QAuhWCF@.g4<se\fP!4E4lkkPNPDSQ;Kr*uSC>IijlB)9lF)k!g7JmRcJM(^qp9$H2#*OBT:rIO)A^'f`6Ur/~>endstream
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 776
>>
stream
GarnU_/c#!&A@rkp=(6+@U"HBfKI2fe'9p5Zse:?,%(j06DTdC)h39;fX,N/%g)p^mb>3hT3s@NM[e","^%ZV]48W9Ug%6s1ktWD5,FI8'6]=.pbZ.E-%eY'VJMYO.?Cq#_mZNkKY[S=;L1udS=g)%CC_6N.6KYb9DA>e2ir$N^db$I8BY-Ma1=[[JiJ\`JjUKo27+5sG]n'-kmp>BHU;*(H@o1e>b@,SNsf<"GrG$3Eu6m]4f=FPVo<&mC$SIM38*Pig7ZFskR.V5>:e^=l#jO:Sn_-_$K?"gE"2!W%S?[e^Lbe:/t*f'>A$frq!dL,\Ar7JY[)f3lCBVU1pFe/N=g;kq@)L7'kPspa(9Q1-Qakc]n'ZDA'_t6G)iE`MoLnG-lp$82[DnblZZd>\t\G82pNPe![WcYOVGJXl9PkpATaN+&+TBc;*O(eS_2*IXKo<Z\"G!adC=hO8j$%s%rg!%C/ZQ<QhOe9bu4K,?meYT;ELt=Ul6*FIItdd^dh!#CDK6cCa"J/dp!fHlk?(Xm\'<"l4L,0'$kpaH`,A,$!G[R*me(Io#Y4@;2L/=/[VR<fLqQ:&F2e[@7*\dJ"^Qjk.RqCQ]L+AgF"@g8/@7e@I't*;+0h$%&11Qk:F7j.]*h=lJj/H@a!u2@I5>nm=f9pf##q4dtnosSY;`^o[I(iZj*71dA\__>Sr,!;MW7ZhTI4V=.VRZ7g53#,)`lDDKH-dJ-25sB^D9%b"pQW3"(aZ@CAPf]:@EcJ;#YIg;!J!^uQ$:4'L\~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 647
>>
stream
Garo?;/b/B&BE]('_b.r_QX[di!l^69eDgB7XLNn@Zhu+i],.WMV:k`ZRN+i6WU9:GPAl0h\S)9cshIMC4K:c+4go<LL^W@*/BV<_P^r6HbeJo,<h.MI*NjpfU0pY3RpuOikqW1O9.M%;L(ugS><gUbfL<H0g)j1kO$G6<fh>C_+(l86A#tCK.te869:X6!KX-ThZB]=jUk:,DBS`YaTolYNRiTh_YNO/Y.nq#3-@K&l7b?O`*o4h*>sKb7bcL9<@]$U]D2R!M6?A&AT8.QVUP?93S(:M"e8D;_'SL3!@<+hK/h)T_qA-':6#JYe7A/jZu/S9DXC@!Pa)J^?I?VfFe]2cdF]8iih_EW/hRF$':7@n8i;=(Y-K^m1^IR?(1iCB8@OYiJCi)(\jg!%0*OE)GJS>b2At&?H_eTgn83LmLZIi?I5b=IR^LYa57YGANV\$YH3'3"Sn6cP;Fm1@gsVZPWh[:G4FgFqj7RD/crr8HDrJ6(lI8]If+g[*Ud5]3PscO5EE/FEk9_]`BurZeh@*H%PYG2Pks`>Sh9RCs!\/n%])c:_e-HsON9+)s!Y9/oAr!Bfl`Ftl7u<PE^lQK3DssViW.d=T>KA*W.88Q27n2O?O&mr(.glf55pEmaQXK:eOi\O@~>endstream
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 875
>>
stream
Gas1]966RV&AJ$Cln*D@:eP24k>leiVN^)fL1r8F">$2G,*bPYafk.?5NB[&#)6Q_n$OY_9^#sa1Mm,X&U_9(^c4_BYeUVh\q@%sn9;Z+.mr`Y$bg"&$^o$NiI^c#H.&%%0d$V-"/YAo!HNeW!Tsr1(jPOXL!&cJ1OOtFo+6B72YR(e;Bbad5\15kOA02Vc_f'J@6/rdjrodU,D_p6qVjpAU?`J$IcqjZH`+oV:RiUeFZOhS';oUrhFV3rgF?]U='=4VSukea?g*<$0q6EYE2U"SG%/o504Ccdhm$Ot_ue+MBS>9J57>nS7E3GR,VaMt`nWQnNSu5X_luPu5=#-G$<8YaaMh$b<?OC:asClRD/B^EA+J4kBc[oAb&'=A?lf1m9ZQ5^"/*Hi6u0'S'"(.;Zg$-\4JWncF<p0-XJF-LQ]+NVWQJ`P;F)YekCA`>eA@R^lsXcl)Cp_@'umm)M[c2?CMhm_\#uA<+R=,Q_EH?@7!,Ag+TA%Z0WclE\.o>$]'V'=SbDDK=%+aB]m,F[GOl]fp&<\ZJeCKLXilQE'B2k3bbK2Cd7Wm+mKIY"W3)-HZ(MRn"*a\]-k71QoKC>bUEnj%JW+[j[%gtoa!u+7g7Y8&esJNPVel[CCpX06[1?B_B*/W,TkPq%LrPFn/er,=<I9gDVnLc@4\\"lHT1L%76kLb3(_H\T7Ec'k0V&LeE2?X6q0V-gV.)l\3a=;i2A;ZN+qeGmC$jE)3:GQ(@GAc_i(IK4<?Tp2f8COh&M_IQYfoS(q$abXMM8d9e'hZl.J%>H5Qg%.T^_TUcX<4;J!F4.sO?SE\,d:[].?&<8tkOO5[4qbY9O$ns'dkehrt9It'gh[1`?'_[d=`rrBTBU?q~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 584
>>
stream
GasJN6$(a6&;BTNMEO!P6]eQ-jOU,\c4FTN_+.jc]s^pWRBGVJrV'S(#_=mtdFaf9Idc[h#oN7Grh$Y"pF4lM"GdW;&k"R6+FSo]l/:_i*,eTid<$GT"XI#$'(b@Xqm\c^AZ>r.&?eoJD]'BIaoc8d(W7P#Ot>.$:L2B:XQoEeUd$"#jJ8CCr'rB"B5ci`<5^>23[/`hY;UQ=-VG#Pd2:Z]*XNa=-'Gt5Jpp[Kf4bJX8.nHJYI--2NNe`%A<Q;KC_B/T5rK/S<`gK,XBRfS5'K3(:1L*&j;'U`_-1"f-(?'Z3aJI5&)k.K0;o:GR8&j#?m07-b:$ACg!(9*/+r_@F<50s+o?'L%7AC>f@bb8Q+F2Lp2<X8n%=V;@I`jWn-c((_rraa?TQm]8)@@_JHE$F4a'h<L@?J1C8B!:.IqR0rjMTnO*`](USpXn;87$\eY(?8f"M7B'9q!\CUhosiXhh7;eoqrS%e(p60U`7;FQXa=j*R=prr9-Y[nt&>CLK,PJ\78Ebq]Qk:r:P\P-8T>HGpQ<bMfI[O6XS#D[rM/(s!:W"g,:mkOlIP^.-93r(K)$anjr[f~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 500
>>
stream
Garo>6$&ML'SPrS'mj\YNpTr'5hoR+'490->3Ls(^sAYROt2Bbj'U[k83.DC)9m-Jg\piJ&6SH7j1"<)D3!qQJ64$\5fCh_q01WWZemS/3gm(9a_&3u8/&6BRq0Dt^1O)6kJaGBi\kJRGAHmYDQE<[)OJPYO4'4lZ<Q5pEiL%[g1Oo2=d$Kcre12bT`3m@!BE`GccESr&UcI/H'-X(F<T'H=k1/UHQq6f_r?7@A?BZodW7B$9@[ItHQ6l7`UQM[_D>>/92_AZ(bVVRS9"Jd<TfB?lcJ-*[UP7t/+8+hT;+-SWC`k^EU;K>B+^t[e[j\Ad<;<B=_'_3oq'g>1KA2p^R/,9nmC+MWpupmKElCuH_Hm)>Uo,5EeYV0JtjR(\[YaPPpOk]\2n]aRXL"k8C$b+*$+c]MC;)BEDBJZ/k-<skPDiJbY,-EeSqMa?n?@(*S(^#AfQqL__Uu[aP_W+LRj^A4UfoZ.3@UQ:.$6fs50H!fj>b6aqk~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 641
>>
stream
GatUo;/_pX&BE]".IJp]2MQcJe$irF3iUBA<$$<>iBl<:40qY^p!`Br&=R.c<,&X'LqEQamd.\+`3`WkY<%2&$(nse?jW'5?s/^[Gn49*aj1841@ld6kK05[OgBkgo/Y>u3"BBG:.TqqU/^!3?p[aWnC#KSM[5VKRch'mBR84G,Jo)nWc>ds(Jm:^q8"k:d!C>6c-dj)#qhXd*lqq7'BY*6p<XaPDpZ94Xi6b'o_C%3""i*f:jn!d+Ok(+)^FR568`kAElYYR!`)ull`)eb<$PT)lD:\<YD$:?YiDO[)E^\I?0VWjoO[0]-X"Pq<"[msjrb(u,cP,=S=g;CC1W)F70aS3p!f+Y;81HFJ?a^sKS"s5O`ZMIGHM^e!D8,#>-62F=s9!K\k7n)P=)q-pJ3c5YWHqH%.5BPqGZ$BLJU^O.SYd']':IR^2@RK<id]_cQ=-E6L1EA5*E^5RuL`kdJP#S^f!->WRV[Ob_'R."=N\EYP)?d,@A0?SAMDU<LO]$A/;ob4jJG8VN>,*8OfR"?[H:)_SNo4O5m,nPc-f]rb%BUmEMpKl5>;hHE634mI67qL6PQnkiS#fG2.$A]XM3bB_>AD)Z8d#WYLk>/KIuX47Uf07d"N9*MTl5%$TRe83@d~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 829
>>
stream
Gasam9i'M/&A@Zck(H&96C\SLj$qN`-&C7^MZ`N]D3(7,p9$KhL;L._X92$4i_llk=k]9hDr-sd&,L[dk!(I!('.ZFH/K,S8AQ8<7M+@l1-#)Pik7eWK:Y:HbWM9f?I,,:1X>#r:@u!&33*1#l>eP\#$f2KlSjSSkY->B=f7C\cf#RUFXE*5H%_#R=fjGYB+R0lnAXAcjDtIYT1n"DrG_p'hLKYtrP-09D_cl^U*rj?_kHP&EUI;Y#1T>a+p&BQ87ieDbMO%t6l"bX!XWGNSjP&Ti-33`P*ar-4J9&1.V"S@.FjY*.DRd*Y?]'k"=VEm&;kfuaFD[W`/K$\nQ/k6&;6U03YcG9?+;Dl2-m^,),di8KAd]\5"d82Z3;fSih6'X`6(H#R8h<qX\*:Z(6K_9,$r1XacTL.reClVd@La7&g&!Fh%4CWU,1g9@%n5l@4]s>Xm'o:@GbuZI5pHjg\$89&fqYQXAFb%W?B`F;[VK=2UY4":dKi%d&A4(o8JjgT5V\i^S['hEbE3,T#-If)ucAb7..:'K&7[i@:"0i8j)V&:,<!ppSop*YbPrbN-%$!:)r;53#ZdAGh,@HRRAXsMKJCqQ*40-H*3QmEaikUd_Z%S!$.0]k#1!b4+b)mOY!<$<kO\c]7X=u<Jn#Z7#i%Mmad@uQ$%]Gh5ns?hT<p1ht*Y=HT\_P[oMM&<i4;UpR[s$!.,U]<4R4Q=dpXu#LO'i&S6eK'umm>p+c!eaDp\&Uu?8J,+)+K02O.b!me4-bCWg-b<,?9(_44T?9+WG0o(*A;Rd/?erH2W;Bdh3o;%58PfE+WE4t5k&!;jV=T~>endstream
endobj
xref
0 27
0000000000 65535 f
0000000073 00000 n
0000000114 00000 n
0000000221 00000 n
0000000333 00000 n
0000000538 00000 n
0000000743 00000 n
0000000948 00000 n
0000001153 00000 n
0000001358 00000 n
0000001563 00000 n
0000001769 00000 n
0000001975 00000 n
0000002181 00000 n
0000002387 00000 n
0000002457 00000 n
0000002741 00000 n
0000002860 00000 n
0000003633 00000 n
0000004345 00000 n
0000005148 00000 n
0000006015 00000 n
0000006753 00000 n
0000007719 00000 n
0000008394 00000 n
0000008985 00000 n
0000009717 00000 n
trailer
<<
/ID
[<a3bfbe3bbd08cbb2043a97ba518d1956><a3bfbe3bbd08cbb2043a97ba518d1956>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 15 0 R
/Root 14 0 R
/Size 27
>>
startxref
10637
%%EOF
@@ -0,0 +1,245 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/PageMode /UseNone /Pages 16 0 R /Type /Catalog
>>
endobj
15 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20251208211515+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20251208211515+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
16 0 obj
<<
/Count 10 /Kids [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R ] /Type /Pages
>>
endobj
17 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 682
>>
stream
GasJN>Aoub'Z],&.F'0-)UC<p5i6T*CS`B.)Q$))?6C4#>%0DEUhW*lNlc.-3_]ljcLpB#3<D0'UAM*;!94bWble:YBaGtk!Mg6fUqNB`EJb/N$Y(%^)#==]i\75lEq@SY2a+/`/k'=Wl&I5c&<X"AV5lNlZ8`Cm6ih!&hE4pnPJo9t/Z/L:dsX!`?[RTnUtj(*GrL(pr:U0>B1P6R/6EGm:2,p4`@Q:,(6\kHQf0s%07oPpP.8(W@fJd@Hi0E6Q[`tm-UDu)aM?b]G?J2rd-ZZF*5W\T6($n1]p,Y-B97Ue%\r79q>C:2rJ("n&U-c9O=StO*kZ3@"q=7qQZe/,!km?R*sB!*U-dKkF`"39W1r"=!fQBHWm#dIl39.-/!"=V<<7BrelDH';IuU2e?*aOjiSYL)@BF[d5g5BP2(ib'teP;Js/_IV)e9;b0ViabdOUB=-thE7#br@h>A+;DiG.3#M0Z1,S5r8:3SB4qg@L\@:!"UL=nM3PN>jKhR.f%=P?F`p?`*$E9HMgI"1?^W?RV6)re]F[U`LC252k\`Yo%+2q*%a;-+Y.:p>#M,^_XQ"A`1C5hE-mk>$5>/5DN&,eX*W[B7U:5b+0Hn-i?THF+u4H4*t3\)1FG5%O*P2j!B?;WRA/T&AlOjgihrLSGGLGjBGG]%-/N9nV:UTnii&~>endstream
endobj
18 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 621
>>
stream
GasbX9lldX&;KZOMEQ*Nb_56pcnXcARATMJ%7ad@OU25E.j1D^q[+L^ng't?"*f)umB^Kr@R0e*k.3VUgnjN%*WmaNmK'YU62C`Adbo>"X](%;?kNLl5Hors?Dc7,+Yi!o0(5eGRW2J^J;PaK3+?iZ'2T>8f)[We[DuY]?t?L[6M8fq8pH`-ObTVJdNa/=Oe'X$elRqS+MGL.$QL??BY:ji2m6B?]4UZ+*s/DI@qpNfQk\2.ZG%oe/=.iib?Y8jflaUdZf\+MePPpkH9]SFUY'*#5+;.-O1G/&-b+j^d9U0b8n%il*sPZMSGFqj[p\VN$m.4ZM\4aGAfci;/Wd.L"Jc!l`)D`-LLP*2.bg<?8[U4_+(a2OP4PZROXI]ID6dKJ195?O';T[Cg6C,lX,8Hp4q=(Y)1+?o[b9(Wd[un+6W:cd_Rt(6$CTV9GUV-d47)QT(uOGS/]t4L/)Os,A\toX;t?fIH/!Q3r%Z3"o;Ku&&9=RZN#j>_pKnG^*_h-MflC>qb8Ah"Ca;Mi+/:XPRH9&h3$OBY_aS%YO`\*(qBJ1jFLXjiE?lRMP&RE4CD!gM6bJ`#SI5-q!-S)/%.DVWGf"<U`Y+SFhM@fdJD_M.2Ibh~>endstream
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 712
>>
stream
Gat%_9lldX&;KZQ'mg>Q"H)u]S!-"aVOb)Lef;5.Zj%6gVaA=mr;*QqE9rV>R1=j(B_Qf0?*+rsF><MopAgZahL8kJ+t30#Os#>81V>@V58J'mbb?4a74SRf`WETk<g6+tTj-*mJdC]GV=^@0&YU1`BlP1%TT%oALO(=:h8'mI"rp=5pgEaHR8Mj.nK!KG<r"7O=4G\]5#]3-AU.s#=_bo\25(bW*J4#W_8f/?KJ[(eap\JY<$9];i)'(NS@l2q1-J,*QqWfcOsl>h`6@p')7W-:SZFYeg^>E))koO[1<qYT2lE!>EHjUE(OVsRG7J**[.ftnampF;94FSk#hrJr8hVXR.$(%9K-g/$PXX^$+',PLU(DWZN'r(c9._a'Zmbk`>QTmK\;DnP/3Mn>n/jW:HY_C9fBaMQRo#(H%!?ouoF&@7jk*KZa52;6N`oM$O6R4b5[+d:r+Uh[\./&=:R+g_O*-4m*ZmTT65l2$"iV'9d]bG1Ni4t<!5=_<T%)S92F.[KcdeGkak.;D/Y9Z2?Yf"'8E'^nFkt>D6>&Tk'Y%0Kc./a2E817Fd`bQkX\3TZ&m4Alre%BIU>&?<]l*B>>1]BDn#[Bu"8elMjf#L:(#<90X%7E:QAuhWCF@.g4<se\fP!4E4lkkPNPDSQ;Kr*uSC>IijlB)9lF)k!g7JmRcJM(^qp9$H2#*OBT:rIO)A^'f`6Ur/~>endstream
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 776
>>
stream
GarnU_/c#!&A@rkp=(6+@U"HBfKI2fe'9p5Zse:?,%(j06DTdC)h39;fX,N/%g)p^mb>3hT3s@NM[e","^%ZV]48W9Ug%6s1ktWD5,FI8'6]=.pbZ.E-%eY'VJMYO.?Cq#_mZNkKY[S=;L1udS=g)%CC_6N.6KYb9DA>e2ir$N^db$I8BY-Ma1=[[JiJ\`JjUKo27+5sG]n'-kmp>BHU;*(H@o1e>b@,SNsf<"GrG$3Eu6m]4f=FPVo<&mC$SIM38*Pig7ZFskR.V5>:e^=l#jO:Sn_-_$K?"gE"2!W%S?[e^Lbe:/t*f'>A$frq!dL,\Ar7JY[)f3lCBVU1pFe/N=g;kq@)L7'kPspa(9Q1-Qakc]n'ZDA'_t6G)iE`MoLnG-lp$82[DnblZZd>\t\G82pNPe![WcYOVGJXl9PkpATaN+&+TBc;*O(eS_2*IXKo<Z\"G!adC=hO8j$%s%rg!%C/ZQ<QhOe9bu4K,?meYT;ELt=Ul6*FIItdd^dh!#CDK6cCa"J/dp!fHlk?(Xm\'<"l4L,0'$kpaH`,A,$!G[R*me(Io#Y4@;2L/=/[VR<fLqQ:&F2e[@7*\dJ"^Qjk.RqCQ]L+AgF"@g8/@7e@I't*;+0h$%&11Qk:F7j.]*h=lJj/H@a!u2@I5>nm=f9pf##q4dtnosSY;`^o[I(iZj*71dA\__>Sr,!;MW7ZhTI4V=.VRZ7g53#,)`lDDKH-dJ-25sB^D9%b"pQW3"(aZ@CAPf]:@EcJ;#YIg;!J!^uQ$:4'L\~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 647
>>
stream
Garo?;/b/B&BE]('_b.r_QX[di!l^69eDgB7XLNn@Zhu+i],.WMV:k`ZRN+i6WU9:GPAl0h\S)9cshIMC4K:c+4go<LL^W@*/BV<_P^r6HbeJo,<h.MI*NjpfU0pY3RpuOikqW1O9.M%;L(ugS><gUbfL<H0g)j1kO$G6<fh>C_+(l86A#tCK.te869:X6!KX-ThZB]=jUk:,DBS`YaTolYNRiTh_YNO/Y.nq#3-@K&l7b?O`*o4h*>sKb7bcL9<@]$U]D2R!M6?A&AT8.QVUP?93S(:M"e8D;_'SL3!@<+hK/h)T_qA-':6#JYe7A/jZu/S9DXC@!Pa)J^?I?VfFe]2cdF]8iih_EW/hRF$':7@n8i;=(Y-K^m1^IR?(1iCB8@OYiJCi)(\jg!%0*OE)GJS>b2At&?H_eTgn83LmLZIi?I5b=IR^LYa57YGANV\$YH3'3"Sn6cP;Fm1@gsVZPWh[:G4FgFqj7RD/crr8HDrJ6(lI8]If+g[*Ud5]3PscO5EE/FEk9_]`BurZeh@*H%PYG2Pks`>Sh9RCs!\/n%])c:_e-HsON9+)s!Y9/oAr!Bfl`Ftl7u<PE^lQK3DssViW.d=T>KA*W.88Q27n2O?O&mr(.glf55pEmaQXK:eOi\O@~>endstream
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 875
>>
stream
Gas1]966RV&AJ$Cln*D@:eP24k>leiVN^)fL1r8F">$2G,*bPYafk.?5NB[&#)6Q_n$OY_9^#sa1Mm,X&U_9(^c4_BYeUVh\q@%sn9;Z+.mr`Y$bg"&$^o$NiI^c#H.&%%0d$V-"/YAo!HNeW!Tsr1(jPOXL!&cJ1OOtFo+6B72YR(e;Bbad5\15kOA02Vc_f'J@6/rdjrodU,D_p6qVjpAU?`J$IcqjZH`+oV:RiUeFZOhS';oUrhFV3rgF?]U='=4VSukea?g*<$0q6EYE2U"SG%/o504Ccdhm$Ot_ue+MBS>9J57>nS7E3GR,VaMt`nWQnNSu5X_luPu5=#-G$<8YaaMh$b<?OC:asClRD/B^EA+J4kBc[oAb&'=A?lf1m9ZQ5^"/*Hi6u0'S'"(.;Zg$-\4JWncF<p0-XJF-LQ]+NVWQJ`P;F)YekCA`>eA@R^lsXcl)Cp_@'umm)M[c2?CMhm_\#uA<+R=,Q_EH?@7!,Ag+TA%Z0WclE\.o>$]'V'=SbDDK=%+aB]m,F[GOl]fp&<\ZJeCKLXilQE'B2k3bbK2Cd7Wm+mKIY"W3)-HZ(MRn"*a\]-k71QoKC>bUEnj%JW+[j[%gtoa!u+7g7Y8&esJNPVel[CCpX06[1?B_B*/W,TkPq%LrPFn/er,=<I9gDVnLc@4\\"lHT1L%76kLb3(_H\T7Ec'k0V&LeE2?X6q0V-gV.)l\3a=;i2A;ZN+qeGmC$jE)3:GQ(@GAc_i(IK4<?Tp2f8COh&M_IQYfoS(q$abXMM8d9e'hZl.J%>H5Qg%.T^_TUcX<4;J!F4.sO?SE\,d:[].?&<8tkOO5[4qbY9O$ns'dkehrt9It'gh[1`?'_[d=`rrBTBU?q~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 584
>>
stream
GasJN6$(a6&;BTNMEO!P6]eQ-jOU,\c4FTN_+.jc]s^pWRBGVJrV'S(#_=mtdFaf9Idc[h#oN7Grh$Y"pF4lM"GdW;&k"R6+FSo]l/:_i*,eTid<$GT"XI#$'(b@Xqm\c^AZ>r.&?eoJD]'BIaoc8d(W7P#Ot>.$:L2B:XQoEeUd$"#jJ8CCr'rB"B5ci`<5^>23[/`hY;UQ=-VG#Pd2:Z]*XNa=-'Gt5Jpp[Kf4bJX8.nHJYI--2NNe`%A<Q;KC_B/T5rK/S<`gK,XBRfS5'K3(:1L*&j;'U`_-1"f-(?'Z3aJI5&)k.K0;o:GR8&j#?m07-b:$ACg!(9*/+r_@F<50s+o?'L%7AC>f@bb8Q+F2Lp2<X8n%=V;@I`jWn-c((_rraa?TQm]8)@@_JHE$F4a'h<L@?J1C8B!:.IqR0rjMTnO*`](USpXn;87$\eY(?8f"M7B'9q!\CUhosiXhh7;eoqrS%e(p60U`7;FQXa=j*R=prr9-Y[nt&>CLK,PJ\78Ebq]Qk:r:P\P-8T>HGpQ<bMfI[O6XS#D[rM/(s!:W"g,:mkOlIP^.-93r(K)$anjr[f~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 500
>>
stream
Garo>6$&ML'SPrS'mj\YNpTr'5hoR+'490->3Ls(^sAYROt2Bbj'U[k83.DC)9m-Jg\piJ&6SH7j1"<)D3!qQJ64$\5fCh_q01WWZemS/3gm(9a_&3u8/&6BRq0Dt^1O)6kJaGBi\kJRGAHmYDQE<[)OJPYO4'4lZ<Q5pEiL%[g1Oo2=d$Kcre12bT`3m@!BE`GccESr&UcI/H'-X(F<T'H=k1/UHQq6f_r?7@A?BZodW7B$9@[ItHQ6l7`UQM[_D>>/92_AZ(bVVRS9"Jd<TfB?lcJ-*[UP7t/+8+hT;+-SWC`k^EU;K>B+^t[e[j\Ad<;<B=_'_3oq'g>1KA2p^R/,9nmC+MWpupmKElCuH_Hm)>Uo,5EeYV0JtjR(\[YaPPpOk]\2n]aRXL"k8C$b+*$+c]MC;)BEDBJZ/k-<skPDiJbY,-EeSqMa?n?@(*S(^#AfQqL__Uu[aP_W+LRj^A4UfoZ.3@UQ:.$6fs50H!fj>b6aqk~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 641
>>
stream
GatUo;/_pX&BE]".IJp]2MQcJe$irF3iUBA<$$<>iBl<:40qY^p!`Br&=R.c<,&X'LqEQamd.\+`3`WkY<%2&$(nse?jW'5?s/^[Gn49*aj1841@ld6kK05[OgBkgo/Y>u3"BBG:.TqqU/^!3?p[aWnC#KSM[5VKRch'mBR84G,Jo)nWc>ds(Jm:^q8"k:d!C>6c-dj)#qhXd*lqq7'BY*6p<XaPDpZ94Xi6b'o_C%3""i*f:jn!d+Ok(+)^FR568`kAElYYR!`)ull`)eb<$PT)lD:\<YD$:?YiDO[)E^\I?0VWjoO[0]-X"Pq<"[msjrb(u,cP,=S=g;CC1W)F70aS3p!f+Y;81HFJ?a^sKS"s5O`ZMIGHM^e!D8,#>-62F=s9!K\k7n)P=)q-pJ3c5YWHqH%.5BPqGZ$BLJU^O.SYd']':IR^2@RK<id]_cQ=-E6L1EA5*E^5RuL`kdJP#S^f!->WRV[Ob_'R."=N\EYP)?d,@A0?SAMDU<LO]$A/;ob4jJG8VN>,*8OfR"?[H:)_SNo4O5m,nPc-f]rb%BUmEMpKl5>;hHE634mI67qL6PQnkiS#fG2.$A]XM3bB_>AD)Z8d#WYLk>/KIuX47Uf07d"N9*MTl5%$TRe83@d~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 829
>>
stream
Gasam9i'M/&A@Zck(H&96C\SLj$qN`-&C7^MZ`N]D3(7,p9$KhL;L._X92$4i_llk=k]9hDr-sd&,L[dk!(I!('.ZFH/K,S8AQ8<7M+@l1-#)Pik7eWK:Y:HbWM9f?I,,:1X>#r:@u!&33*1#l>eP\#$f2KlSjSSkY->B=f7C\cf#RUFXE*5H%_#R=fjGYB+R0lnAXAcjDtIYT1n"DrG_p'hLKYtrP-09D_cl^U*rj?_kHP&EUI;Y#1T>a+p&BQ87ieDbMO%t6l"bX!XWGNSjP&Ti-33`P*ar-4J9&1.V"S@.FjY*.DRd*Y?]'k"=VEm&;kfuaFD[W`/K$\nQ/k6&;6U03YcG9?+;Dl2-m^,),di8KAd]\5"d82Z3;fSih6'X`6(H#R8h<qX\*:Z(6K_9,$r1XacTL.reClVd@La7&g&!Fh%4CWU,1g9@%n5l@4]s>Xm'o:@GbuZI5pHjg\$89&fqYQXAFb%W?B`F;[VK=2UY4":dKi%d&A4(o8JjgT5V\i^S['hEbE3,T#-If)ucAb7..:'K&7[i@:"0i8j)V&:,<!ppSop*YbPrbN-%$!:)r;53#ZdAGh,@HRRAXsMKJCqQ*40-H*3QmEaikUd_Z%S!$.0]k#1!b4+b)mOY!<$<kO\c]7X=u<Jn#Z7#i%Mmad@uQ$%]Gh5ns?hT<p1ht*Y=HT\_P[oMM&<i4;UpR[s$!.,U]<4R4Q=dpXu#LO'i&S6eK'umm>p+c!eaDp\&Uu?8J,+)+K02O.b!me4-bCWg-b<,?9(_44T?9+WG0o(*A;Rd/?erH2W;Bdh3o;%58PfE+WE4t5k&!;jV=T~>endstream
endobj
xref
0 27
0000000000 65535 f
0000000073 00000 n
0000000114 00000 n
0000000221 00000 n
0000000333 00000 n
0000000538 00000 n
0000000743 00000 n
0000000948 00000 n
0000001153 00000 n
0000001358 00000 n
0000001563 00000 n
0000001769 00000 n
0000001975 00000 n
0000002181 00000 n
0000002387 00000 n
0000002457 00000 n
0000002741 00000 n
0000002860 00000 n
0000003633 00000 n
0000004345 00000 n
0000005148 00000 n
0000006015 00000 n
0000006753 00000 n
0000007719 00000 n
0000008394 00000 n
0000008985 00000 n
0000009717 00000 n
trailer
<<
/ID
[<a3bfbe3bbd08cbb2043a97ba518d1956><a3bfbe3bbd08cbb2043a97ba518d1956>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 15 0 R
/Root 14 0 R
/Size 27
>>
startxref
10637
%%EOF
@@ -0,0 +1,245 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/PageMode /UseNone /Pages 16 0 R /Type /Catalog
>>
endobj
15 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20251208211515+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20251208211515+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
16 0 obj
<<
/Count 10 /Kids [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R ] /Type /Pages
>>
endobj
17 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 682
>>
stream
GasJN>Aoub'Z],&.F'0-)UC<p5i6T*CS`B.)Q$))?6C4#>%0DEUhW*lNlc.-3_]ljcLpB#3<D0'UAM*;!94bWble:YBaGtk!Mg6fUqNB`EJb/N$Y(%^)#==]i\75lEq@SY2a+/`/k'=Wl&I5c&<X"AV5lNlZ8`Cm6ih!&hE4pnPJo9t/Z/L:dsX!`?[RTnUtj(*GrL(pr:U0>B1P6R/6EGm:2,p4`@Q:,(6\kHQf0s%07oPpP.8(W@fJd@Hi0E6Q[`tm-UDu)aM?b]G?J2rd-ZZF*5W\T6($n1]p,Y-B97Ue%\r79q>C:2rJ("n&U-c9O=StO*kZ3@"q=7qQZe/,!km?R*sB!*U-dKkF`"39W1r"=!fQBHWm#dIl39.-/!"=V<<7BrelDH';IuU2e?*aOjiSYL)@BF[d5g5BP2(ib'teP;Js/_IV)e9;b0ViabdOUB=-thE7#br@h>A+;DiG.3#M0Z1,S5r8:3SB4qg@L\@:!"UL=nM3PN>jKhR.f%=P?F`p?`*$E9HMgI"1?^W?RV6)re]F[U`LC252k\`Yo%+2q*%a;-+Y.:p>#M,^_XQ"A`1C5hE-mk>$5>/5DN&,eX*W[B7U:5b+0Hn-i?THF+u4H4*t3\)1FG5%O*P2j!B?;WRA/T&AlOjgihrLSGGLGjBGG]%-/N9nV:UTnii&~>endstream
endobj
18 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 621
>>
stream
GasbX9lldX&;KZOMEQ*Nb_56pcnXcARATMJ%7ad@OU25E.j1D^q[+L^ng't?"*f)umB^Kr@R0e*k.3VUgnjN%*WmaNmK'YU62C`Adbo>"X](%;?kNLl5Hors?Dc7,+Yi!o0(5eGRW2J^J;PaK3+?iZ'2T>8f)[We[DuY]?t?L[6M8fq8pH`-ObTVJdNa/=Oe'X$elRqS+MGL.$QL??BY:ji2m6B?]4UZ+*s/DI@qpNfQk\2.ZG%oe/=.iib?Y8jflaUdZf\+MePPpkH9]SFUY'*#5+;.-O1G/&-b+j^d9U0b8n%il*sPZMSGFqj[p\VN$m.4ZM\4aGAfci;/Wd.L"Jc!l`)D`-LLP*2.bg<?8[U4_+(a2OP4PZROXI]ID6dKJ195?O';T[Cg6C,lX,8Hp4q=(Y)1+?o[b9(Wd[un+6W:cd_Rt(6$CTV9GUV-d47)QT(uOGS/]t4L/)Os,A\toX;t?fIH/!Q3r%Z3"o;Ku&&9=RZN#j>_pKnG^*_h-MflC>qb8Ah"Ca;Mi+/:XPRH9&h3$OBY_aS%YO`\*(qBJ1jFLXjiE?lRMP&RE4CD!gM6bJ`#SI5-q!-S)/%.DVWGf"<U`Y+SFhM@fdJD_M.2Ibh~>endstream
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 712
>>
stream
Gat%_9lldX&;KZQ'mg>Q"H)u]S!-"aVOb)Lef;5.Zj%6gVaA=mr;*QqE9rV>R1=j(B_Qf0?*+rsF><MopAgZahL8kJ+t30#Os#>81V>@V58J'mbb?4a74SRf`WETk<g6+tTj-*mJdC]GV=^@0&YU1`BlP1%TT%oALO(=:h8'mI"rp=5pgEaHR8Mj.nK!KG<r"7O=4G\]5#]3-AU.s#=_bo\25(bW*J4#W_8f/?KJ[(eap\JY<$9];i)'(NS@l2q1-J,*QqWfcOsl>h`6@p')7W-:SZFYeg^>E))koO[1<qYT2lE!>EHjUE(OVsRG7J**[.ftnampF;94FSk#hrJr8hVXR.$(%9K-g/$PXX^$+',PLU(DWZN'r(c9._a'Zmbk`>QTmK\;DnP/3Mn>n/jW:HY_C9fBaMQRo#(H%!?ouoF&@7jk*KZa52;6N`oM$O6R4b5[+d:r+Uh[\./&=:R+g_O*-4m*ZmTT65l2$"iV'9d]bG1Ni4t<!5=_<T%)S92F.[KcdeGkak.;D/Y9Z2?Yf"'8E'^nFkt>D6>&Tk'Y%0Kc./a2E817Fd`bQkX\3TZ&m4Alre%BIU>&?<]l*B>>1]BDn#[Bu"8elMjf#L:(#<90X%7E:QAuhWCF@.g4<se\fP!4E4lkkPNPDSQ;Kr*uSC>IijlB)9lF)k!g7JmRcJM(^qp9$H2#*OBT:rIO)A^'f`6Ur/~>endstream
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 776
>>
stream
GarnU_/c#!&A@rkp=(6+@U"HBfKI2fe'9p5Zse:?,%(j06DTdC)h39;fX,N/%g)p^mb>3hT3s@NM[e","^%ZV]48W9Ug%6s1ktWD5,FI8'6]=.pbZ.E-%eY'VJMYO.?Cq#_mZNkKY[S=;L1udS=g)%CC_6N.6KYb9DA>e2ir$N^db$I8BY-Ma1=[[JiJ\`JjUKo27+5sG]n'-kmp>BHU;*(H@o1e>b@,SNsf<"GrG$3Eu6m]4f=FPVo<&mC$SIM38*Pig7ZFskR.V5>:e^=l#jO:Sn_-_$K?"gE"2!W%S?[e^Lbe:/t*f'>A$frq!dL,\Ar7JY[)f3lCBVU1pFe/N=g;kq@)L7'kPspa(9Q1-Qakc]n'ZDA'_t6G)iE`MoLnG-lp$82[DnblZZd>\t\G82pNPe![WcYOVGJXl9PkpATaN+&+TBc;*O(eS_2*IXKo<Z\"G!adC=hO8j$%s%rg!%C/ZQ<QhOe9bu4K,?meYT;ELt=Ul6*FIItdd^dh!#CDK6cCa"J/dp!fHlk?(Xm\'<"l4L,0'$kpaH`,A,$!G[R*me(Io#Y4@;2L/=/[VR<fLqQ:&F2e[@7*\dJ"^Qjk.RqCQ]L+AgF"@g8/@7e@I't*;+0h$%&11Qk:F7j.]*h=lJj/H@a!u2@I5>nm=f9pf##q4dtnosSY;`^o[I(iZj*71dA\__>Sr,!;MW7ZhTI4V=.VRZ7g53#,)`lDDKH-dJ-25sB^D9%b"pQW3"(aZ@CAPf]:@EcJ;#YIg;!J!^uQ$:4'L\~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 647
>>
stream
Garo?;/b/B&BE]('_b.r_QX[di!l^69eDgB7XLNn@Zhu+i],.WMV:k`ZRN+i6WU9:GPAl0h\S)9cshIMC4K:c+4go<LL^W@*/BV<_P^r6HbeJo,<h.MI*NjpfU0pY3RpuOikqW1O9.M%;L(ugS><gUbfL<H0g)j1kO$G6<fh>C_+(l86A#tCK.te869:X6!KX-ThZB]=jUk:,DBS`YaTolYNRiTh_YNO/Y.nq#3-@K&l7b?O`*o4h*>sKb7bcL9<@]$U]D2R!M6?A&AT8.QVUP?93S(:M"e8D;_'SL3!@<+hK/h)T_qA-':6#JYe7A/jZu/S9DXC@!Pa)J^?I?VfFe]2cdF]8iih_EW/hRF$':7@n8i;=(Y-K^m1^IR?(1iCB8@OYiJCi)(\jg!%0*OE)GJS>b2At&?H_eTgn83LmLZIi?I5b=IR^LYa57YGANV\$YH3'3"Sn6cP;Fm1@gsVZPWh[:G4FgFqj7RD/crr8HDrJ6(lI8]If+g[*Ud5]3PscO5EE/FEk9_]`BurZeh@*H%PYG2Pks`>Sh9RCs!\/n%])c:_e-HsON9+)s!Y9/oAr!Bfl`Ftl7u<PE^lQK3DssViW.d=T>KA*W.88Q27n2O?O&mr(.glf55pEmaQXK:eOi\O@~>endstream
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 875
>>
stream
Gas1]966RV&AJ$Cln*D@:eP24k>leiVN^)fL1r8F">$2G,*bPYafk.?5NB[&#)6Q_n$OY_9^#sa1Mm,X&U_9(^c4_BYeUVh\q@%sn9;Z+.mr`Y$bg"&$^o$NiI^c#H.&%%0d$V-"/YAo!HNeW!Tsr1(jPOXL!&cJ1OOtFo+6B72YR(e;Bbad5\15kOA02Vc_f'J@6/rdjrodU,D_p6qVjpAU?`J$IcqjZH`+oV:RiUeFZOhS';oUrhFV3rgF?]U='=4VSukea?g*<$0q6EYE2U"SG%/o504Ccdhm$Ot_ue+MBS>9J57>nS7E3GR,VaMt`nWQnNSu5X_luPu5=#-G$<8YaaMh$b<?OC:asClRD/B^EA+J4kBc[oAb&'=A?lf1m9ZQ5^"/*Hi6u0'S'"(.;Zg$-\4JWncF<p0-XJF-LQ]+NVWQJ`P;F)YekCA`>eA@R^lsXcl)Cp_@'umm)M[c2?CMhm_\#uA<+R=,Q_EH?@7!,Ag+TA%Z0WclE\.o>$]'V'=SbDDK=%+aB]m,F[GOl]fp&<\ZJeCKLXilQE'B2k3bbK2Cd7Wm+mKIY"W3)-HZ(MRn"*a\]-k71QoKC>bUEnj%JW+[j[%gtoa!u+7g7Y8&esJNPVel[CCpX06[1?B_B*/W,TkPq%LrPFn/er,=<I9gDVnLc@4\\"lHT1L%76kLb3(_H\T7Ec'k0V&LeE2?X6q0V-gV.)l\3a=;i2A;ZN+qeGmC$jE)3:GQ(@GAc_i(IK4<?Tp2f8COh&M_IQYfoS(q$abXMM8d9e'hZl.J%>H5Qg%.T^_TUcX<4;J!F4.sO?SE\,d:[].?&<8tkOO5[4qbY9O$ns'dkehrt9It'gh[1`?'_[d=`rrBTBU?q~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 584
>>
stream
GasJN6$(a6&;BTNMEO!P6]eQ-jOU,\c4FTN_+.jc]s^pWRBGVJrV'S(#_=mtdFaf9Idc[h#oN7Grh$Y"pF4lM"GdW;&k"R6+FSo]l/:_i*,eTid<$GT"XI#$'(b@Xqm\c^AZ>r.&?eoJD]'BIaoc8d(W7P#Ot>.$:L2B:XQoEeUd$"#jJ8CCr'rB"B5ci`<5^>23[/`hY;UQ=-VG#Pd2:Z]*XNa=-'Gt5Jpp[Kf4bJX8.nHJYI--2NNe`%A<Q;KC_B/T5rK/S<`gK,XBRfS5'K3(:1L*&j;'U`_-1"f-(?'Z3aJI5&)k.K0;o:GR8&j#?m07-b:$ACg!(9*/+r_@F<50s+o?'L%7AC>f@bb8Q+F2Lp2<X8n%=V;@I`jWn-c((_rraa?TQm]8)@@_JHE$F4a'h<L@?J1C8B!:.IqR0rjMTnO*`](USpXn;87$\eY(?8f"M7B'9q!\CUhosiXhh7;eoqrS%e(p60U`7;FQXa=j*R=prr9-Y[nt&>CLK,PJ\78Ebq]Qk:r:P\P-8T>HGpQ<bMfI[O6XS#D[rM/(s!:W"g,:mkOlIP^.-93r(K)$anjr[f~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 500
>>
stream
Garo>6$&ML'SPrS'mj\YNpTr'5hoR+'490->3Ls(^sAYROt2Bbj'U[k83.DC)9m-Jg\piJ&6SH7j1"<)D3!qQJ64$\5fCh_q01WWZemS/3gm(9a_&3u8/&6BRq0Dt^1O)6kJaGBi\kJRGAHmYDQE<[)OJPYO4'4lZ<Q5pEiL%[g1Oo2=d$Kcre12bT`3m@!BE`GccESr&UcI/H'-X(F<T'H=k1/UHQq6f_r?7@A?BZodW7B$9@[ItHQ6l7`UQM[_D>>/92_AZ(bVVRS9"Jd<TfB?lcJ-*[UP7t/+8+hT;+-SWC`k^EU;K>B+^t[e[j\Ad<;<B=_'_3oq'g>1KA2p^R/,9nmC+MWpupmKElCuH_Hm)>Uo,5EeYV0JtjR(\[YaPPpOk]\2n]aRXL"k8C$b+*$+c]MC;)BEDBJZ/k-<skPDiJbY,-EeSqMa?n?@(*S(^#AfQqL__Uu[aP_W+LRj^A4UfoZ.3@UQ:.$6fs50H!fj>b6aqk~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 641
>>
stream
GatUo;/_pX&BE]".IJp]2MQcJe$irF3iUBA<$$<>iBl<:40qY^p!`Br&=R.c<,&X'LqEQamd.\+`3`WkY<%2&$(nse?jW'5?s/^[Gn49*aj1841@ld6kK05[OgBkgo/Y>u3"BBG:.TqqU/^!3?p[aWnC#KSM[5VKRch'mBR84G,Jo)nWc>ds(Jm:^q8"k:d!C>6c-dj)#qhXd*lqq7'BY*6p<XaPDpZ94Xi6b'o_C%3""i*f:jn!d+Ok(+)^FR568`kAElYYR!`)ull`)eb<$PT)lD:\<YD$:?YiDO[)E^\I?0VWjoO[0]-X"Pq<"[msjrb(u,cP,=S=g;CC1W)F70aS3p!f+Y;81HFJ?a^sKS"s5O`ZMIGHM^e!D8,#>-62F=s9!K\k7n)P=)q-pJ3c5YWHqH%.5BPqGZ$BLJU^O.SYd']':IR^2@RK<id]_cQ=-E6L1EA5*E^5RuL`kdJP#S^f!->WRV[Ob_'R."=N\EYP)?d,@A0?SAMDU<LO]$A/;ob4jJG8VN>,*8OfR"?[H:)_SNo4O5m,nPc-f]rb%BUmEMpKl5>;hHE634mI67qL6PQnkiS#fG2.$A]XM3bB_>AD)Z8d#WYLk>/KIuX47Uf07d"N9*MTl5%$TRe83@d~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 829
>>
stream
Gasam9i'M/&A@Zck(H&96C\SLj$qN`-&C7^MZ`N]D3(7,p9$KhL;L._X92$4i_llk=k]9hDr-sd&,L[dk!(I!('.ZFH/K,S8AQ8<7M+@l1-#)Pik7eWK:Y:HbWM9f?I,,:1X>#r:@u!&33*1#l>eP\#$f2KlSjSSkY->B=f7C\cf#RUFXE*5H%_#R=fjGYB+R0lnAXAcjDtIYT1n"DrG_p'hLKYtrP-09D_cl^U*rj?_kHP&EUI;Y#1T>a+p&BQ87ieDbMO%t6l"bX!XWGNSjP&Ti-33`P*ar-4J9&1.V"S@.FjY*.DRd*Y?]'k"=VEm&;kfuaFD[W`/K$\nQ/k6&;6U03YcG9?+;Dl2-m^,),di8KAd]\5"d82Z3;fSih6'X`6(H#R8h<qX\*:Z(6K_9,$r1XacTL.reClVd@La7&g&!Fh%4CWU,1g9@%n5l@4]s>Xm'o:@GbuZI5pHjg\$89&fqYQXAFb%W?B`F;[VK=2UY4":dKi%d&A4(o8JjgT5V\i^S['hEbE3,T#-If)ucAb7..:'K&7[i@:"0i8j)V&:,<!ppSop*YbPrbN-%$!:)r;53#ZdAGh,@HRRAXsMKJCqQ*40-H*3QmEaikUd_Z%S!$.0]k#1!b4+b)mOY!<$<kO\c]7X=u<Jn#Z7#i%Mmad@uQ$%]Gh5ns?hT<p1ht*Y=HT\_P[oMM&<i4;UpR[s$!.,U]<4R4Q=dpXu#LO'i&S6eK'umm>p+c!eaDp\&Uu?8J,+)+K02O.b!me4-bCWg-b<,?9(_44T?9+WG0o(*A;Rd/?erH2W;Bdh3o;%58PfE+WE4t5k&!;jV=T~>endstream
endobj
xref
0 27
0000000000 65535 f
0000000073 00000 n
0000000114 00000 n
0000000221 00000 n
0000000333 00000 n
0000000538 00000 n
0000000743 00000 n
0000000948 00000 n
0000001153 00000 n
0000001358 00000 n
0000001563 00000 n
0000001769 00000 n
0000001975 00000 n
0000002181 00000 n
0000002387 00000 n
0000002457 00000 n
0000002741 00000 n
0000002860 00000 n
0000003633 00000 n
0000004345 00000 n
0000005148 00000 n
0000006015 00000 n
0000006753 00000 n
0000007719 00000 n
0000008394 00000 n
0000008985 00000 n
0000009717 00000 n
trailer
<<
/ID
[<a3bfbe3bbd08cbb2043a97ba518d1956><a3bfbe3bbd08cbb2043a97ba518d1956>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 15 0 R
/Root 14 0 R
/Size 27
>>
startxref
10637
%%EOF
Binary file not shown.
Binary file not shown.
+98
View File
@@ -0,0 +1,98 @@
# 24HRS Messe-Lotse — Executive Summary
**Date:** 2026-06-10 | **Status:** Planned | **Team:** AI-only execution
---
## The Product
A multi-user web application for managing trade fair booth projects. Replaces the current single-page prototype (`index.html`) with a production system backed by Pimcore, featuring AI-powered document processing, resource planning, and 3D booth generation. Scale: ~300 jobs/year, peaks at 113 jobs per fair.
---
## Delivery Strategy: 6 Vertical Slices
Each slice delivers a complete, independently shippable user capability. No slice waits on a later slice.
```
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Slice 1 │───▶│ Slice 2 │───▶│ Slice 3 │ ← MVP: full trade fair
│ Messe │ │ Jobs │ │ Workflow │ management system
└────────────┘ └────────────┘ └────────────┘
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Slice 4 │ │ Slice 5 │ │ Slice 6 │ ← Intelligence &
│ AI Skills │ │ Resources │ │ 3D Booth │ automation layers
└────────────┘ └────────────┘ └────────────┘
```
| Slice | Capability Delivered | Value |
|---|---|---|
| **1. Messe** | Login, create venues, customers, and trade fairs; dashboard with fair list | Proves entire stack works; first visible UI |
| **2. Jobs** | Create booth projects linked to fairs/customers; master data editing; file upload via Pimcore DAM | Core entity relationships operational; replaces spreadsheet tracking |
| **3. Workflow** | All 8 tabs, 8-state workflow (Angebot→Abgeschlossen), Gotenberg PDF export, calendar view | Complete replacement of the index.html prototype |
| **4. AI Skills** | 12 AI skills process uploaded documents — extract contacts, deadlines, budgets, materials; generate checklist items; detect brand consistency | Eliminates manual data entry from briefing documents |
| **5. Resources** | Worker, vehicle, equipment, accommodation pools with demand/assignment gap analysis; travel management | Replaces ad-hoc team/vehicle lists with structured resource planning |
| **6. 3D Booth** | One-click 3D booth model generation via SketchUp + Aluvision; PNG preview + BOM returned to job | Automated design pipeline from job data to production model |
---
## Architecture at a Glance
```
┌──────────────┐ ┌─────────────────┐ ┌──────────────┐
│ React SPA │────▶│ Pimcore REST │────▶│ MySQL DB │
│ (TypeScript)│ │ API (OAuth2) │ │ │
└──────────────┘ └────────┬────────┘ └──────────────┘
│
┌─────────┼─────────┐
│ │
┌─────▼─────┐ ┌───────▼──────┐
│ Gotenberg │ │ Symfony │
│ (PDF) │ │ Workflow │
└───────────┘ └──────────────┘
```
- 4-entity data model: FairLocation → Fair → Job ← Customer
- 4 user roles: Admin, Editor, Viewer, Customer
- File storage: Pimcore DAM (not localStorage base64)
- AI skills: Symfony Messenger queue with parallel workers
---
## Key Decisions
| Decision | Rationale |
|---|---|
| Vertical slices over sequential phases | AI requires small, verifiable increments; each slice independently shippable |
| Admin workflow override (`force=true`) | Real-world fair prep is messy; guards alone insufficient |
| Async 3D generation via message queue | SketchUp cannot handle sync HTTP from web controllers |
| Tiered document model with graceful fallback | Customer/fair/job-scoped docs, but zero-config for customers without brandbooks |
| LLM cost investigation before Slice 4 | 10,000+ calls/year needs cost modeling before commitment |
---
## Resource Pool (Slice 5) — Justification
The modeling of 11 resource entities is driven by real scale: CFC2026 alone has 113 booths across 113 customers at one fair, requiring coordinated worker (Barista, Rigger, Driver, etc.), vehicle, equipment, accommodation, and travel planning. A simple text field on the job entity would not support fair-level gap analysis.
---
## Risks & Open Items
| Risk | Status | Mitigation |
|---|---|---|
| LLM operational cost may exceed budget | ⚠️ Investigation required | Defer non-critical skills if cost prohibitive |
| SketchUp must be running for 3D generation | ⚠️ Accepted | Health monitoring + alerting; Slice 6 is optional nice-to-have |
| AI-only code quality | ⚠️ Managed | Every slice ships with Playwright E2E tests; quality gate = test pass |
| Workflow guard rigidity vs real-world entropy | 🔬 Investigation | Admin force transition implemented (Slice 3); guard strategy under review |
---
## Timeline
AI-only execution. Slices 1-3 form the MVP and can be verified independently. Slices 4-6 add intelligence and automation on top of the stable core. No external team dependencies; no developer hiring required.
---
*Related: [architecture-vertical-slices-1.0.md](architecture-vertical-slices-1.0.md) (full implementation plan, 368 lines, 100+ tasks)*
Binary file not shown.
+54
View File
@@ -0,0 +1,54 @@
# AI Skill Prompt Files — Index
15 prompt files for all LLM-using AI skills. Each file contains: system prompt, user prompt template
with `{{variable}}` placeholders, output schema, confidence scoring guidance, and examples.
Regex-only skills (SKILL-001 Classifier, SKILL-008 MismatchDetector, SKILL-008a VersionDetector)
do not need prompt files — they use pure pattern matching.
## Prompt File Inventory
### Tier 1 — High Impact
| File | Skill | Model | Notes |
|---|---|---|---|
| `SKILL-001a-DocumentScopeDetector.md` | Scope detection (customer/fair/job) | gpt-4o-mini | Receives upload context + entity IDs |
| `SKILL-002-ContactExtractor.md` | Contact extraction | gpt-4o-mini | Regex primary, LLM fallback |
| `SKILL-003-DeadlineDetector.md` | Deadline detection (German focus) | gpt-4o-mini | Regex primary, LLM fallback for context |
| `SKILL-004-ChecklistGenerator.md` | Action item extraction | gpt-4o-mini | German output, 20-item limit, source quoting |
| `SKILL-004b-MultiFairDetector.md` | Multi-fair detection | gpt-4o-mini | Returns fair array from consultation docs |
| `SKILL-004c-BudgetExtractor.md` | Budget/cost extraction | gpt-4o-mini | Regex primary, LLM for context classification |
| `SKILL-004d-PainPointSummarizer.md` | Pain points + expectations | gpt-4o-mini | German output, structured summary |
### Tier 2 — Validation
| File | Skill | Model | Notes |
|---|---|---|---|
| `SKILL-005-StandDataExtractor.md` | Stand dimension extraction | gpt-4o | Handles multi-booth specs + hall info |
| `SKILL-006-ConfirmationParser.md` | Booking confirmation parsing | gpt-4o-mini | Service-type detection, discrepancy flagging |
| `SKILL-007-BrandConsistencyChecker.md` | Brand vs. photo comparison | gpt-4o (vision) | Compares brandbook palette to booth photos |
### Tier 3 — Enhancement
| File | Skill | Model | Notes |
|---|---|---|---|
| `SKILL-009-MaterialListExtractor.md` | Material/furniture extraction | gpt-4o-mini | Categorizes items into 4 types, German names |
| `SKILL-009a-LeadMagnetIdeator.md` | Activation idea generation | gpt-4o | Creative output, temperature 0.7, 3 ideas |
| `SKILL-010-AcceptanceReportGenerator.md` | Acceptance report generation | gpt-4o | Structured JSON for Gotenberg PDF template |
| `SKILL-011-SetupProgressAnalyzer.md` | Photo progress comparison | gpt-4o (vision) | Detects construction changes, suggests checklist |
| `SKILL-012-WeeklyProtocolSummarizer.md` | Protocol summarization | gpt-4o-mini | German output, timeline-friendly structure |
## Usage
Each prompt file is a Markdown document containing:
1. **Front matter** (YAML): skill ID, name, tier, model, temperature, response format
2. **System Prompt**: instructions for the LLM
3. **User Prompt Template**: the message sent to the LLM, with `{{variable}}` placeholders
that are populated at runtime by the PHP handler with extracted document text,
entity context, and metadata
4. **Output Schema**: the expected JSON structure
5. **Example** (some files): sample input/output pairs for testing
These files are the canonical prompt source. The PHP handler (`AiSkillTriggerListener`)
loads the prompt, replaces `{{placeholders}}` with runtime data, sends to the LLM API,
and parses the response against the schema.
@@ -0,0 +1,64 @@
---
skill: SKILL-001a
name: DocumentScopeDetector
tier: 1
model: gpt-4o-mini
temperature: 0.0
response_format: json_object
---
# System Prompt
You determine the scope of a trade fair document. Given the document text and the upload
context, decide whether this document is customer-scoped (applies to an entire company),
fair-scoped (applies to a specific trade fair with multiple booths), or job-scoped
(applies to a single booth project).
Instructions:
1. Analyze the document text for scope indicators.
2. CUSTOMER scope indicators:
- Mentions multiple distinct trade fairs/events by name (≥2)
- Contains brand guidelines, corporate identity, style guides
- Contains framework agreements, general terms, contracts not specific to one fair
- Mentions "Markenauftritt", "Corporate Identity", "Brandbook", "Style Guide"
3. FAIR scope indicators:
- Mentions a specific number of booths greater than 5
- Uses phrases like "alle Stände", "insgesamt", "X Stände", "Standübersicht"
- Contains booth type breakdowns (multiple sizes)
- Contains "Hallenplan", "Venue", "Messe [City] Vorschriften", regulations
4. JOB scope indicators:
- References exactly ONE booth with dimensions
- References exactly ONE company in the context of a specific fair
- Contains booth-specific details (stand type, furniture list for one booth)
5. If ambiguous, default to "job" with lower confidence.
6. Return ONLY valid JSON.
# User Prompt Template
Determine the scope of this document. It was uploaded to field "{{upload_field}}" on entity "{{upload_entity}}".
Return a JSON object:
```json
{
"suggestedScope": "customer|fair|job",
"suggestedEntityId": "ID of the customer/fair/job this should belong to (or null if cannot determine)",
"reason": "brief explanation in English of why this scope was chosen",
"fairsReferenced": "number of distinct fairs mentioned (0 if none detected)",
"boothsReferenced": "number of booths or booth types mentioned (0 if none)",
"isMultiBooth": true/false,
"isMultiFair": true/false,
"isBrandbook": true/false,
"confidence": 0.0-1.0
}
```
Current entity context:
- customer: {{customer_name}} (ID: {{customer_id}})
- fair: {{fair_name}} (ID: {{fair_id}})
- job: {{job_project}} (ID: {{job_id}})
Document text:
"""
{{document_text}}
"""
+101
View File
@@ -0,0 +1,101 @@
---
skill: SKILL-002
name: ContactExtractor
tier: 1
model: gpt-4o-mini
temperature: 0.0
response_format: json_object
fallback: regex
---
# System Prompt
You are a precise data extraction tool specialized in German and English business documents.
Your sole task is to extract contact information (names, email addresses, phone numbers, roles/titles)
from trade fair and event planning documents.
Instructions:
1. Extract every person mentioned with an email or phone number.
2. If a person has no email/phone but is clearly a contact (e.g. listed under "Ansprechpartner", "Contact"),
include them with confidence 0.60.
3. Normalize phone numbers to E.164 format if possible (e.g. +49 123 456789).
4. For each contact, determine their role if mentioned (e.g. "Marketing Manager", "Event Coordinator").
5. If the document explicitly states the company name, include it.
6. Return ONLY valid JSON — no explanations, no markdown.
# User Prompt Template
Extract all contacts from the following document text. For each contact, return:
- name: full name (string)
- email: email address or null (string)
- phone: phone number in international format or null (string)
- role: job title or null (string)
- company: company name if explicitly mentioned, otherwise null (string)
- confidence: 0.0-1.0 based on how clearly identified this person is as a contact
Return a JSON object with a "contacts" array:
```json
{
"contacts": [
{
"name": "...",
"email": "...",
"phone": "...",
"role": "...",
"company": "...",
"confidence": 0.95
}
]
}
```
Document text:
"""
{{document_text}}
"""
# Output Schema
{
"contacts": [
{
"name": "string",
"email": "string | null",
"phone": "string | null",
"role": "string | null",
"company": "string | null",
"confidence": "number (0.0-1.0)"
}
]
}
# Example
Input text:
"Für Rückfragen wenden Sie sich bitte an Lisa Reinhardt (lisa.reinhardt@brevo.com, +49 30 12345678).
Die Projektleitung übernimmt Max Mustermann (max@eventagentur.de)."
Output:
```json
{
"contacts": [
{
"name": "Lisa Reinhardt",
"email": "lisa.reinhardt@brevo.com",
"phone": "+49 30 12345678",
"role": null,
"company": null,
"confidence": 0.95
},
{
"name": "Max Mustermann",
"email": "max@eventagentur.de",
"phone": null,
"role": null,
"company": null,
"confidence": 0.85
}
]
}
```
+127
View File
@@ -0,0 +1,127 @@
---
skill: SKILL-003
name: DeadlineDetector
tier: 1
model: gpt-4o-mini
temperature: 0.0
response_format: json_object
fallback: regex
---
# System Prompt
You are a deadline extraction tool for trade fair project management. Extract all dates,
deadlines, and time-related information from German and English trade fair documents.
Instructions:
1. Extract every date mentioned in the document.
2. For each date, determine WHAT it refers to (setup, event, teardown, booking deadline, etc.).
3. For date ranges, return both start and end.
4. Recognize German date formats: "01.–02.07.2026", "bis zum 15. Mai 2026", "spätestens 30.06.2026".
5. Recognize time formats: "ab 16:30 Uhr", "09:00–18:00".
6. Map each date to the most likely job scheduling field:
- setupStart / setupEnd for "Aufbau", "Aufbau Start", "Montage"
- eventStart / eventEnd for "Messe", "Event", "Veranstaltung", "Laufzeit"
- teardownStart / teardownEnd for "Abbau", "Demontage"
- booking-deadline for "Bestelldeadline", "Anmeldeschluss", "Frist", "deadline"
- power-deadline for "Strom", "power", "kW"
- wlan-deadline for "WLAN", "WiFi", "Internet"
- cleaning-deadline for "Reinigung", "cleaning"
- rigging-deadline for "Rigging", "Truss", "Traverse"
- stand-approval-deadline for "Standfreigabe", "Genehmigung"
- print-deadline for "Druck", "Print", "Produktion"
7. Return ONLY valid JSON.
# User Prompt Template
Extract all dates and deadlines from the following document. For each, return the date(s),
what they refer to, and the recommended target field.
Return a JSON object with a "dates" array:
```json
{
"dates": [
{
"label": "what this date refers to in German (original phrasing if possible)",
"startDate": "YYYY-MM-DD",
"endDate": "YYYY-MM-DD or null if single date",
"startTime": "HH:MM or null",
"endTime": "HH:MM or null",
"suggestedField": "setupStart|setupEnd|eventStart|eventEnd|teardownStart|teardownEnd|booking-deadline|power-deadline|wlan-deadline|cleaning-deadline|rigging-deadline|stand-approval-deadline|print-deadline|other",
"confidence": 0.0-1.0
}
]
}
```
Document text:
"""
{{document_text}}
"""
# Output Schema
{
"dates": [
{
"label": "string (original German phrasing)",
"startDate": "string (YYYY-MM-DD)",
"endDate": "string | null (YYYY-MM-DD)",
"startTime": "string | null (HH:MM)",
"endTime": "string | null (HH:MM)",
"suggestedField": "string (one of the enum values)",
"confidence": "number (0.0-1.0)"
}
]
}
# Example
Input text:
"Aufbau: 01.–02.07.2026, Event: 03.–04.07.2026, Abbau ab 04.07. 16:30 Uhr.
Strom-Bestelldeadline: 15.05.2026."
Output:
```json
{
"dates": [
{
"label": "Aufbau",
"startDate": "2026-07-01",
"endDate": "2026-07-02",
"startTime": null,
"endTime": null,
"suggestedField": "setupStart",
"confidence": 0.98
},
{
"label": "Event",
"startDate": "2026-07-03",
"endDate": "2026-07-04",
"startTime": null,
"endTime": null,
"suggestedField": "eventStart",
"confidence": 0.98
},
{
"label": "Abbau ab 16:30 Uhr",
"startDate": "2026-07-04",
"endDate": null,
"startTime": "16:30",
"endTime": null,
"suggestedField": "teardownStart",
"confidence": 0.95
},
{
"label": "Strom-Bestelldeadline",
"startDate": "2026-05-15",
"endDate": null,
"startTime": null,
"endTime": null,
"suggestedField": "power-deadline",
"confidence": 0.92
}
]
}
```
@@ -0,0 +1,61 @@
---
skill: SKILL-004
name: ChecklistGenerator
tier: 1
model: gpt-4o-mini
temperature: 0.1
response_format: json_object
---
# System Prompt
You are analyzing a trade fair booth briefing document. Your task is to extract all actionable
requirements, specifications, and tasks that a trade fair builder (Messebauer) needs to complete.
The output language must be German. Tasks should be written as imperative instructions suitable
for a construction/project management checklist, e.g. "Teppichboden in Grau verlegen",
"2 Banner 3×2m mit Ösen produzieren".
Instructions:
1. Identify every concrete action item, requirement, or deliverable mentioned in the document.
2. Each checklist item must be ONE specific, atomic task. Avoid combining multiple tasks.
3. If document mentions quantities, include them in the task text.
4. If document mentions materials or specifications, include them.
5. Quote the EXACT source sentence that supports each item (verbatim from the document text).
6. Rate confidence based on how explicitly the task is stated (not implied).
7. Maximum 20 items. Prioritize the most concrete, specific ones.
8. Return ONLY valid JSON — no explanations.
# User Prompt Template
Extract all actionable tasks and requirements from this trade fair booth briefing document.
Return them as checklist items in German.
Return a JSON object with an "items" array:
```json
{
"items": [
{
"text": "imperative task description in German",
"confidence": 0.0-1.0,
"source": "exact quote from the document text",
"category": "Bau|Grafik|Boden|Möbel|Technik|Logistik|Sonstiges"
}
]
}
```
Categories:
- "Bau" — construction, stand system, walls, frames
- "Grafik" — graphics, prints, branding, signage
- "Boden" — flooring
- "Möbel" — furniture
- "Technik" — screens, lighting, power, AV equipment
- "Logistik" — transport, setup/teardown planning, storage
- "Sonstiges" — other
Document text:
"""
{{document_text}}
"""
@@ -0,0 +1,58 @@
---
skill: SKILL-004b
name: MultiFairDetector
tier: 1
model: gpt-4o-mini
temperature: 0.0
response_format: json_object
---
# System Prompt
You extract trade fair references from customer consultation and briefing documents.
Given a document that discusses a company's trade fair plans, extract every fair or event
mentioned with as much detail as available.
Instructions:
1. Identify every trade fair, expo, conference, or event reference.
2. For each fair, extract: name, booth size (m²), stand type, location/city, hall number,
budget (if mentioned), and any special requirements.
3. If the fair name is abbreviated (e.g. "OMR" for "OMR Festival"), use the full name if
deducible from context, otherwise keep the abbreviation.
4. Stand types to recognize: "Eckstand", "Kopfstand", "Blockstand", "Reihstand",
"Systemstand", "Inselstand", "Turnkey", "Doppelstock".
5. If no booth size is mentioned for a fair, set size to null.
6. Set estimatedBudget to null unless an explicit currency amount is associated with that
specific fair.
7. Return ONLY valid JSON.
# User Prompt Template
Extract all trade fair/event references from this consultation document. For each fair,
return the details available.
Return a JSON object with a "fairs" array:
```json
{
"fairs": [
{
"name": "full fair/event name",
"size": "booth size in m² (number or null)",
"standType": "Eckstand|Kopfstand|Blockstand|Reihstand|Systemstand|Inselstand|Turnkey|Doppelstock|null",
"city": "city name or null",
"hall": "hall number or name or null",
"estimatedBudget": "number in EUR or null",
"notes": "any special requirements or notes",
"confidence": 0.0-1.0
}
],
"totalFairsDetected": 5,
"customerHint": "company name if mentioned in the document or null"
}
```
Document text:
"""
{{document_text}}
"""
@@ -0,0 +1,59 @@
---
skill: SKILL-004c
name: BudgetExtractor
tier: 1
model: gpt-4o-mini
temperature: 0.0
response_format: json_object
fallback: regex
---
# System Prompt
You extract budget and cost information from trade fair planning documents.
Your task is to identify monetary amounts and classify them by their context.
Instructions:
1. Find every monetary amount mentioned (EUR, €, Euro, k€).
2. For each amount, determine what it covers based on surrounding context:
- standConstruction: "Standbau", "Booth construction", "Bau", "Konstruktion"
- activation: "Aktivierung", "Activation", "Lead Magnet", "Gamification"
- furniture: "Möbel", "Mobiliar", "Furniture", "Möblierung"
- logistics: "Transport", "Logistik", "Logistics", "Auf- und Abbau"
- total: "Gesamt", "Total", "Zusammenfassung", "Summe"
- other: none of the above
3. Normalize amounts to EUR integers. Handle k€ notation (5k€ = 5000).
4. Return ONLY valid JSON.
# User Prompt Template
Extract all budget/cost information from this trade fair document.
Classify each amount by what it covers.
Return a JSON object with an "amounts" array:
```json
{
"amounts": [
{
"value": "integer (EUR)",
"currency": "EUR",
"category": "standConstruction|activation|furniture|logistics|total|other",
"source": "the original phrase that contains this amount",
"confidence": 0.0-1.0
}
],
"summary": {
"standConstruction": "integer|null",
"activation": "integer|null",
"furniture": "integer|null",
"logistics": "integer|null",
"total": "integer|null"
}
}
```
Document text:
"""
{{document_text}}
"""
@@ -0,0 +1,48 @@
---
skill: SKILL-004d
name: PainPointSummarizer
tier: 1
model: gpt-4o-mini
temperature: 0.2
response_format: json_object
---
# System Prompt
You are a trade fair project analyst. Given a customer consultation summary or briefing
document, extract structured insights about the customer's challenges and expectations.
The output language must be German.
Instructions:
1. Extract all pain points (Schmerzpunkte) — problems, complaints, frustrations the customer
has experienced with previous trade fair appearances.
2. Extract all expectations (Erwartungen) — what the customer explicitly wants, needs, or
hopes for from the trade fair builder.
3. Generate a concise structured summary (3-5 sentences in German) that captures the key
takeaways: who the customer is, what fairs they're planning, what they need, and any
critical requirements.
4. Each pain point and expectation should be ONE clear bullet point in German.
5. Return ONLY valid JSON.
# User Prompt Template
Analyze this customer consultation document. Extract:
1. Pain points / challenges (Schmerzpunkte) as short bullet points in German
2. Customer expectations (Erwartungen) as short bullet points in German
3. A short structured summary (3-5 German sentences) capturing key takeaways
Return a JSON object:
```json
{
"painPoints": ["string", "string", ...],
"expectations": ["string", "string", ...],
"structuredSummary": "string (3-5 German sentences)"
}
```
Document text:
"""
{{document_text}}
"""
@@ -0,0 +1,61 @@
---
skill: SKILL-005
name: StandDataExtractor
tier: 2
model: gpt-4o
temperature: 0.0
response_format: json_object
---
# System Prompt
You extract booth/stand specifications from trade fair floor plans and construction briefings.
Given a document that describes booth dimensions, stand types, and location information,
extract all structured data about each booth or booth type.
Instructions:
1. Extract booth dimensions (width × depth / Breite × Tiefe in meters).
2. Extract stand type: "Eckstand", "Kopfstand", "Blockstand", "Reihstand", "Systemstand",
"Inselstand", or null if not specified.
3. Extract hall number and stand number (e.g. "Halle 5.1, Stand C12").
4. Extract total area in m² if explicitly stated.
5. Extract booth height if specified (in meters).
6. If multiple booth types are described, return all of them.
7. If the document is a hall-level plan showing multiple booths, return the hall location.
8. Return ONLY valid JSON.
# User Prompt Template
Extract all booth/stand specifications from this trade fair floor plan or construction briefing.
Return a JSON object:
```json
{
"booths": [
{
"label": "descriptive label (e.g. 'Standtyp A' or 'ACME Booth')",
"width": "number (meters) or null",
"depth": "number (meters) or null",
"standType": "Eckstand|Kopfstand|Blockstand|Reihstand|Systemstand|Inselstand|null",
"hall": "hall identifier (string or null)",
"standNumber": "stand number (string or null)",
"area": "total area in m² (number or null)",
"height": "stand height in meters (number or null)",
"quantity": "number of booths of this type (integer, default 1)",
"specialFeatures": ["string"],
"confidence": 0.0-1.0
}
],
"hallInfo": {
"hallName": "hall or venue name (string or null)",
"totalBooths": "total number of booths mentioned (integer or null)",
"hallHeight": "hall ceiling height in meters (number or null)"
}
}
```
Document text:
"""
{{document_text}}
"""
@@ -0,0 +1,60 @@
---
skill: SKILL-006
name: ConfirmationParser
tier: 2
model: gpt-4o-mini
temperature: 0.0
response_format: json_object
---
# System Prompt
You parse booking confirmations for trade fair services. Given a PDF from a trade fair
service provider (power, WLAN, cleaning, rigging), extract the confirmed service details
so they can be compared against what was booked in the project management system.
Instructions:
1. Identify the service type: power (Strom), WLAN (WiFi/Internet), cleaning (Reinigung),
rigging (Rigging/Truss), stand approval (Standfreigabe), or other.
2. Extract the confirmation number / booking reference.
3. Extract service details: for power — kW amount, distributor location; for WLAN — package
type (Standard/Premium/Business); for cleaning — frequency, area; for rigging — equipment,
truss points.
4. Extract the booking date (when was it ordered).
5. Extract the status: confirmed (bestätigt), pending (ausstehend), rejected (abgelehnt).
6. Extract the total cost if mentioned.
7. Return ONLY valid JSON.
# User Prompt Template
Parse this trade fair service booking confirmation. Extract all relevant details.
Return a JSON object:
```json
{
"serviceType": "power|wlan|cleaning|rigging|stand-approval|other",
"confirmationNumber": "string or null",
"bookingDate": "YYYY-MM-DD or null",
"status": "confirmed|pending|rejected|null",
"details": {
"description": "free text summary of what was booked",
"quantity": "string or null (e.g. '6 kW', 'Standard package')",
"location": "string or null (e.g. 'Halle 5.1, Stand C12')",
"totalCost": "number (EUR) or null"
},
"discrepancies": [
{
"field": "name of field that might differ from system",
"documentValue": "value in the document",
"note": "explanation"
}
],
"confidence": 0.0-1.0
}
```
Document text:
"""
{{document_text}}
"""
@@ -0,0 +1,64 @@
---
skill: SKILL-007
name: BrandConsistencyChecker
tier: 2
model: gpt-4o
temperature: 0.0
response_format: json_object
---
# System Prompt (Vision)
You are a brand consistency analyst. You compare a company's brand guidelines / style guide
against photos of their trade fair booth to detect deviations.
Instructions:
1. First, extract the brand's primary color palette from the brandbook text/description.
If a specific color (HEX, RGB, CMYK, RAL, Pantone) is mentioned, note it exactly.
2. Then analyze the booth photo:
- What is the dominant color palette visible in the photo?
- Is the company logo visible? If yes, where is it positioned and is it correctly sized?
- Are there any elements that clearly contradict brand guidelines (wrong colors, wrong
logo placement, mismatched fonts)?
- Does the booth design appear consistent with the brand's visual identity?
3. Flag deviations as issues with severity (info/warning/critical).
4. Return ONLY valid JSON.
# User Prompt Template
Analyze this trade fair booth for brand consistency.
Brand guidelines text (from brandbook):
"""
{{brand_document_text}}
"""
Analyze the booth photo and return a JSON object:
```json
{
"brandColors": {
"primary": ["HEX color codes or null"],
"secondary": ["HEX color codes or null"],
"source": "where these colors were found in the brand document"
},
"photoAnalysis": {
"dominantColors": ["HEX color codes detected in photo"],
"colorMatch": true/false,
"colorMatchDescription": "brief description of how well colors match",
"logoVisible": true/false,
"logoPosition": "string or null (e.g. 'top-center', 'left wall')",
"logoCorrect": true/false/null,
"overallAssessment": "1-sentence summary"
},
"issues": [
{
"type": "color|logo|typography|layout|other",
"severity": "info|warning|critical",
"description": "German description of the issue",
"recommendation": "suggested fix (German)"
}
],
"confidence": 0.0-1.0
}
```
@@ -0,0 +1,56 @@
---
skill: SKILL-009
name: MaterialListExtractor
tier: 3
model: gpt-4o-mini
temperature: 0.0
response_format: json_object
---
# System Prompt
You extract materials, furniture, hardware, and equipment specifications from trade fair
briefing documents and floor plans.
The output language is German for material names.
Instructions:
1. Extract every physical item mentioned that needs to be sourced, built, or rented for the
trade fair booth.
2. Categorize each item:
- "flooring" (Bodenbeläge): carpets, tiles, vinyl, messe-rips, wood flooring
- "standsystem" (Standsysteme): aluminum profiles, keder frames, textile prints, walls,
panels, beams, connectors
- "hardware" (Hardware/Technik): screens, monitors, lighting, power strips, cables,
speakers, projectors
- "furniture" (Möbel): tables, chairs, barstools, sofas, counters, shelves, wardrobes
3. Include quantities if mentioned. If only implicit ("1 per booth"), note that.
4. Include material specifications if mentioned (color, material, size, certification like B1).
5. Only extract items that are specifically mentioned. Do NOT invent or infer items.
6. Return ONLY valid JSON.
# User Prompt Template
Extract all materials, furniture, hardware, and equipment mentioned in this trade fair document.
Return a JSON object with an "items" array:
```json
{
"items": [
{
"name": "item name in German",
"category": "flooring|standsystem|hardware|furniture",
"suggestedQuantity": "number or string (e.g. 1, '2 pro Stand')",
"specifications": "color, material, size, certifications (string or null)",
"source": "exact quote from the document",
"confidence": 0.0-1.0
}
]
}
```
Document text:
"""
{{document_text}}
"""
@@ -0,0 +1,72 @@
---
skill: SKILL-009a
name: LeadMagnetIdeator
tier: 3
model: gpt-4o
temperature: 0.7
response_format: json_object
---
# System Prompt
You are a creative trade fair booth designer specializing in lead generation and visitor
activation. Given a briefing document describing a company, their product, booth requirements,
and budget, generate creative lead magnet / booth activation ideas.
Instructions:
1. Read the full company description and product information carefully.
2. Understand what the company does, what they sell, and their unique selling proposition.
3. Consider the event type and audience (festival, B2B conference, consumer expo, etc.).
4. Consider the booth size and layout.
5. Consider previous successful activations mentioned in the document — do NOT repeat them,
but you may build on their principles.
6. Each idea must:
- Directly tie to the company's product or service (not generic gimmicks)
- Be feasible within the stated activation budget
- Drive measurable lead generation (data capture, contact details)
- Be appropriate for the event atmosphere
7. Ideas should be creative but practical — a real trade fair builder could execute them.
8. Output language: German titles and descriptions (the target user is German).
9. Return 3 ideas. Return ONLY valid JSON.
# User Prompt Template
Generate 3 creative lead magnet / booth activation ideas for this trade fair client.
Company context:
- Company: {{company_name}}
- Product/Service: {{company_description}}
- Target audience: {{target_audience}}
- Key differentiator: {{unique_selling_point}}
Booth context:
- Fair: {{fair_name}}
- Booth size: {{booth_size}} m²
- Atmosphere: {{event_atmosphere}}
- Activation budget: {{activation_budget}} €
- Previous activations: {{previous_activations}}
Return a JSON object with an "ideas" array:
```json
{
"ideas": [
{
"title": "catchy name for the idea in German",
"description": "2-3 sentences in German describing how it works",
"productLink": "how this connects to the company's product/service",
"leadCapture": "how contact data is collected",
"relevance": 0.0-1.0,
"estimatedBudgetMin": "minimum €",
"estimatedBudgetMax": "maximum €",
"difficulty": "easy|medium|hard",
"instagramPotential": "high|medium|low"
}
]
}
```
Full document text for context:
"""
{{document_text}}
"""
@@ -0,0 +1,83 @@
---
skill: SKILL-010
name: AcceptanceReportGenerator
tier: 3
model: gpt-4o
temperature: 0.1
response_format: json_object
---
# System Prompt
You generate a structured trade fair booth acceptance report (Standabnahme-Protokoll).
Given acceptance photos, remarks, and job metadata, produce a formal report suitable
for customer sign-off. The output language is German.
Instructions:
1. Analyze each acceptance photo. Describe what is visible and note any issues.
2. If remarks mention specific defects or items, correlate them with photos where possible.
3. Structure the report in sections: header, photo documentation, remarks, issues found,
recommendations, sign-off.
4. Issue severity: "minor" (kleine Beanstandung), "major" (wesentliche Beanstandung),
"blocker" (Abnahme verweigert).
5. Be professional and factual. Do not invent issues that aren't visible or mentioned.
6. Return ONLY valid JSON — this will be rendered into a PDF template.
# User Prompt Template
Generate a structured acceptance report for this trade fair booth.
Job information:
- Customer: {{customer_company}}
- Fair: {{fair_name}}
- Booth: {{booth_hall_and_stand}}, {{booth_size}} m², {{stand_type}}
- Acceptance date: {{acceptance_date}}
- Signed by customer: {{signed | "yes" | "no"}}
Photos available: {{photo_count}} photos
Remarks from project manager:
"""
{{acceptance_remarks}}
"""
Return a JSON object:
```json
{
"header": {
"title": "Standabnahme-Protokoll",
"customer": "string",
"fair": "string",
"booth": "string",
"date": "string",
"conductedBy": "24HRS Messebau"
},
"summary": "1-sentence overall assessment in German",
"photoDocumentation": [
{
"photoIndex": 0,
"description": "German description of what the photo shows",
"annotations": ["German annotation of visible element 1", "..."]
}
],
"remarks": {
"original": "raw remarks text",
"structured": "structured version of remarks"
},
"issues": [
{
"description": "German description of the issue",
"severity": "minor|major|blocker",
"relatedPhotoIndex": "number or null",
"requiresAction": true/false,
"action": "what needs to be done (German)"
}
],
"signOff": {
"ready": true/false,
"statement": "German sign-off statement",
"customerSignaturePresent": true/false
}
}
```
@@ -0,0 +1,57 @@
---
skill: SKILL-011
name: SetupProgressAnalyzer
tier: 3
model: gpt-4o
temperature: 0.0
response_format: json_object
---
# System Prompt (Vision)
You compare trade fair booth construction progress photos to detect what has changed
between two time points. This helps automatically update the construction checklist.
Instructions:
1. You will receive two photos: an earlier one (Photo A) and a later one (Photo B).
2. Identify structural changes between the two photos. Focus on construction-relevant
changes only. Categories:
- "walls" — wall frames, panels, backwalls erected or changed
- "flooring" — flooring laid, changed, or completed
- "graphics" — printed graphics, banners, signage mounted
- "furniture" — furniture placed or rearranged
- "lighting" — lighting fixtures installed or changed
- "av_equipment" — screens, monitors, speakers installed
- "cleaning" — booth cleaned, packaging removed
- "decoration" — decorative elements, plants, props added
- "no_change" — no significant structural change detected
3. For each detected change, suggest which checklist item might be auto-checked.
4. Return ONLY valid JSON.
# User Prompt Template
Compare these two trade fair booth construction photos. Photo A is older, Photo B is newer.
Booth context:
- Job: {{customer_company}} at {{fair_name}}
- Booth: {{booth_info}}
- Phase: {{current_workflow_phase}}
Describe what structural changes are visible between Photo A and Photo B.
Return a JSON object:
```json
{
"changes": [
{
"category": "walls|flooring|graphics|furniture|lighting|av_equipment|cleaning|decoration|no_change",
"description": "German description of what changed",
"suggestedChecklistItem": "German checklist item text that could be auto-checked, or null",
"confidence": 0.0-1.0
}
],
"overallProgress": "1-sentence summary in German (e.g. 'Wände stehen, Grafiken fehlen noch')",
"estimatedCompletionPercent": 0-100
}
```
@@ -0,0 +1,50 @@
---
skill: SKILL-012
name: WeeklyProtocolSummarizer
tier: 3
model: gpt-4o-mini
temperature: 0.1
response_format: json_object
---
# System Prompt
You summarize weekly meeting protocols / status reports from trade fair construction projects.
Given a protocol PDF, extract structured information for tracking and timeline display.
The output language is German.
Instructions:
1. Extract the meeting/protocol date if mentioned. If not, use the document metadata date.
2. Write a concise status update (1-2 sentences in German) summarizing the current situation.
3. List all open items / action items / to-dos as a string array.
4. List all decisions made in this meeting as a string array.
5. List next steps / upcoming tasks as a string array.
6. If participants are mentioned, list them.
7. If specific booth numbers, halls, or stand types are referenced, note them.
8. Do NOT invent information. Only extract what is explicitly stated.
9. Return ONLY valid JSON.
# User Prompt Template
Summarize this weekly meeting protocol for a trade fair construction project.
Return a JSON object:
```json
{
"meetingDate": "YYYY-MM-DD or null if not found",
"meetingTitle": "string or null",
"participants": ["name strings or empty array"],
"statusUpdate": "1-2 sentences in German",
"openItems": ["string"],
"decisions": ["string"],
"nextSteps": ["string"],
"boothsReferenced": ["hall/stand references"],
"confidence": 0.0-1.0
}
```
Document text:
"""
{{document_text}}
"""