# Implementation Plan: Event Management App

**Branch**: `001-event-management-app` | **Date**: 2026-05-30 | **Spec**: [spec.md](file:///Volumes/Totoro/Trabalho/Dev/abrat-sp/specs/001-event-management-app/spec.md)

**Input**: Feature specification from `/specs/001-event-management-app/spec.md`

## Summary

Build an event management application supporting multiple ticket categories and date-based batches with manual/offline checkout and admin approval. The backend will use PocketBase for authentication/files, with a traditional PHP 8.4 Slim/Twig application powered by SQLite.

## Technical Context

**Language/Version**: PHP 8.4

**Primary Dependencies**: Slim Framework v4, Twig, PocketBase (External Service)

**Storage**: SQLite (Local Database), PocketBase (Files/Auth)

**Testing**: PHPUnit

**Target Platform**: Docker (Linux)

**Project Type**: Web Application

**Performance Goals**: Fast TTFB and page rendering using Twig and SQLite

**Constraints**: Must integrate with PocketBase via HTTP/SDK for auth/files, no external payment gateway

**Scale/Scope**: Low-to-Medium volume ticketing for local events

**Admin Auth & RBAC**:
- **Authentication**: Login screen restricted to `/admin` route; integrates directly with PocketBase User Manager.
- **RBAC**: Implemented via a user management interface in the Admin Dashboard (mapping permissions/endpoints to user roles in PocketBase).
- **Roles Matrix**:
  - `superadmin`: Unrestricted access to all UI interfaces and API endpoints.
  - `financeiro`: Access restricted exclusively to financial and payment screens.
  - `administrador`: Permission to print sold tickets and check attendee payment status.
- **Session & Profile Management**:
  - **Logout**: UI button in the admin header to invalidate session and redirect to login.
  - **Self-Service Password Change**: Dedicated Profile/Security view allowing logged-in admins to change their own password securely via PocketBase's update endpoint.

**UI Refactoring (Admin Panel)**:
- **Base Layout (`admin/layout.twig`)**: A centralized template containing the global structure (`<html>`, `<head>`, shared CSS, Flexbox/Grid skeleton, Sidebar/Header with navigation and logout).
- **Template Inheritance**: All administrative `.twig` files (users, events, categories, orders, profile, manual sales) must `{% extends "admin/layout.twig" %}`.
- **Content Blocks**: Individual views must inject their specific content inside `{% block content %}{% endblock %}` and specific styles in `{% block extra_css %}{% endblock %}` without duplicating HTML boilerplate.
- **Consistency**: Ensure all components (tables, buttons, forms, selects) use the same standard color palette, border-radius, and padding definitions.
- **Terminology Update**: The term "Ingressos" in the Event Management UI (e.g. `events/list.twig`) has been changed to "Tipos de Ingresso" for clarity when referring to ticket categories.

**Phase 10: Event & Ticket Management Dashboard**:
- **Database Schema Updates**: Add `event_date` (DATETIME), `location` (TEXT), and `status` (TEXT DEFAULT 'ACTIVE') to the `events` table to support the active events listing on the dashboard.
- **Dashboard (`/admin`)**: A new section displaying active events (`status = 'ACTIVE'`). It calculates sold tickets (orders with status `APPROVED`) vs available capacity (sum of active batches) per event.
- **Tickets Management View (`/admin/events/{id}/tickets`)**: A new route handling the listing of tickets associated with a specific event. Includes a sales summary and detailed view of all issued tickets and pending orders for that event.

**Phase 12: Refactoring UI - Admin Layout**:
- **Layout Base (`admin/layout.twig`)**: Refactor to a full-viewport, two-column layout consisting of a fixed left Sidebar for global navigation (Dashboard, Users, Events, Orders, Profile, Logout) and a main content area.
- **Inner Headers**: Create a structured header inside the content area (`page-header`) containing a title (`header_title` block) and a row for actions (`global_actions` block).
- **Contextual Actions**: Align table action buttons (Edit, Delete, Manage) to the right-most column of the list items for consistency across all sub-views.

**Phase 14: Dashboard Actions Refinement**:
- **Dashboard (`/admin`)**: Remove the "Editar" button for each event in the active events list. Add a new button "Ver Página do Evento" that links to the public event checkout/details page (`/event/{id}`), allowing admins to easily visualize the public purchase page.

**Phase 15: Missing Integration Tests**:
- **Registration Flow Test**: Create `tests/Integration/RegistrationTest.php` to verify the public checkout process (User Story 1 - Attendee Event Registration). This covers form submission, order creation, and redirection to the payment instructions page.

**Phase 16: System Info & Developer Contact Screen**:
- **Layout Update**: In `templates/admin/layout.twig`, add a watermark-style button in the sidebar (immediately above the "Sair" button) displaying the app's version number.
- **New Route**: Create a new admin route (e.g., `GET /admin/system-info`) managed by a new controller `SystemInfoController`.
- **System Info View**: Create a view (`system_info.twig`) that displays development data: Tech stack (PHP version, App version, PocketBase version).
- **Contact Form**: In the same view, implement a contact form that visually points/submits to `eduardogcorrea@gmail.com`, and display the developer logo `public/images/logo_ectec.png`.

**Phase 17: Global Settings Interface**:
- **Data Persistence (SQLite Migration)**: Create a migration script to set up an `app_settings` table (key-value store or fixed columns: `app_name`, `favicon_url`, `logo_url`, `smtp_host`, `smtp_port`, `smtp_user`, `smtp_pass`).
- **Base Layout Update (`admin/layout.twig`)**: Add a logo placeholder (`<img src="{{ settings.logo_url ?? '/assets/img/placeholder-logo.png' }}" />`) beside the "Administração" text, and a "Configurações" link in the sidebar pointing to `/admin/configuracoes`.
- **Route & Controller (`/admin/configuracoes`)**:
  - `GET`: Query `app_settings`, inject into Twig renderer, and return `admin/settings.twig`.
  - `POST`: Save text fields (SMTP, App Name). For images (Favicon, Logo), intercept uploads, send them to PocketBase via HTTP, and save the resulting public URL in SQLite.
- **Twig Interface (`admin/settings.twig`)**: Create a view with identity (App Name, Logo, Favicon) and SMTP sections, maintaining consistent styling.

**Branch**: `001-event-management-app` | **Date**: 2026-06-03

## Summary
Build an event management application supporting multiple ticket categories, date-based batches, and automated payment processing via ASAAS (Pix and Credit Card). The backend uses PocketBase for authentication/files, with a traditional PHP 8.4 Slim/Twig application powered by SQLite.

## Technical Context
**Constraints**: Must integrate with PocketBase for auth/files. Must integrate with ASAAS API for payment generation and webhooks.

*(... Mantenha todas as Fases da 1 até a 17 exatamente como estavam no seu arquivo original ...)*

**Phase 18: ASAAS Payment Integration & Webhooks**:
- **API Client**: Create `src/Services/AsaasClient.php` using Guzzle to handle Customer Creation, Payment Generation (Pix/Credit Card), and fetching QR Codes.
- **Checkout Refactoring**: Update `OrderService` and `EventController` to branch logic based on payment method. If Pix, render the transparent checkout Twig view with the QR code. If Credit Card, perform an HTTP redirect to the ASAAS `invoice_url`.
- **Webhook Controller**: Create `src/Controllers/WebhookController.php` to receive POST requests from ASAAS. It must validate the payload, locate the order via `asaas_payment_id`, update the order status to `APPROVED`, and trigger the `TicketService` to dispatch the email.
- **Database Migration**: Create a migration to add `asaas_customer_id`, `asaas_payment_id`, `payment_method`, `pix_qr_code`, `pix_qr_code_url`, and `invoice_url` to the `orders` table.

**Phase 19: Event Deletion**:
- **Edit View (`admin/events/edit.twig`)**: Add a "Delete Event" button (styled as danger/red) in the event edit form. It should use a Javascript confirmation dialog (`confirm('Tem certeza?')`) before submitting.
- **Route & Controller**: Create a new route `POST /admin/events/{id}/delete` in `EventController`.
- **Logic**: The controller must delete the event from the SQLite `events` table (and ideally handle cascading deletes for tickets/categories if applicable) and redirect the admin back to the dashboard (`/admin`) with a success message.

**Phase 20: Debug Environment Variables Display**:
- **Controller Logic**: In `SettingsController::index`, check if the `DEBUG` environment variable is strictly set to `true`. If it is, pass all loaded `$_ENV` (or `getenv()`) variables to the template.
- **View Update (`admin/settings.twig`)**: Add a conditional block at the bottom of the settings page that renders a table of environment variables if they are passed from the controller, serving as a debug helper.

**Phase 21: Event Shortcode (Slug) URLs**:
- **Database Migration**: Create a migration to add a `slug` column to the `events` table.
- **Slug Generation**: Update `AdminEventService::createEvent` and `updateEvent` to automatically generate a URL-friendly slug based on the event title (e.g., lowercase, replace spaces with hyphens, remove special characters).
- **Public Routing**: Update `public/index.php` to map `GET /event/{slug}` (instead of `{id}`) to `EventController::show`.
- **Public Controller**: Update `EventController::show` to fetch the event by `slug` instead of `id`.
- **Admin Views**: Update `templates/admin/dashboard.twig` (and any other admin views linking to the public page) to use `/event/{{ event.slug }}` instead of `/event/{{ event.id }}`.

**Phase 22: Event Cover Image Styling**:
- **View Update (`event_details.twig`)**: Update the CSS and styling for the event cover image on the public event page. Apply `width: 100%; max-height: 350px; object-fit: cover; border-radius: 8px;` (or similar) to make the image act as a horizontal banner, shrinking vertically but spanning horizontally without distortion.

**Phase 23: Event Description Alignment**:
- **View Update (`event_details.twig`)**: Apply `text-align: center;` to the event description paragraph on the public event page so that the text is centered along with the other elements.

**Phase 24: Direct PIX Payment Configuration**:
- **Database Migration**: Add `accepts_pix_direto` (BOOLEAN, default 0) and `pix_direto_instructions` (TEXT) to the `events` table to store the Direct PIX configuration per event.
- **Admin Controllers & Routes**: Create `GET /admin/events/{id}/payment-settings` and `POST /admin/events/{id}/payment-settings` (e.g., in `EventAdminController`) to handle loading and saving the payment configuration.
- **Admin Views**: Create `templates/admin/events/payment_settings.twig` with a form to toggle "PIX Direto" and edit its instructions via a textarea. Add a link to this page in the admin event management sidebar or actions list.
- **Public Checkout Flow**:
  - Update `CheckoutController::showForm` to inject the event's payment settings.
  - Update the checkout form template to display "PIX Direto" as a payment method option if enabled. Show the instructions when selected.
  - Update `CheckoutController::processForm` to bypass the Asaas integration when "PIX Direto" is chosen. Instead, directly create the order/registration in the database with a `PENDING` status (or `AWAITING_MANUAL_PAYMENT`) and redirect the user to a success page displaying the PIX instructions.
- **Admin Manual Approval**: Ensure the admin orders view allows transitioning the `PENDING` order to `APPROVED` manually, which triggers the ticket generation logic (as it would via the webhook).

**Phase 25: Ticket Management and Printing**:
- **Database Query**: Update `AdminEventService::getEventTickets` to sort by `o.attendee_name ASC` (alphabetic order).
- **Admin Views**: 
  - Add a "Ingressos Vendidos" button to `templates/admin/events/list.twig` pointing to `/admin/events/{id}/tickets`.
  - Update `templates/admin/events/tickets.twig` to add an "Ações" column with buttons to "Imprimir" and "Reenviar E-mail" for each ticket.
- **Admin Controllers & Routes**: Add routes `GET /admin/events/{id}/tickets/{ticket_code}/print` to render a printable ticket view and `POST /admin/events/{id}/tickets/{ticket_code}/send` to re-trigger the ticket dispatch email. Create the printable ticket template.

**Phase 26: Event Order and Ticket Management Consolidation**:
- **Database Query**: Create or modify a method in `AdminEventService` (e.g., `getEventOrdersAndTickets`) to query ALL orders (`orders o`) for a specific event using `LEFT JOIN tickets t ON t.order_id = o.id`. This ensures both `PENDING` and `APPROVED` registrations are fetched, ordered alphabetically by `attendee_name`.
- **Admin Views**: Update `templates/admin/events/tickets.twig` to handle both pending orders and confirmed tickets in the same list. 
  - In the "Ações" column, dynamically render buttons based on the status:
    - If `PENDING`: Show an "Aprovar Pagamento" button (hitting the existing `/admin/orders/{id}/approve` endpoint).
    - If `APPROVED`: Show the "Imprimir" and "Reenviar E-mail" buttons using the `ticket_code`.
- **Controller Adjustments**: Ensure that the `/admin/orders/{id}/approve` endpoint redirects back to the event tickets page if possible, or verify it operates correctly without breaking the flow. Update the summary statistics in `EventAdminController::listTickets` to account for `PENDING` vs `APPROVED` revenue accurately.

**Phase 27: Deployment and Database Migration Automation**:
- **GitHub Actions (`.github/workflows/deploy.yml`)**:
  - Add execution of SQLite migrations (`php scripts/migrate.php` and `php scripts/migrate_orders_add_columns.php`) inside the `app` container automatically after `docker-compose up -d`.
  - Add execution of PocketBase setup/migrations (`php scripts/setup_pocketbase.php`) inside the `app` container. Ensure a slight delay (e.g., `sleep 5`) so the PocketBase container has time to start serving HTTP requests.
- **Docker Compose (`docker-compose.yml`)**:
  - Ensure the `pocketbase` service explicitly defines `--dir=/pb_data` in its startup command and correctly maps the volume `./pb_data:/pb_data` to ensure data persistence works across deployments, bypassing the default image behavior if necessary.

**Phase 28: Automated Migration Runner System**:
- **Database Schema**: Create a new table `app_migrations` with columns `id`, `migration_name` (UNIQUE), and `executed_at` to track which migrations have successfully run.
- **Migration Runner Script**: Create `scripts/run_migrations.php`. This script will:
  - Create the `app_migrations` table if it does not exist.
  - Scan a new directory `scripts/migrations/` for `*.php` files.
  - For each file, check if its name exists in `app_migrations`. If not, execute the file.
  - Upon successful execution (exit code 0), insert the file name into the `app_migrations` table to prevent future runs.
- **Restructuring**:
  - Create the `scripts/migrations/` directory.
  - Move standalone, non-base migrations (like `migrate_orders_add_columns.php`) into `scripts/migrations/`.
- **Deployment Pipeline Update**:
  - Modify `.github/workflows/deploy.yml` to replace the explicit migration calls with a single call to `docker-compose exec -T app php scripts/run_migrations.php` (alongside the base `migrate.php` which handles `IF NOT EXISTS` logic safely).

**Phase 29: PocketBase Configuration Alignment**:
- **Docker Compose (`docker-compose.yml`)**:
  - Remove the explicit `command: serve --http=0.0.0.0:8090 --dir=/pb_data` from the `pocketbase` service.
  - Use environment variables `PB_HOST: 0.0.0.0` and `PB_PORT: 8090` as recommended by the `muchobien/pocketbase` image documentation to let the container correctly wire its internal volumes and data paths.
  - Add `restart: unless-stopped`.
  - Add `healthcheck` definition to ensure dependent containers know when PocketBase is truly ready.

**Phase 30: UI Navigation and Link Refinements**:
- **Admin Sidebar Refinement**:
  - Update `templates/admin/layout.twig` to remove the "Pedidos" (Orders) menu item from the sidebar navigation.
- **Dashboard Event Links**:
  - Update `templates/admin/dashboard.twig` to adjust the "Ver Página do Evento" (View Event Page) link. It should correctly construct the URL using the event slug (e.g., `event/{{ event.slug }}`) instead of the raw event ID.

**Phase 31: Dashboard Slug Bug Fix**:
- **Dashboard Service**: Update `AdminDashboardService::getActiveEvents` to include `e.slug` in the `SELECT` query so that the dashboard correctly renders the "Ver Página do Evento" link with the slug instead of an empty string.

**Phase 32: Extended Registration Form Fields**:
- **Database Migration**: Create `scripts/migrations/002_add_attendee_fields.php` to add the following columns to the `orders` table:
  - `attendee_nationality` (TEXT, optional)
  - `attendee_phone` (TEXT, optional)
  - `attendee_country` (TEXT, optional)
  - `attendee_state` (TEXT, optional)
  - `attendee_city` (TEXT, optional)
  - `attendee_oab` (TEXT, optional)
- **Form Template (`templates/checkout.twig`)**: Add all new fields to the registration form:
  - `Nome Completo` (required — already exists)
  - `E-mail` (required — already exists)
  - `Confirmar E-mail` (required — client-side + server-side validation: must match `attendee_email`)
  - `Nacionalidade`, `Telefone Celular`, `País`, `Estado`, `Cidade`, `OAB` (all optional)
- **Controller (`CheckoutController::processForm`)**: 
  - Extract all new fields from `$data`.
  - Validate that `attendee_email` matches `attendee_email_confirm`; if not, return back to the form with an error.
  - Pass the new fields down to `$orderData`.
- **OrderService**: Update both `createOrder` and `createDirectOrder` methods to include the new fields in the `INSERT` SQL and the bound parameters.
- **Admin Orders/Tickets View**: Update `templates/admin/events/tickets.twig` to display the extra attendee fields where relevant.

**Phase 33: SITE_URL Global — Fix PocketBase Image URLs in Production**:
- **O problema**: as imagens enviadas para o PocketBase são salvas com URLs absolutas geradas em tempo de upload usando `POCKETBASE_PUBLIC_URL` (ex: `http://localhost:8090`). Em produção, esse endereço é inacessível pelo navegador.
- **A solução adotada** usa a variável `SITE_URL` do `.env` como base para montar as URLs públicas do PocketBase, garantindo que as imagens sejam acessíveis via o domínio real da aplicação.
- **`public/index.php` (DI Container)**:
  - Ler `SITE_URL` do `.env` (ex: `https://app.abrat-sp.orb.local`).
  - Substituir a lógica de `POCKETBASE_PUBLIC_URL` para usar `SITE_URL` diretamente, passando-o ao construtor `PocketBaseClient`.
  - Adicionar `SITE_URL` como variável global Twig (`$twig->getEnvironment()->addGlobal('site_url', $siteUrl)`) para que todas as telas possam usá-la.
- **`PocketBaseClient::getFileUrl`**:
  - Alterar para montar a URL usando o `publicUrl` (que agora será `SITE_URL`), apontando para o endpoint público do PocketBase via proxy reverso.
  - Formato: `{SITE_URL}:8090/api/files/{collection}/{record}/{filename}` — OU, se o Nginx faz proxy de `/pb/` para o PocketBase, usar `{SITE_URL}/pb/api/files/...`.
  - **Decisão adotada**: usar `{SITE_URL}:8090` como `publicUrl`, mantendo a mesma porta do PocketBase, pois o `orb.local` resolve o hostname corretamente e a porta 8090 está exposta via `docker-compose.yml`.
- **Twig Global em todas as telas**:
  - Todas as views que exibem `cover_image_url` já usam a URL armazenada no banco de dados.
- **Novos uploads**: A partir desta fase, toda nova imagem salva terá URL baseada em `SITE_URL`.
- **Imagens antigas**: Criar um script utilitário `scripts/fix_image_urls.php` que faz um `UPDATE events SET cover_image_url = REPLACE(cover_image_url, 'http://localhost:8090', :site_url)`.

**Phase 34: Fix PocketBase Public URL — Restore POCKETBASE_PUBLIC_URL**:
- **Diagnóstico**: A Fase 33 introduziu um problema ao tentar derivar a URL pública do PocketBase a partir de `SITE_URL + :8090`. No OrbStack, a porta 8090 não é exposta via HTTPS; apenas a porta 8000 (app) recebe o proxy HTTPS automático do OrbStack. Além disso, o container não foi reiniciado, então `SITE_URL` não estava visível para o PHP via `getenv()`.
- **Solução**: Restaurar a variável dedicada `POCKETBASE_PUBLIC_URL` no `.env` e `docker-compose.yml`, que o usuário configura explicitamente com o endereço acessível pelo browser para o PocketBase. `SITE_URL` continua existindo apenas como global Twig.
- **`docker-compose.yml`**: Substituir `SITE_URL=${SITE_URL}` por `POCKETBASE_PUBLIC_URL=${POCKETBASE_PUBLIC_URL}` no serviço `app`.
- **`public/index.php`**: Restaurar a leitura de `POCKETBASE_PUBLIC_URL` para o `PocketBaseClient`. Manter `SITE_URL` como global Twig.
- **`scripts/fix_image_urls.php`**: Atualizar o script para usar `POCKETBASE_PUBLIC_URL` em vez de `SITE_URL` ao corrigir registros.
- **`.env` (orientação ao usuário)**: Confirmar que `POCKETBASE_PUBLIC_URL=http://localhost:8090` está definida.
- **Banco de dados**: Re-executar o script de correção para reverter as URLs de `https://app.abrat-sp.orb.local:8090` de volta para `http://localhost:8090`.

**Phase 35: Database Migration & Schema Updates for Check-in**:
- **Database Migration**: Create a migration script `scripts/migrations/003_add_ticket_checkin.php` to add `checked_in_at` (DATETIME, default NULL) to the `tickets` table.
- **Test Environment Integration**: Update `tests/TestCase.php` in `runMigrations()` to add `checked_in_at DATETIME` column to the `tickets` table definition.

**Phase 36: QR Code Generation on Ticket Print**:
- **QR Code Rendering**: Use the public, highly compatible API `https://api.qrserver.com/v1/create-qr-code/` to generate a static QR code representing the ticket code inside an `<img>` tag.
- **Print View Update**: Update `templates/admin/events/ticket_print.twig` to display the QR code beside the ticket text code.

**Phase 37: Admin Check-in Screen (`/admin/checkin`) with Webcam Scanner**:
- **Routes & Middleware**: Add routes `GET /admin/checkin` and `POST /admin/checkin` to `public/index.php`.
- **Controller (`src/Controllers/Admin/CheckinController.php`)**:
  - `showCheckinForm` (GET): Displays the check-in screen. If query parameter `code` is provided, fetches the ticket using `AdminEventService::getTicketByCode()`. If valid, passes it to the view.
  - `confirmCheckin` (POST): Receives `ticket_code` and executes `AdminEventService::checkInTicket()`. Returns a JSON response with status and message for AJAX requests, or redirects for standard POST.
- **Admin Event Service Update**: Add `checkInTicket(string $code): bool` in `AdminEventService` to update the ticket's `checked_in_at` timestamp.
- **Check-in View (`templates/admin/checkin.twig`)**:
  - Design a modern, user-friendly check-in layout extending `admin/layout.twig`.
  - Include an HTML5 QR code scanning webcam container using the `html5-qrcode` library via CDN.
  - Display a manual input text box.
  - Render a clear verification status card (Success/Green for check-in confirmed, Warning/Orange if already checked in, Danger/Red if invalid).
  - Add a list at the bottom showing the last 5 successful check-ins in the session.

**Phase 38: Security Role `recepcao` (Receptionist) & RBAC Checks**:
- **Admin User Interface Updates**:
  - Add `recepcao` as a selectable option in `templates/admin/users/form.twig`.
  - Add badge support and colors for the `recepcao` role in `templates/admin/users/list.twig`.
- **Authentication Middleware Refactoring**:
  - Update `src/Middleware/AdminAuthMiddleware.php`:
    - Allow users with the `recepcao` role to access `/admin/checkin` and login/logout endpoints.
    - If a `recepcao` user attempts to access `/admin` or `/admin/`, redirect them to `/admin/checkin`.
    - If a `recepcao` user accesses any other administrative URL, return a 403 Forbidden status.
    - Allow users with the `financeiro` role to also access `/admin/checkin` in addition to `/admin/orders` and the main dashboard.

**Phase 39: Event Participant Attendance Report & CSV Export**:
- **Routes**: Add `GET /admin/events/{id}/report` and `GET /admin/events/{id}/report/export` to `public/index.php`.
- **Controller Logic (`EventAdminController.php`)**:
  - `showAttendanceReport(Request $request, Response $response, array $args)`: Queries all tickets (and attendee orders) associated with the event. Calculates registration total, checked-in total, and the percentage. Renders `admin/events/report.twig`.
  - `exportAttendanceCsv(Request $request, Response $response, array $args)`: Generates and triggers download of a CSV file listing all event participants, their document, ticket code, category, batch, check-in status, and check-in timestamp.
- **Admin Event Service Update**: Update database query methods to fetch `checked_in_at` and check-in statuses.
- **Report View (`templates/admin/events/report.twig`)**:
  - Display metrics/cards.
  - Render a filterable and searchable participant list table with check-in timestamps.
  - Add a button in the event page header to "Exportar CSV".
- **Navigation Update**: Add a link to "Relatório de Check-in" inside the action buttons for each event in the main events list and tickets dashboard.



**Phase 41: Event Badge Dimensions Database Schema & Admin Form**:
- **Database Migration**: Create `scripts/migrations/004_add_event_badge_settings.php` to add `badge_width_cm` (DECIMAL(5,2), default 8.0) and `badge_height_cm` (DECIMAL(5,2), default 4.0) columns to the `events` table.
- **Test Environment Integration**: Update `tests/TestCase.php` in `runMigrations()` to include these columns in the `events` table schema.
- **Admin Views**: Add `badge_width_cm` and `badge_height_cm` fields to the Event create and edit forms (`templates/admin/events/form.twig`), with default values of `8.0` and `4.0` respectively, and input validation ensuring positive decimal values.
- **Controller/Service Update**: Update `AdminEventService::createEvent` and `updateEvent` to capture and persist `badge_width_cm` and `badge_height_cm`.

**Phase 42: PDF Badge Generation & Automatic Print Trigger**:
- **Composer Package**: Run `composer require dompdf/dompdf` inside the container.
- **Badge Routes**: Register `GET /admin/tickets/{ticket_code}/badge` in `public/index.php`.
- **Controller Logic (`CheckinController.php`)**:
  - Implement `showBadgePdf(Request $request, Response $response, array $args)`: 
    - Fetch the ticket by `ticket_code` and load its associated event and category.
    - Set up Dompdf with custom paper dimensions based on the event's `badge_width_cm` and `badge_height_cm` (converting cm to points: `cm * 28.346`).
    - Render a simple custom template `templates/admin/events/badge.twig` containing only the attendee's name centered horizontally and vertically, styled cleanly.
    - Return the PDF output with `Content-Type: application/pdf` and inline disposition so the browser loads it correctly.
- **Check-in Controller AJAX Update**:
  - In `confirmCheckin`, return `badge_url` (e.g. `/admin/tickets/{ticket_code}/badge`) in the successful check-in JSON response.
- **Frontend Print Automation**:
  - Update `templates/admin/checkin.twig` to automatically open the `badge_url` in a new window/tab (or hidden iframe) upon receiving a successful check-in AJAX response, initiating the print dialog.

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

- [x] I. Code Quality MUST Be Paramount
- [x] II. Strict Testing Standards MUST Be Followed
- [x] III. User Experience MUST Be Consistent
- [x] IV. Performance Requirements MUST Be Met

## Project Structure

### Documentation (this feature)

```text
specs/001-event-management-app/
├── plan.md              # This file (/speckit-plan command output)
├── research.md          # Phase 0 output (/speckit-plan command)
├── data-model.md        # Phase 1 output (/speckit-plan command)
├── quickstart.md        # Phase 1 output (/speckit-plan command)
└── tasks.md             # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan)
```

### Source Code (repository root)

```text
src/
├── Controllers/
├── Models/
├── Views/
└── Services/

public/
└── index.php

templates/

tests/
├── Integration/
└── Unit/
```

**Structure Decision**: Option 1: Single project (Traditional PHP MVC setup with Slim Framework and Twig templates inside `templates/`).

## Verification Constraints
- **MANDATORY**: Always check Docker server logs (`docker-compose logs --tail 50 app`) after running `speckit-implement` to verify if the implementation is actually working and not throwing 500 errors.
