# Plan: Cancellation Statistics Feature ## Objective Enable stakeholders to see how often dispositions and applications have been canceled by either the teamer or the office, per hotel and for a given date range. --- ## Current State Analysis ### Dispositions - Already Tracked The `Disposition` entity has comprehensive cancellation tracking: | Field | Type | Purpose | |-------|------|---------| | `status` | string | `'called_off'` when cancelled | | `calledOffBy` | string (nullable) | `'teamer'` or `'office'` | | `calledOffReason` | text (nullable) | Reason for cancellation | | `updatedAt` | datetime | Timestamp of last change | **Constants:** ```php public const STATUS_CALLED_OFF = 'called_off'; public const CALLED_OFF_BY_TEAMER = 'teamer'; public const CALLED_OFF_BY_OFFICE = 'office'; ``` **Cancellation entry points:** 1. Individual disposition cancellation via `Administrative/Disposition/CallOffController` - Form requires `calledOffBy` and `calledOffReason` - Dispatches `DispositionCalledOffEvent` 2. Full assignment cancellation via `Administrative/Assignment/CallOffController` - Sets all dispositions to `calledOffBy = 'office'` - Dispatches `AssignmentCalledOffEvent` ### Applications - Missing Tracking The `Application` entity lacks cancellation tracking: | Current Field | Issue | |---------------|-------| | `status` | Only `'rejected'` - no distinction between teamer withdrawal and office rejection | | - | No `cancelledBy` field | | - | No `cancellationReason` field | **Current rejection mechanisms:** 1. Manual rejection by office (no tracking of who) 2. Automatic rejection by `RejectApplicationListener` when disposition slots fill 3. Automatic removal by `InvalidateApplicationsListener` when teamer has overlapping confirmed dispositions --- ## Pending Decision **Question for stakeholders:** Should automatic rejections be included in statistics? | Option | Description | Impact | |--------|-------------|--------| | **Only user-initiated** | Count only manual cancellations by teamer or office | Simpler model, clearer accountability | | **Include automatic** | Also track system-initiated rejections | Full picture of lost applications, requires `'system'` category | **Status:** Awaiting stakeholder response --- ## Proposed Changes ### Application Entity **Option A: If only user-initiated cancellations count** Add new status and fields: ```php public const STATUS_WITHDRAWN = 'withdrawn'; // teamer-initiated public const STATUS_REJECTED = 'rejected'; // office-initiated (existing) private ?string $cancellationReason = null; ``` **Option B: If automatic rejections should be tracked** Add new status, fields, and constant: ```php public const STATUS_WITHDRAWN = 'withdrawn'; // teamer-initiated public const STATUS_REJECTED = 'rejected'; // office or system initiated public const CANCELLED_BY_TEAMER = 'teamer'; public const CANCELLED_BY_OFFICE = 'office'; public const CANCELLED_BY_SYSTEM = 'system'; private ?string $cancelledBy = null; private ?string $cancellationReason = null; ``` ### Database Migration Add columns to `application` table: - `cancellation_reason` (LONGTEXT, nullable) - Possibly `cancelled_by` (VARCHAR(64), nullable) if Option B ### Repository Methods Add to `ApplicationRepository`: ```php /** * @return array */ public function getCancellationStatsByHotel( \DateTimeImmutable $startDate, \DateTimeImmutable $endDate ): array; ``` Add to `DispositionRepository`: ```php /** * @return array */ public function getCancellationStatsByHotel( \DateTimeImmutable $startDate, \DateTimeImmutable $endDate ): array; ``` **Date filtering:** Based on assignment travel dates (confirmed in discussion). ### Update Listeners (if Option B) Modify `RejectApplicationListener` and `InvalidateApplicationsListener` to set `cancelledBy = 'system'`. ### UI Changes Add withdrawal functionality for teamers (if not already present) that sets `STATUS_WITHDRAWN`. --- ## Implementation Steps 1. [ ] Finalize decision on automatic rejection tracking 2. [ ] Add new constants and fields to `Application` entity 3. [ ] Create database migration 4. [ ] Update `ApplicationRepository` with statistics methods 5. [ ] Update `DispositionRepository` with statistics methods 6. [ ] Update listeners if tracking automatic rejections 7. [ ] Add/update controllers for teamer withdrawal flow 8. [ ] Write tests for new repository methods 9. [ ] Run php-cs-fixer --- ## Related Files ### Entities - `src/Entity/Application.php` - `src/Entity/Disposition.php` - `src/Entity/Assignment.php` ### Repositories - `src/Repository/ApplicationRepository.php` - `src/Repository/DispositionRepository.php` ### Controllers - `src/Controller/Administrative/Disposition/CallOffController.php` - `src/Controller/Administrative/Assignment/CallOffController.php` - `src/Controller/Teamer/Disposition/DetailController.php` ### Listeners - `src/EventListener/InvalidateApplicationsListener.php` - `src/EventListener/RejectApplicationListener.php` ### Events - `src/Event/DispositionCalledOffEvent.php` - `src/Event/AssignmentCalledOffEvent.php` ### Forms - `src/Form/DispositionCallOffType.php` --- ## Notes - Filter statistics by **assignment travel dates**, not cancellation timestamp - Disposition tracking already works - only need repository query methods - Application tracking requires entity changes and migration - Consider whether teamer self-service withdrawal exists or needs to be added