# Solution Design Document

## Validation Checklist

- [x] All required sections are complete
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] All context sources are listed with relevance ratings
- [x] Project commands are discovered from actual project files
- [x] Constraints → Strategy → Design → Implementation path is logical
- [x] Architecture pattern is clearly stated with rationale
- [x] Every component in diagram has directory mapping
- [x] Every interface has specification
- [x] Error handling covers all error types
- [x] Quality requirements are specific and measurable
- [x] Every quality requirement has test coverage
- [x] **All architecture decisions confirmed by user**
- [x] Component names consistent across diagrams
- [x] A developer could implement from this design

---

## Constraints

**CON-1 Technology Stack**
- PHP 8.x with Slim 2.6.2 framework for backend
- MySQL for database (per-store databases named `kiosk_{typeNum}`)
- Twig 1.44.8 for server-side templates
- **SyncFusion EJ2 JavaScript Diagram Library** for floor plan editor (replacing Fabric.js)
- **SyncFusion EJ2 UI Components** for related UI controls where appropriate (date pickers, buttons, textboxes, textareas, radios/checkboxes, dropdowns/comboboxes, listbox, cards, avatars, messages/notifications, etc.)
- Bootstrap 5.3.3 for CSS framework (NOT Bootstrap 3)
- Modern browsers with canvas/SVG support (Chrome, Firefox, Safari, Edge)

**CON-2 Architecture Patterns**
- PSR-4 autoloading under `BuyerKiosk\` namespace
- Per-store database isolation (no cross-store data access)
- camelCase for all database columns and new table names
- Migration JSON format with `{{store}}` placeholder for per-store tables
- Migration operations may be limited to specific store types (e.g., Plato's Closet only) via an optional `storeTypes` filter in migration definitions
- Controllers follow existing pattern with lazy-loaded services
- API responses must include `success` boolean flag

**CON-3 Security & Access Control**
- All API endpoints require session authentication
- Permission-based access:
  - Admin floor plan access: `uri_floor_plans` (read), `uri_floor_plans_manage` (write)
  - Workbook access: `task-lists` (read), `workbook_complete_tasks` (task completion + bin assignment)
- Store group validation via `checkStoreGroup($typeNum)`
- CSRF token required for all mutations (from `meta[name="csrf_token"]`)
- No cross-store data access allowed
- Maintenance verification ("double check") is allowed for any user with `uri_floor_plans` (owners/managers)

**CON-4 Performance Targets**
- Floor plan canvas render: < 500ms initial load
- SyncFusion diagram initialization: < 300ms
- API response times: < 200ms for CRUD operations
- Heatmap calculation: < 2s for 30-day sales aggregation
- Support floor plans with up to 100 rack elements without degradation

**CON-5 SyncFusion Licensing**
- Must use SyncFusion EJ2 JavaScript Diagram Library
- Community license available for organizations < $1M revenue, ≤5 developers, ≤10 employees
- OR commercial license required for larger organizations
- Must include license key in initialization

## Implementation Context

**IMPORTANT**: You MUST read and analyze ALL listed context sources to understand constraints, patterns, and existing architecture.

### Required Context Sources

- **ICO-1 Database & Migration Patterns**
```yaml
- file: userfrosting/models/BaseModel.php
  relevance: HIGH
  sections: [dbConnectByName, getStoreFromID]
  why: "Database connection patterns and store access"

- file: userfrosting/migrations/methods/storeMigration.php
  relevance: HIGH
  why: "Per-store migration execution pattern"

- file: userfrosting/migrations/input/20251210_002_events_core.json
  relevance: MEDIUM
  why: "JSON migration format reference for floor plan tables"
```

- **ICO-2 Controller & API Patterns**
```yaml
- file: userfrosting/src/BuyerKiosk/EventManagement/Controllers/EventApiController.php
  relevance: HIGH
  why: "Modern API controller pattern with auth, validation, error handling"

- file: userfrosting/src/BuyerKiosk/EventManagement/Controllers/EventPageController.php
  relevance: HIGH
  why: "Admin page controller pattern with store context and permissions"

- file: userfrosting/routes/groups/backstock.php
  relevance: MEDIUM
  why: "Route grouping and REST endpoint patterns"

- file: userfrosting/src/BuyerKiosk/Core/Controllers/BaseController.php
  relevance: HIGH
  why: "Base controller class with permission checking utilities"
```

- **ICO-3 Frontend & Workbook Patterns**
```yaml
- file: userfrosting/templates/themes/default/workspace/workspace.html
  relevance: HIGH
  why: "Workbook SPA container for panel integration"

- file: userfrosting/src/BuyerKiosk/Workbook/Controllers/BackstockPanelController.php
  relevance: HIGH
  why: "Workbook panel API controller pattern"

- file: public_html/css/admin/tokens.css
  relevance: HIGH
  why: "Design system tokens for consistent styling"
```

- **ICO-4 Event Integration**
```yaml
- file: userfrosting/src/BuyerKiosk/EventManagement/Services/IntegrationService.php
  relevance: HIGH
  why: "Adapter pattern for event system integrations"

- file: userfrosting/src/BuyerKiosk/EventManagement/Adapters/AbstractAdapter.php
  relevance: HIGH
  why: "Base adapter class for integration implementations"

- file: userfrosting/src/BuyerKiosk/EventManagement/Models/EventIntegration.php
  relevance: MEDIUM
  why: "Integration type constants and model structure"
```

- **ICO-5 SyncFusion External Documentation**
```yaml
- url: https://ej2.syncfusion.com/javascript/documentation/diagram/serialization
  relevance: HIGH
  sections: [saveDiagram, loadDiagram, serializationSettings]
  why: "Diagram save/load API for persistence"

- url: https://ej2.syncfusion.com/javascript/documentation/diagram/symbol-palette/symbol-palette
  relevance: HIGH
  sections: [palette creation, custom symbols, drag-drop]
  why: "Symbol palette for rack templates"

- url: https://www.syncfusion.com/blogs/post/floor-planner-diagrams-in-javascript
  relevance: HIGH
  why: "Floor planner reference implementation patterns"
```

### Implementation Boundaries

- **Must Preserve**:
  - Existing POS category/subcategory structure (store-specific; e.g., `drsSubCategories` for Plato's Closet)
  - Existing Event Management integration adapter pattern
  - Existing workbook panel registration system
  - Permission model (Owner > Manager > Employee hierarchy)
  - Per-store database isolation

- **Can Modify**:
  - Add new tables to per-store databases
  - Add new routes to API router
  - Add new workbook panels via panel registry
  - Extend Event Integration types with TYPE_FLOORPLAN
  - Add new admin pages under /admin/:typeNum/floorplan

- **Must Not Touch**:
  - Core POS tables (`kiosk_sales.sales`, category tables)
  - Authentication/user tables in `kiosk_users`
  - Existing workbook panels (tasks, notes, schedule, etc.)
  - Core event management tables (add FK only, no modifications)

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    Owner[Store Owner<br/>Admin Panel] --> FloorPlan[Floor Plan<br/>Designer]
    Manager[Store Manager<br/>Admin Panel] --> FloorPlan
    Employee[Employee<br/>Workbook] --> FloorPlan

    FloorPlan --> StoreDB[(Per-Store DB<br/>kiosk_{typeNum})]
    FloorPlan --> SalesDB[(Sales DB<br/>kiosk_sales)]
    FloorPlan --> EventSystem[Event Management<br/>System]

    SalesDB --> Heatmap[Sales Heatmap<br/>Calculator]
    Heatmap --> FloorPlan

    EventSystem --> LayoutActivation[Scheduled Layout<br/>Activation]
    LayoutActivation --> FloorPlan

    SyncFusion[SyncFusion EJ2<br/>Diagram Library] --> FloorPlan
```

#### Interface Specifications

```yaml
# Inbound Interfaces (what calls this system)
inbound:
  - name: "Admin Panel Web Interface"
    type: HTTPS
    format: REST JSON
    authentication: Session (UserFrosting)
    data_flow: "Floor plan CRUD, layout management, heatmap requests"

  - name: "Workbook Web Interface"
    type: HTTPS
    format: REST JSON
    authentication: Session (UserFrosting)
    data_flow: "Read-only floor view, maintenance task completion"

  - name: "Event System Integration"
    type: Internal PHP
    format: Adapter Pattern
    authentication: N/A (same process)
    data_flow: "Layout activation triggers, event date sync"

# Outbound Interfaces (what this system calls)
outbound:
  - name: "Sales Data Query"
    type: MySQL Query
    format: SQL
    data_flow: "Aggregate sales by subcategory for heatmap"
    criticality: MEDIUM

  - name: "SyncFusion CDN"
    type: HTTPS
    format: JavaScript/CSS
    data_flow: "Load SyncFusion EJ2 Diagram library assets"
    criticality: HIGH

# Data Interfaces
data:
  - name: "Per-Store Database"
    type: MySQL
    connection: PDO via dbConnectByName()
    data_flow: "Floor plans, racks, sockets, layouts, maintenance logs"

  - name: "Sales Database"
    type: MySQL (kiosk_sales)
    connection: PDO
    data_flow: "Read-only sales aggregation for heatmaps"
```

### Cross-Component Boundaries

- **API Contracts**: Floor Plan REST API endpoints are internal contracts (not exposed to external systems)
- **Event Integration**: Must implement `IntegrationAdapterInterface` for event system compatibility
- **Shared Resources**:
  - Sales data (read-only access to `kiosk_sales.sales`)
  - POS categories (read-only access to subcategory mappings)
  - Employee data (read-only for audit/tracking)
- **Breaking Change Policy**: Database migrations must be backward compatible; API versioning not required for internal APIs

### Project Commands

```bash
# Testing
./test.sh                           # Run all tests
./test.sh --testsuite unit          # Run unit tests only
./test.sh --testsuite integration   # Run integration tests
./test.sh --coverage                # Run with coverage report
./test.sh --stan                    # Run tests + PHPStan analysis

# Static Analysis
cd userfrosting && ./vendor/bin/phpstan analyse src/BuyerKiosk/FloorPlan/

# CSS Build (after styling changes)
php userfrosting/conductor build-css           # Development build
php userfrosting/conductor build-css --minify  # Production build

# Database Migrations
php userfrosting/conductor run                 # Apply migrations

# Deployment
./deploy.sh                                    # Test + deploy

# Dependencies
cd userfrosting && composer install
```

## Design Assumptions

### Floor Plan Cardinality

**MVP supports exactly one floor plan per store.** The API remains `planId`-based for forward compatibility, but UI and services should enforce a single active plan per store (create if missing, otherwise edit existing).

### Heatmap Data Semantics - IMPORTANT CLARIFICATION

**The floor plan heatmaps visualize category performance projected onto current socket assignments, NOT actual per-rack POS tracking.**

This distinction is critical for understanding what the data represents:

| What Heatmaps Show | What Heatmaps Do NOT Show |
|--------------------|---------------------------|
| Sales volume by subcategory, displayed on sockets where that category is assigned | Which physical rack actually sold the item |
| "If Women's Dresses sold $5,000, and WD is on Socket 3, then Socket 3 shows $5,000" | "Socket 3 sold $5,000 worth of items" |
| Category performance visualization | Rack-specific POS data |

**Data Flow**:
1. Query `kiosk_sales.sales` aggregated by `catID` (subcategory code) (MVP: sales only; no special handling for returns/refunds)
2. Look up which sockets have that subcategory assigned in current layout (`fpSocketAssignments`)
3. Display the sales total on each socket where that category is assigned
4. If a category is on multiple sockets, **each socket shows the full category total** (not divided)

**Why This Design**:
- The PRD explicitly states "Won't Have: POS location tracking" - we don't know which rack rang up a sale
- This approach provides actionable insight: "high-performing categories should get prime floor space"
- It enables data-driven layout decisions without requiring POS hardware changes

---

## Solution Strategy

- **Architecture Pattern**: Layered MVC with Service Layer
  - Controllers handle HTTP concerns and authentication
  - Services encapsulate business logic and validation
  - Models represent domain entities with serialization
  - Repositories handle database operations (implicit in services)

- **Frontend Pattern**: SyncFusion EJ2 Diagram with Component Architecture
  - SyncFusion Diagram component for canvas editor
  - Symbol Palette for rack template drag-drop
  - Custom nodes for rack shapes with socket overlays
  - JSON serialization for diagram persistence

- **Integration Approach**:
  - New feature module under `BuyerKiosk\FloorPlan\` namespace
  - REST API endpoints following existing `/api/:typeNum/` pattern
  - Event system integration via `FloorPlanAdapter` implementing `IntegrationAdapterInterface`
  - Workbook panel registered in panel registry for employee access

- **Justification**:
  - Follows established codebase patterns for consistency
  - Per-store database isolation maintains data security
  - **SyncFusion EJ2 Diagram** provides professional-grade floor planning features:
    - Built-in symbol palette with drag-drop
    - Snap-to-grid and alignment guides
    - Pan/zoom with mouse and touch support
    - JSON serialization via `saveDiagram()`/`loadDiagram()`
    - Rulers and measurement tools
  - Event adapter pattern enables scheduled layout activations

- **Key Decisions**:
  1. Store floor plan canvas data as SyncFusion JSON (via `saveDiagram()`) - efficient and complete state capture
  2. Separate relational tables for racks, sockets, and assignments - enables SQL queries for heatmaps and reporting
  3. Dual storage: SyncFusion JSON for visual rendering + relational data for business logic
  4. Layouts stored as references to assignments, not duplicated diagram data
  5. Maintenance tracking uses combined score algorithm (time + traffic) per PRD

## Phased Delivery Strategy

**IMPORTANT**: This feature represents an entire subsystem (9+ tables, 4+ controllers, multiple services,
admin pages, workbook panels, event integration, and dual heatmaps). It MUST be delivered in phases,
with each phase providing independent value.

### Phase 1: Foundation (MVP)
**Goal**: Enable stores to create and manage floor plans with current state tracking.

| Component | Tables | Endpoints | UI |
|-----------|--------|-----------|-----|
| Floor Plan Designer | `fpFloorPlans`, `fpRackTypes`, `fpRacks`, `fpRackSockets` | CRUD for plans, racks, sockets | SyncFusion designer canvas, Symbol palette |
| Current Layout | `fpLayouts` (current only), `fpSocketAssignments` | Assignments CRUD | Socket assignment dropdowns |
| Permissions | New permissions in auth tables | — | Sidebar menu item |

**Deliverables**:
- Admin can create floor plan with SyncFusion canvas editor
- Admin can place racks from symbol palette and define sockets
- Admin can assign categories to sockets for "current" layout
- Employees see read-only floor view in workbook (simplified)

**Exit Criteria**: Store can document current floor configuration. No scheduled layouts, no heatmaps.

---

### Phase 1b: Usability Enhancements (Should)
**Goal**: Improve day-to-day usability without expanding core scope.

**Deliverables**:
- Background image import + calibration (scale/measure tool)
- Unique rack auto-naming (R1/R2/...) + find rack search
- Draft vs published workflow for current layout (edit draft, publish, discard)
- Export floor plan as JSON and PNG (published by default; optional draft)

**Exit Criteria**: Editing is safer and faster; support/debugging exports available.

**Dependency**: Phase 1 complete.

---

### Phase 2: Layout Planning
**Goal**: Enable future layout planning with diff-based task generation.

| Component | Tables | Endpoints | UI |
|-----------|--------|-----------|-----|
| Wanted Layouts | `fpLayouts` (wanted type), `fpMoveTasks` | Create wanted, diff, generate tasks | Layout manager, diff view |
| Move Task Tracking | `fpMoveTasks` | Task list, complete task | Task list in admin + workbook |
| Manual Activation | — | Activate layout | Activation button |

**Deliverables**:
- Admin can create "wanted" layouts for future states
- System calculates diff between current and wanted
- Move tasks are generated and trackable
- Admin can manually activate a wanted layout

**Exit Criteria**: Store can plan layout changes and track completion. No automated scheduling, no heatmaps.

**Dependency**: Phase 1 complete.

---

### Phase 3: Heatmaps & Maintenance
**Goal**: Add data visualization and maintenance tracking for operational optimization.

| Component | Tables | Endpoints | UI |
|-----------|--------|-----------|-----|
| Sales Heatmap | — (reads `kiosk_sales.sales`) | GET heatmap/sales | Canvas overlay, date picker |
| Maintenance Heatmap | `fpMaintenanceLogs`, `fpSettings` | GET heatmap/maintenance, POST maintain | Canvas overlay, maintenance panel |
| Workbook Maintenance Panel | — | GET workbook/maintenance | Priority queue UI |

**Deliverables**:
- Sales heatmap overlays category performance on floor plan
- Maintenance heatmap shows time-since-last-service
- Combined scoring algorithm (time + traffic) prioritizes racks
- Employees can mark racks as maintained via workbook

**Exit Criteria**: Operational insights available. No event integration.

**Dependency**: Phase 1 complete (Phase 2 not required).

---

### Phase 4: Event Integration
**Goal**: Enable automated layout scheduling tied to store events.

| Component | Tables | Endpoints | UI |
|-----------|--------|-----------|-----|
| Event Adapter | — (FK to events table) | Integration via adapter | Event wizard integration |
| Scheduled Activation | `fpLayouts.activationDate`, `fpAuditLog` | Auto-activation cron | Scheduled status display |
| Audit Trail | `fpAuditLog` | GET audit log | Audit log viewer |

**Deliverables**:
- Layouts can be linked to events
- Activation dates sync with event dates
- Cron job auto-activates scheduled layouts
- Full audit trail for compliance

**Exit Criteria**: Fully automated layout management tied to event calendar.

**Dependency**: Phase 2 complete + Event Management system deployed.

---

### Phase Dependency Graph

```
Phase 1 (Foundation)
    │
    ├──> Phase 2 (Layout Planning)
    │        │
    │        └──> Phase 4 (Event Integration)
    │
    └──> Phase 3 (Heatmaps & Maintenance)
```

**Recommended Delivery Order**: 1 → 2 → 3 → 4 (sequential for simplest coordination)
**Parallel Option**: After Phase 1, Phases 2 and 3 can be developed in parallel by separate developers.

## Building Block View

### Components

```mermaid
graph TB
    subgraph "Admin Panel"
        FPDesigner[Floor Plan Designer<br/>SyncFusion Canvas]
        LayoutMgr[Layout Manager<br/>Current + Scheduled]
        HeatmapView[Heatmap Reports<br/>Sales + Maintenance]
        SymbolPalette[Symbol Palette<br/>Rack Templates]
    end

    subgraph "Workbook"
        FloorPanel[Floor Panel<br/>Read-Only View]
        MaintPanel[Rack Maintenance<br/>Panel]
    end

    subgraph "API Layer"
        FloorPlanAPI[FloorPlanApiController]
        LayoutAPI[LayoutApiController]
        HeatmapAPI[HeatmapApiController]
        MaintenanceAPI[MaintenanceApiController]
    end

    subgraph "Service Layer"
        FloorPlanService[FloorPlanService]
        LayoutService[LayoutService]
        HeatmapService[HeatmapService]
        MaintenanceService[MaintenanceService]
        DiffService[LayoutDiffService]
    end

    subgraph "Event Integration"
        FloorPlanAdapter[FloorPlanAdapter]
        EventSystem[Event Management]
    end

    subgraph "Data Layer"
        StoreDB[(Per-Store DB)]
        SalesDB[(Sales DB)]
    end

    FPDesigner --> FloorPlanAPI
    SymbolPalette --> FPDesigner
    LayoutMgr --> LayoutAPI
    HeatmapView --> HeatmapAPI
    FloorPanel --> FloorPlanAPI
    MaintPanel --> MaintenanceAPI

    FloorPlanAPI --> FloorPlanService
    LayoutAPI --> LayoutService
    LayoutAPI --> DiffService
    HeatmapAPI --> HeatmapService
    MaintenanceAPI --> MaintenanceService

    FloorPlanService --> StoreDB
    LayoutService --> StoreDB
    HeatmapService --> StoreDB
    HeatmapService --> SalesDB
    MaintenanceService --> StoreDB
    DiffService --> StoreDB

    EventSystem --> FloorPlanAdapter
    FloorPlanAdapter --> LayoutService
```

### Directory Map

**Backend (PHP)**
```
userfrosting/
├── src/BuyerKiosk/FloorPlan/                    # NEW: Feature module
│   ├── Controllers/
│   │   ├── FloorPlanApiController.php           # NEW: Floor plan CRUD
│   │   ├── LayoutApiController.php              # NEW: Layout management
│   │   ├── HeatmapApiController.php             # NEW: Heatmap reports
│   │   ├── MaintenanceApiController.php         # NEW: Rack maintenance
│   │   └── FloorPlanPageController.php          # NEW: Admin pages
│   ├── Models/
│   │   ├── FloorPlan.php                        # NEW: Floor plan entity
│   │   ├── Rack.php                             # NEW: Rack entity
│   │   ├── RackSocket.php                       # NEW: Socket entity
│   │   ├── RackType.php                         # NEW: Rack template type
│   │   ├── Layout.php                           # NEW: Layout version
│   │   ├── SocketAssignment.php                 # NEW: Category-socket link
│   │   ├── MoveTask.php                         # NEW: Move task entity
│   │   └── MaintenanceLog.php                   # NEW: Maintenance record
│   ├── Services/
│   │   ├── FloorPlanService.php                 # NEW: Floor plan business logic
│   │   ├── LayoutService.php                    # NEW: Layout operations
│   │   ├── LayoutDiffService.php                # NEW: Diff calculation
│   │   ├── HeatmapService.php                   # NEW: Heatmap calculations
│   │   ├── MaintenanceService.php               # NEW: Maintenance scoring
│   │   └── AuditService.php                     # NEW: Audit logging
│   └── Adapters/
│       └── FloorPlanAdapter.php                 # NEW: Event integration
│
├── routes/
│   └── groups/floorplan.php                     # NEW: API routes
│
├── migrations/input/
│   ├── 20251212_000_pc_category_lookups.json    # NEW: drsCategories/drsSubCategories + PC-only seeding (from docs/*.csv)
│   ├── 20251212_001_floorplan_core.json         # NEW: Core tables
│   ├── 20251212_002_floorplan_layouts.json      # NEW: Layout tables
│   ├── 20251212_003_floorplan_maintenance.json  # NEW: Maintenance tables
│   └── 20251212_004_floorplan_permissions.json  # NEW: Permissions
│
└── templates/themes/default/
    ├── admin/floorplan/
    │   ├── dashboard.html                        # NEW: Admin dashboard
    │   ├── designer.html                         # NEW: SyncFusion canvas editor
    │   ├── layouts.html                          # NEW: Layout manager
    │   └── reports.html                          # NEW: Heatmap reports
    └── workspace/partials/
        ├── floor-panel.html                      # NEW: Workbook floor view
        └── maintenance-panel.html                # NEW: Workbook maintenance
```

**Frontend (JavaScript)**
```
public_html/
├── js/
│   ├── admin/floorplan/
│   │   ├── designer.js                          # NEW: SyncFusion Diagram wrapper
│   │   ├── symbol-palette.js                    # NEW: Rack template palette
│   │   ├── layout-manager.js                    # NEW: Layout UI
│   │   ├── heatmap-overlay.js                   # NEW: Heatmap rendering (dual mode)
│   │   ├── heatmap-socket-coloring.js           # NEW: Socket coloring algorithm
│   │   ├── heatmap-gradient.js                  # NEW: heatmap.js integration
│   │   └── rack-types.js                        # NEW: Rack type definitions
│   └── workspace/modules/floorplan/
│       ├── floor-panel.js                       # NEW: Workbook floor display
│       └── maintenance-panel.js                 # NEW: Maintenance list
│
├── css/admin/modules/
│   └── floorplan.css                            # NEW: Feature styles
│
└── vendor/
    ├── syncfusion/                              # NEW: SyncFusion assets (if not CDN)
    │   └── ej2/                                 # SyncFusion EJ2 library files
    └── heatmap.js/                              # NEW: heatmap.js library (MIT)
        └── heatmap.min.js                       # Via npm or direct download
```

### Interface Specifications

#### Data Storage Changes

**Database**: Per-store (`{{store}}` placeholder in migrations)

```yaml
# ============================================================
# CORE FLOOR PLAN TABLES
# ============================================================

Table: fpFloorPlans (NEW)
  Purpose: "Store floor plan with SyncFusion canvas data"
  Columns:
    id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
    name: VARCHAR(100) NOT NULL
    description: TEXT NULL
    canvasWidth: INT UNSIGNED NOT NULL DEFAULT 4000 COMMENT 'Canvas width in pixels'
    canvasHeight: INT UNSIGNED NOT NULL DEFAULT 3000 COMMENT 'Canvas height in pixels'
    gridSize: ENUM('small','medium','large') DEFAULT 'medium' COMMENT 'Preset size'
    backgroundImageUrl: VARCHAR(255) NULL COMMENT 'Optional user-uploaded background image URL (not stored in repo)'
    backgroundOpacity: DECIMAL(3,2) NOT NULL DEFAULT 0.30 COMMENT '0.00-1.00'
    measurementUnit: ENUM('ft') NOT NULL DEFAULT 'ft'
    pixelsPerUnit: DECIMAL(10,4) NULL COMMENT 'Scale calibration (pixels per foot); NULL = uncalibrated'
    diagramData: LONGTEXT NULL COMMENT 'SyncFusion saveDiagram() JSON output'
    thumbnail: LONGTEXT NULL COMMENT 'Base64 encoded preview image'
    isActive: TINYINT(1) UNSIGNED DEFAULT 1
    createdBy: INT UNSIGNED NULL COMMENT 'FK to kiosk_users.users.id (no FK constraint in store DB)'
    created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    updated_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
  Indexes:
    - idx_isActive (isActive)
    - idx_createdBy (createdBy)

Table: fpRackTypes (NEW)
  Purpose: "Rack template library - predefined and custom"
  Columns:
    id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
    name: VARCHAR(50) NOT NULL COMMENT 'e.g., Rounder, Gondola Shoe Deck'
    category: VARCHAR(50) NOT NULL COMMENT 'Gondola, Endcap, Freestanding, Wall, Combo, Service'
    description: VARCHAR(255) NULL
    defaultSocketCount: INT UNSIGNED DEFAULT 1
    defaultSocketNames: JSON NULL COMMENT '["Top", "Middle", "Bottom"]'
    icon: VARCHAR(50) NULL COMMENT 'Font Awesome icon class'
    shape: ENUM('rectangle','circle','cross','custom') DEFAULT 'rectangle'
    defaultWidth: INT UNSIGNED DEFAULT 100 COMMENT 'Default width in pixels'
    defaultHeight: INT UNSIGNED DEFAULT 80 COMMENT 'Default height in pixels'
    svgPath: TEXT NULL COMMENT 'Custom SVG path data for SyncFusion node'
    maintenanceIntervalDays: INT UNSIGNED DEFAULT 3 COMMENT 'Days between maintenance'
    isSystem: TINYINT(1) UNSIGNED DEFAULT 0 COMMENT '1=predefined, 0=custom'
    isActive: TINYINT(1) UNSIGNED DEFAULT 1
    created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  Indexes:
    - idx_isActive (isActive)
    - idx_isSystem (isSystem)
    - idx_category (category)

Table: fpRacks (NEW)
  Purpose: "Rack instances placed on floor plan"
  Columns:
    id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
    floorPlanId: INT UNSIGNED NOT NULL COMMENT 'FK to fpFloorPlans'
    rackTypeId: INT UNSIGNED NOT NULL COMMENT 'FK to fpRackTypes'
    name: VARCHAR(50) NOT NULL COMMENT 'Unique rack label (auto R1, R2, ...)'
    syncfusionNodeId: VARCHAR(50) NOT NULL COMMENT 'SyncFusion diagram node ID'
    positionX: DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 'X position on canvas'
    positionY: DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 'Y position on canvas'
    width: INT UNSIGNED NOT NULL DEFAULT 100
    height: INT UNSIGNED NOT NULL DEFAULT 80
    rotation: FLOAT DEFAULT 0 COMMENT 'Rotation in degrees'
    zIndex: INT DEFAULT 0 COMMENT 'Layer order'
    created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    updated_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
  Indexes:
    - idx_floorPlanId (floorPlanId)
    - idx_rackTypeId (rackTypeId)
    - uk_syncfusionNodeId (floorPlanId, syncfusionNodeId) UNIQUE
    - uk_floorPlan_rackName (floorPlanId, name) UNIQUE
  ForeignKeys:
    - fk_fpRacks_floorPlanId REFERENCES fpFloorPlans(id) ON DELETE CASCADE
    - fk_fpRacks_rackTypeId REFERENCES fpRackTypes(id) ON DELETE RESTRICT

Table: fpRackSockets (NEW)
  Purpose: "Individual sockets on each rack for category assignment"
  Columns:
    id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
    rackId: INT UNSIGNED NOT NULL COMMENT 'FK to fpRacks'
    name: VARCHAR(50) NOT NULL COMMENT 'e.g., Top Shelf, Shoe Display'
    sortOrder: INT UNSIGNED DEFAULT 0 COMMENT 'Display order within rack'
    created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  Indexes:
    - idx_rackId (rackId)
    - uk_rack_socket (rackId, name) UNIQUE
  ForeignKeys:
    - fk_fpRackSockets_rackId REFERENCES fpRacks(id) ON DELETE CASCADE

# ============================================================
# LAYOUT & CATEGORY ASSIGNMENT TABLES
# ============================================================

Table: fpLayouts (NEW)
  Purpose: "Layout versions - current and scheduled wanted layouts"
  Columns:
    id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
    floorPlanId: INT UNSIGNED NOT NULL COMMENT 'FK to fpFloorPlans'
    name: VARCHAR(100) NOT NULL COMMENT 'e.g., Holiday 2025, Summer Sale'
    description: TEXT NULL
    layoutType: ENUM('current','wanted') DEFAULT 'wanted'
    status: ENUM('draft','scheduled','active','archived') DEFAULT 'draft'
    activationDate: DATE NULL COMMENT 'When wanted layout becomes current'
    eventId: INT UNSIGNED NULL COMMENT 'FK to events table (if linked)'
    draftOfLayoutId: INT UNSIGNED NULL COMMENT 'If set, this layout is a draft of the referenced layout'
    activated_at: TIMESTAMP NULL COMMENT 'When layout became active'
    archived_at: TIMESTAMP NULL
    createdBy: INT UNSIGNED NULL COMMENT 'FK to kiosk_users.users.id (no FK constraint in store DB)'
    created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    updated_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
  Indexes:
    - idx_floorPlanId (floorPlanId)
    - idx_status (status)
    - idx_layoutType (layoutType)
    - idx_activationDate (activationDate)
    - idx_eventId (eventId)
    - idx_draftOfLayoutId (draftOfLayoutId)
  ForeignKeys:
    - fk_fpLayouts_floorPlanId REFERENCES fpFloorPlans(id) ON DELETE CASCADE
    - fk_fpLayouts_draftOfLayoutId REFERENCES fpLayouts(id) ON DELETE CASCADE

Table: fpSocketAssignments (NEW)
  Purpose: "Category assignments to sockets for a specific layout"
  Columns:
    id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
    layoutId: INT UNSIGNED NOT NULL COMMENT 'FK to fpLayouts'
    socketId: INT UNSIGNED NOT NULL COMMENT 'FK to fpRackSockets'
    subcategoryCode: VARCHAR(10) NOT NULL COMMENT 'POS subcategory code'
    sortOrder: INT UNSIGNED DEFAULT 0 COMMENT 'Order within socket'
    created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  Indexes:
    - idx_layoutId (layoutId)
    - idx_socketId (socketId)
    - idx_subcategoryCode (subcategoryCode)
    - uk_layout_socket_subcategory (layoutId, socketId, subcategoryCode) UNIQUE
  ForeignKeys:
    - fk_fpSocketAssignments_layoutId REFERENCES fpLayouts(id) ON DELETE CASCADE
    - fk_fpSocketAssignments_socketId REFERENCES fpRackSockets(id) ON DELETE CASCADE

# ============================================================
# STORE CATEGORY LOOKUP TABLES (PLATO'S CLOSET ONLY)
# ============================================================

Table: drsCategories (NEW, storeType=pc only)
  Purpose: "Store-specific category lookup for assignment UI + reporting"
  Seed Source: "docs/drsCategories.csv"
  Columns:
    catCode: VARCHAR(10) NOT NULL PRIMARY KEY COMMENT 'Matches POS category code'
    description: VARCHAR(100) NOT NULL
    isActive: TINYINT(1) UNSIGNED DEFAULT 1
    created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    updated_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
  Indexes:
    - idx_isActive (isActive)

Table: drsSubCategories (NEW, storeType=pc only)
  Purpose: "Store-specific subcategory lookup for assignment UI + heatmap labeling"
  Seed Source: "docs/drsSubCategories.csv"
  Columns:
    subCatCode: VARCHAR(10) NOT NULL PRIMARY KEY COMMENT 'Matches POS subcategory code (sales.catID)'
    description: VARCHAR(100) NOT NULL
    catCode: VARCHAR(10) NOT NULL COMMENT 'FK to drsCategories.catCode'
    labelShort: VARCHAR(10) NULL COMMENT 'Optional short label (e.g., LABEL_SH)'
    labelQty: INT UNSIGNED NULL COMMENT 'Optional label quantity (e.g., LABEL_QTY)'
    isActive: TINYINT(1) UNSIGNED DEFAULT 1
    created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    updated_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
  Indexes:
    - idx_catCode (catCode)
    - idx_isActive (isActive)
  ForeignKeys:
    - fk_drsSubCategories_catCode REFERENCES drsCategories(catCode) ON DELETE RESTRICT

Seeding Notes (storeType=pc only):
- These tables should be created + seeded only for Plato's Closet stores (storeType = `1`).
- Migrations should be **idempotent** and should not require manual store-owner action.
- Recommended migration approach: generate `INSERT` SQL from the CSV files and include it via migration `type: insert` with `sql` (batched inserts).
- Backstock alignment: seed `bsCategories` with the same category names for PC stores so floor/backstock workflows can share a consistent category set:
  - Insert missing only (do not overwrite/rename existing categories)
  - Suggested strategy: `INSERT ... SELECT ... WHERE NOT EXISTS` (match on `bsCategories.name`)

# ============================================================
# MAINTENANCE TRACKING TABLES
# ============================================================

Table: fpMaintenanceLogs (NEW)
  Purpose: "Track rack maintenance completions"
  Columns:
    id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
    rackId: INT UNSIGNED NOT NULL COMMENT 'FK to fpRacks'
    userId: INT UNSIGNED NOT NULL COMMENT 'kiosk_users.users.id (no FK constraint in store DB)'
    action: ENUM('done','double_checked') DEFAULT 'done'
    notes: TEXT NULL
    completed_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  Indexes:
    - idx_rackId (rackId)
    - idx_userId (userId)
    - idx_completed_at (completed_at)
    - idx_rack_completed (rackId, completed_at)
  ForeignKeys:
    - fk_fpMaintenanceLogs_rackId REFERENCES fpRacks(id) ON DELETE CASCADE

Table: fpMoveTasks (NEW)
  Purpose: "Generated atomic tasks for layout transitions"
  Columns:
    id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
    layoutId: INT UNSIGNED NOT NULL COMMENT 'FK to target fpLayouts'
    taskType: ENUM('add','remove') NOT NULL COMMENT 'Atomic task type'
    subcategoryCode: VARCHAR(10) NOT NULL
    fromSocketId: INT UNSIGNED NULL COMMENT 'Source socket (NULL if adding)'
    toSocketId: INT UNSIGNED NULL COMMENT 'Target socket (NULL if removing)'
    removalDisposition: ENUM('backstock') NULL COMMENT 'Only for remove tasks; dropped categories marked Gone do not generate tasks (audit only)'
    backstockBinId: INT UNSIGNED NULL COMMENT 'Optional destination bin for backstock disposition'
    taskDescription: VARCHAR(255) NOT NULL COMMENT 'Human-readable instruction'
    status: ENUM('pending','completed') DEFAULT 'pending'
    completedByUserId: INT UNSIGNED NULL COMMENT 'kiosk_users.users.id (no FK constraint in store DB)'
    completed_at: TIMESTAMP NULL
    created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  Indexes:
    - idx_layoutId (layoutId)
    - idx_status (status)
    - idx_subcategoryCode (subcategoryCode)
    - idx_completed_at (completed_at)
    - idx_backstockBinId (backstockBinId)
  ForeignKeys:
    - fk_fpMoveTasks_layoutId REFERENCES fpLayouts(id) ON DELETE CASCADE

# ============================================================
# AUDIT & SETTINGS TABLES
# ============================================================

Table: fpAuditLog (NEW)
  Purpose: "Track all floor plan changes for audit trail"
  Columns:
    id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
    entityType: ENUM('floorplan','rack','socket','layout','assignment','maintenance','task')
    entityId: INT UNSIGNED NOT NULL
    action: VARCHAR(50) NOT NULL COMMENT 'created, updated, deleted, activated, etc.'
    oldValue: JSON NULL
    newValue: JSON NULL
    userId: INT UNSIGNED NULL COMMENT 'kiosk_users.users.id (no FK constraint in store DB)'
    ipAddress: VARCHAR(45) NULL
    created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  Indexes:
    - idx_entityType_entityId (entityType, entityId)
    - idx_userId (userId)
    - idx_created_at (created_at)

Table: fpSettings (NEW)
  Purpose: "Store-level floor plan configuration"
  Columns:
    id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
    settingKey: VARCHAR(50) NOT NULL
    settingValue: TEXT NOT NULL
    updated_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
  Indexes:
    - uk_settingKey (settingKey) UNIQUE
  DefaultData:
    - ('doubleCheckRequired', '0')
    - ('maintenanceWeightFactor', '0.5')
    - ('defaultMaintenanceInterval', '3')
```

#### Internal API Changes

**Base Path**: `/api/:typeNum/floor-plan`

```yaml
# ============================================================
# FLOOR PLAN CRUD
# ============================================================

Endpoint: List Floor Plans
  Method: GET
  Path: /api/:typeNum/floor-plan/plans
  Permission: uri_floor_plans
  Notes: MVP returns 0 or 1 plans (single floor plan per store)
  Response:
    success: true
    data: [FloorPlan objects]
    count: number

Endpoint: Get Floor Plan
  Method: GET
  Path: /api/:typeNum/floor-plan/plans/:planId
  Permission: uri_floor_plans
  Response:
    success: true
    data: FloorPlan with racks, sockets, current layout
    diagramData: SyncFusion JSON (for loadDiagram)

Endpoint: Create Floor Plan
  Method: POST
  Path: /api/:typeNum/floor-plan/plans
  Permission: uri_floor_plans_manage
  Request:
    name: string (required, max 100)
    description: string (optional)
    gridSize: enum (small|medium|large)
  Response:
    success: true
    data: FloorPlan object
    message: "Floor plan created successfully"

Endpoint: Update Floor Plan
  Method: PUT
  Path: /api/:typeNum/floor-plan/plans/:planId
  Permission: uri_floor_plans_manage
  Request:
    name: string (optional)
    description: string (optional)
    backgroundImageUrl: string|null (optional)
    backgroundOpacity: number (optional, 0.00-1.00)
    measurementUnit: enum (ft) (optional)
    pixelsPerUnit: number|null (optional) # scale calibration
    diagramData: string (optional, SyncFusion saveDiagram() output)
    thumbnail: string (optional, base64)
  Response:
    success: true
    data: FloorPlan object

Endpoint: Upload Background Image
  Method: POST
  Path: /api/:typeNum/floor-plan/plans/:planId/background-image
  Permission: uri_floor_plans_manage
  Description: Upload a background image for a floor plan; stored on local filesystem (not in repo) and referenced by URL in `fpFloorPlans.backgroundImageUrl`
  Request:
    multipart/form-data:
      file: image (required)
  Notes:
    - Defaults: allow png/jpg/jpeg/webp; max size 5MB (configurable later if needed)
    - Response should return the stored URL which can also be persisted via Update Floor Plan
    - Storage: write to local filesystem under a non-repo directory (or gitignored path) and serve same-origin to avoid a CORS-tainted canvas breaking PNG export
    - Validation: do not trust extension; validate by MIME sniffing; disallow SVG
    - Lifecycle: if a new background is uploaded for the same plan, delete/replace the prior file; deleting a plan should delete its background file(s)
  Response:
    success: true
    data:
      backgroundImageUrl: string

Endpoint: Delete Floor Plan
  Method: DELETE
  Path: /api/:typeNum/floor-plan/plans/:planId
  Permission: uri_floor_plans_manage
  Response:
    success: true
    message: "Floor plan deleted"

# ============================================================
# RACK MANAGEMENT
# ============================================================

Endpoint: List Rack Types
  Method: GET
  Path: /api/:typeNum/floor-plan/rack-types
  Permission: uri_floor_plans
  Query:
    category: string (optional filter by category)
  Response:
    success: true
    data: [RackType objects grouped by category]

Endpoint: Create Custom Rack Type
  Method: POST
  Path: /api/:typeNum/floor-plan/rack-types
  Permission: uri_floor_plans_manage
  Request:
    name: string (required)
    category: string (required)
    defaultSocketCount: number (1-10)
    defaultSocketNames: array of strings
    maintenanceIntervalDays: number (1-30)
  Response:
    success: true
    data: RackType object

Endpoint: Sync Racks from Diagram
  Method: POST
  Path: /api/:typeNum/floor-plan/plans/:planId/sync-racks
  Permission: uri_floor_plans_manage
  Description: Sync rack positions from SyncFusion diagram state
  Request:
    racks: [{ syncfusionNodeId, rackTypeId, name?, positionX, positionY, rotation, width, height }]
  Response:
    success: true
    data: { created: number, updated: number, deleted: number }

Endpoint: Update Rack
  Method: PUT
  Path: /api/:typeNum/floor-plan/racks/:rackId
  Permission: uri_floor_plans_manage
  Request:
    name: string
    positionX: number
    positionY: number
    rotation: number
  Response:
    success: true
    data: Rack object

Endpoint: Find Racks
  Method: GET
  Path: /api/:typeNum/floor-plan/plans/:planId/racks/find
  Permission: uri_floor_plans
  Query:
    query: string (required; matches rack name)
  Response:
    success: true
    data: [{ rackId, rackName, syncfusionNodeId }]

Endpoint: Delete Rack
  Method: DELETE
  Path: /api/:typeNum/floor-plan/racks/:rackId
  Permission: uri_floor_plans_manage
  Response:
    success: true
    message: "Rack deleted"

Endpoint: Update Socket Name
  Method: PUT
  Path: /api/:typeNum/floor-plan/sockets/:socketId
  Permission: uri_floor_plans_manage
  Request:
    name: string (required)
  Response:
    success: true
    data: Socket object

# ============================================================
# LAYOUT MANAGEMENT
# ============================================================

Endpoint: List Layouts
  Method: GET
  Path: /api/:typeNum/floor-plan/plans/:planId/layouts
  Permission: uri_floor_plans
  Query:
    status: enum (optional, filter by status)
  Response:
    success: true
    data: [Layout objects]

Endpoint: Get Current Layout
  Method: GET
  Path: /api/:typeNum/floor-plan/plans/:planId/layouts/current
  Permission: uri_floor_plans
  Response:
    success: true
    data: Layout with all socket assignments

Endpoint: Get Draft Current Layout
  Method: GET
  Path: /api/:typeNum/floor-plan/plans/:planId/layouts/current/draft
  Permission: uri_floor_plans_manage
  Response:
    success: true
    data: Layout|null

Endpoint: Create/Reset Draft Current Layout
  Method: POST
  Path: /api/:typeNum/floor-plan/plans/:planId/layouts/current/draft
  Permission: uri_floor_plans_manage
  Description: Create a draft by copying current assignments (replaces existing draft with confirmation)
  Response:
    success: true
    data: Layout

Endpoint: Discard Draft Current Layout
  Method: DELETE
  Path: /api/:typeNum/floor-plan/plans/:planId/layouts/current/draft
  Permission: uri_floor_plans_manage
  Response:
    success: true
    message: "Draft discarded"

Endpoint: Create Wanted Layout
  Method: POST
  Path: /api/:typeNum/floor-plan/plans/:planId/layouts
  Permission: uri_floor_plans_manage
  Request:
    name: string (required)
    description: string (optional)
    activationDate: date (optional, YYYY-MM-DD)
    eventId: number (optional, link to event)
    copyFromLayoutId: number (optional, copy assignments from existing)
  Response:
    success: true
    data: Layout object

Endpoint: Update Layout
  Method: PUT
  Path: /api/:typeNum/floor-plan/layouts/:layoutId
  Permission: uri_floor_plans_manage
  Request:
    name: string
    description: string
    activationDate: date
    eventId: number (null to unlink)
  Response:
    success: true
    data: Layout object

Endpoint: Delete Layout
  Method: DELETE
  Path: /api/:typeNum/floor-plan/layouts/:layoutId
  Permission: uri_floor_plans_manage
  Validation: Cannot delete current/active layout
  Response:
    success: true
    message: "Layout deleted"

# ============================================================
# CATEGORY ASSIGNMENTS
# ============================================================

Endpoint: Get Layout Assignments
  Method: GET
  Path: /api/:typeNum/floor-plan/layouts/:layoutId/assignments
  Permission: uri_floor_plans
  Response:
    success: true
    data: [SocketAssignment objects grouped by socket]

Endpoint: Assign Category to Socket
  Method: POST
  Path: /api/:typeNum/floor-plan/layouts/:layoutId/assignments
  Permission: uri_floor_plans_manage
  Request:
    socketId: number (required)
    subcategoryCode: string (required)
  Response:
    success: true
    data: Assignment object

Endpoint: Bulk Update Assignments
  Method: PUT
  Path: /api/:typeNum/floor-plan/layouts/:layoutId/assignments
  Permission: uri_floor_plans_manage
  Request:
    assignments: [{ socketId, subcategoryCodes: [] }]
  Response:
    success: true
    data: { updated: number }

Endpoint: Remove Assignment
  Method: DELETE
  Path: /api/:typeNum/floor-plan/assignments/:assignmentId
  Permission: uri_floor_plans_manage
  Response:
    success: true
    message: "Assignment removed"

# ============================================================
# LAYOUT DIFF & TASKS
# ============================================================

Endpoint: Compare Layouts (Diff)
  Method: GET
  Path: /api/:typeNum/floor-plan/layouts/:layoutId/diff
  Permission: uri_floor_plans
  Description: Compare wanted layout to current layout
  Response:
    success: true
    data:
      additions: [{ subcategoryCode, subcategoryName, toSocket, toRack }]
      removals: [{ subcategoryCode, subcategoryName, fromSocket, fromRack }]
      moves: [{ subcategoryCode, subcategoryName, fromSocket, fromRack, toSocket, toRack }]
      dropped: [{ subcategoryCode, subcategoryName, fromSocket, fromRack }] # removals not part of a move; requires resolution before task generation
      summary: { addCount, removeCount, moveCount }

Endpoint: Generate Move Tasks
  Method: POST
  Path: /api/:typeNum/floor-plan/layouts/:layoutId/generate-tasks
  Permission: uri_floor_plans_manage
  Description: Create atomic tasks from diff; requires resolutions for dropped categories
  Request:
    droppedResolutions: [{ subcategoryCode, fromSocketId, resolution (gone|backstock), removalReason?, backstockBinId? }]
  Notes:
    - resolution=gone: write to fpAuditLog (details include optional removalReason); do NOT create a task
    - resolution=backstock: create a remove task; backstockBinId may be omitted and provided later (completion is allowed without bin)
  Response:
    success: true
    data: { taskCount, tasks: [MoveTask objects] }

Endpoint: List Move Tasks
  Method: GET
  Path: /api/:typeNum/floor-plan/layouts/:layoutId/tasks
  Permission: uri_floor_plans
  Query:
    status: enum (pending|completed)
  Response:
    success: true
    data: [MoveTask objects]

Endpoint: Complete Move Task
  Method: POST
  Path: /api/:typeNum/floor-plan/tasks/:taskId/complete
  Permission: workbook_complete_tasks
  Request:
    backstockBinId: number (optional; can be provided now or later)
  Notes:
    - UI may create a new empty bin via existing Backstock endpoints (`POST /:typeNum/backstock/bin`) and then pass the created bin ID here
    - If the task is a Backstock removal and no bin is provided, the task may still be completed and the bin can be assigned later
  Response:
    success: true
    data: MoveTask object

Endpoint: Set Backstock Bin for Task
  Method: POST
  Path: /api/:typeNum/floor-plan/tasks/:taskId/set-backstock-bin
  Permission: workbook_complete_tasks
  Description: Assign a backstock bin to an already-generated (or completed) backstock remove task
  Request:
    backstockBinId: number (required)
  Response:
    success: true
    data: MoveTask object

Endpoint: Activate Layout
  Method: POST
  Path: /api/:typeNum/floor-plan/layouts/:layoutId/activate
  Permission: uri_floor_plans_manage
  Description: Manually activate a wanted layout
  Response:
    success: true
    data: Layout object
    message: "Layout activated"

Endpoint: Publish Draft Current Layout
  Method: POST
  Path: /api/:typeNum/floor-plan/plans/:planId/layouts/current/publish
  Permission: uri_floor_plans_manage
  Description: Publish the current draft (if any) to become the new current layout
  Response:
    success: true
    data: Layout object
    message: "Draft published"

# ============================================================
# HEATMAPS & REPORTS
# ============================================================

Endpoint: Get Sales Heatmap Data
  Method: GET
  Path: /api/:typeNum/floor-plan/plans/:planId/heatmap/sales
  Permission: uri_floor_plans
  Query:
    startDate: date (default: 30 days ago)
    endDate: date (default: today)
  Response:
    success: true
    data:
      sockets: [{ socketId, rackId, totalSales, subcategoryBreakdown }]
      range: { min, max, average }
      period: { startDate, endDate, dayCount }

Endpoint: Get Maintenance Heatmap Data
  Method: GET
  Path: /api/:typeNum/floor-plan/plans/:planId/heatmap/maintenance
  Permission: uri_floor_plans
  Response:
    success: true
    data:
      racks: [{ rackId, daysSinceMaintenance, lastMaintainedBy, score }]
      overdueCount: number

# ============================================================
# EXPORTS
# ============================================================

Endpoint: Export Floor Plan JSON
  Method: GET
  Path: /api/:typeNum/floor-plan/plans/:planId/export/json
  Permission: uri_floor_plans
  Query:
    layout: enum (published|draft) default published
  Response:
    success: true
    data:
      floorPlan: { id, name, canvasWidth, canvasHeight, gridSize, backgroundImageUrl?, backgroundOpacity?, measurementUnit?, pixelsPerUnit? }
      diagramData: string|null
      layout: { id, name, status }
      assignments: [{ socketId, subcategoryCodes: [] }]

Endpoint: Export Floor Plan PNG
  Method: GET
  Path: /api/:typeNum/floor-plan/plans/:planId/export/png
  Permission: uri_floor_plans
  Query:
    layout: enum (published|draft) default published
  Response:
    Content-Type: image/png

# ============================================================
# RACK MAINTENANCE
# ============================================================

Endpoint: Get Maintenance Queue
  Method: GET
  Path: /api/:typeNum/floor-plan/maintenance
  Permission: uri_floor_plans
  Description: Prioritized list for workbook panel
  Response:
    success: true
    data: [{ rack, rackName, rackType, score, daysSince, lastMaintenance }]

Endpoint: Mark Rack Maintained
  Method: POST
  Path: /api/:typeNum/floor-plan/racks/:rackId/maintain
  Permission: uri_floor_plans (employee can mark)
  Request:
    action: enum (done|double_checked)
    notes: string (optional)
  Response:
    success: true
    data: MaintenanceLog object

# ============================================================
# WORKBOOK ENDPOINTS
# ============================================================

Endpoint: Get Workbook Floor View
  Method: GET
  Path: /api/:typeNum/floor-plan/workbook/floor
  Permission: task-lists
  Response:
    success: true
    data:
      floorPlan: { id, name, thumbnail, diagramData }
      currentLayout: { id, name }

Endpoint: Get Workbook Maintenance List
  Method: GET
  Path: /api/:typeNum/floor-plan/workbook/maintenance
  Permission: task-lists
  Response:
    success: true
    data:
      maintenanceTasks: [{ rackId, rackName, priority, daysSince }]
      moveTasks: [{ id, description, status }]
      pendingCount: number
```

#### Application Data Models

```pseudocode
ENTITY: FloorPlan (NEW)
  FIELDS:
    id: int
    name: string
    description: string|null
    canvasWidth: int
    canvasHeight: int
    gridSize: enum(small|medium|large)
    backgroundImageUrl: string|null
    backgroundOpacity: float
    measurementUnit: enum(ft)
    pixelsPerUnit: float|null
    diagramData: string|null (SyncFusion JSON)
    thumbnail: string|null
    isActive: bool
    createdBy: int|null
    createdAt: datetime
    updatedAt: datetime

  BEHAVIORS:
    toArray(): array - Serialize for API response
    createFromRow(array): void - Hydrate from DB row
    getPresetDimensions(): array - Get width/height for gridSize
    getCurrentLayout(): Layout|null - Get active layout
    getRacks(): array<Rack> - Get all rack instances

ENTITY: RackType (NEW)
  FIELDS:
    id: int
    name: string
    category: string
    description: string|null
    defaultSocketCount: int
    defaultSocketNames: array
    icon: string|null
    shape: enum(rectangle|circle|cross|custom)
    defaultWidth: int
    defaultHeight: int
    svgPath: string|null
    maintenanceIntervalDays: int
    isSystem: bool
    isActive: bool

  BEHAVIORS:
    toArray(): array
    createFromRow(array): void
    generateSockets(Rack): array<RackSocket> - Create default sockets
    getSyncFusionNodeConfig(): array - Generate SyncFusion node definition

ENTITY: Rack (NEW)
  FIELDS:
    id: int
    floorPlanId: int
    rackTypeId: int
    name: string
    syncfusionNodeId: string
    positionX: float
    positionY: float
    width: int
    height: int
    rotation: float
    zIndex: int

  BEHAVIORS:
    toArray(): array
    createFromRow(array): void
    getSockets(): array<RackSocket>
    getLastMaintenance(): MaintenanceLog|null
    getDaysSinceLastMaintenance(): int
    getMaintenanceScore(float trafficMultiplier): float

ENTITY: RackSocket (NEW)
  FIELDS:
    id: int
    rackId: int
    name: string
    sortOrder: int

  BEHAVIORS:
    toArray(): array
    getAssignments(int layoutId): array<SocketAssignment>

ENTITY: Layout (NEW)
  FIELDS:
    id: int
    floorPlanId: int
    name: string
    description: string|null
    layoutType: enum(current|wanted)
    status: enum(draft|scheduled|active|archived)
    activationDate: date|null
    eventId: int|null
    activatedAt: datetime|null
    archivedAt: datetime|null
    createdBy: int|null

  BEHAVIORS:
    toArray(): array
    createFromRow(array): void
    getAssignments(): array<SocketAssignment>
    isCurrent(): bool
    isScheduled(): bool
    canBeDeleted(): bool
    activate(): void - Transition to active status
    archive(): void - Transition to archived status

ENTITY: SocketAssignment (NEW)
  FIELDS:
    id: int
    layoutId: int
    socketId: int
    subcategoryCode: string
    sortOrder: int

  BEHAVIORS:
    toArray(): array
    getSubcategoryName(): string - Lookup from store category lookup (e.g., drsSubCategories for Plato's Closet)

ENTITY: MoveTask (NEW)
  FIELDS:
    id: int
    layoutId: int
    taskType: enum(add|remove)
    subcategoryCode: string
    fromSocketId: int|null
    toSocketId: int|null
    removalDisposition: enum(backstock)|null
    backstockBinId: int|null
    taskDescription: string
    status: enum(pending|completed)
    completedByUserId: int|null
    completedAt: datetime|null

  BEHAVIORS:
    toArray(): array
    complete(int userId): void

ENTITY: MaintenanceLog (NEW)
  FIELDS:
    id: int
    rackId: int
    userId: int
    action: enum(done|double_checked)
    notes: string|null
    completedAt: datetime

  BEHAVIORS:
    toArray(): array
    getUserName(): string
```

#### Integration Points

```yaml
# Event System Integration
EventSystem:
  adapter: FloorPlanAdapter implements IntegrationAdapterInterface
  integration_type: TYPE_FLOORPLAN (new constant)
  capabilities:
    - Link layouts to events for automatic activation
    - Sync activation dates when event dates change
    - Generate move tasks on event build-up phase
  critical_data:
    - layoutId: Which layout to activate
    - config: { autoGenerateTasks: bool }

# POS Sales Data Integration
SalesData:
  source: kiosk_sales.sales table
  access: Read-only via SQL query
  semantics: "Gross sales by subcategory (SUM(price))"
  query_pattern: |
    SELECT catID as subcategoryCode, SUM(price) as totalSales
    FROM kiosk_sales.sales
    WHERE storeID = :storeId
    AND date BETWEEN :startDate AND :endDate
    GROUP BY catID
  critical_data:
    - subcategoryCode: Maps to fpSocketAssignments.subcategoryCode
    - totalSales: Used for heatmap color calculation

# POS Category Data
CategoryMapping:
  source: Store DB lookup tables seeded per store type
  access: Read-only
  data_flow: Subcategory codes and display names for assignment UI
  plato_specific:
    tables:
      - drsCategories (seeded from docs/drsCategories.csv)
      - drsSubCategories (seeded from docs/drsSubCategories.csv)

# SyncFusion Diagram Integration
SyncFusionDiagram:
  library: @syncfusion/ej2-diagrams
  version: Latest stable (2025.x)
  key_methods:
    - saveDiagram(): Serialize diagram state to JSON
    - loadDiagram(json): Restore diagram from JSON
    - addPaletteItem(): Add rack to symbol palette
    - nodeDefaults: Configure rack appearance

# Heatmap.js Integration (for Gradient Mode)
HeatmapJS:
  library: heatmap.js
  version: 2.0.5 (MIT license)
  install: npm install heatmap.js
  github: https://github.com/pa7/heatmap.js
  key_methods:
    - h337.create({ container }): Create heatmap instance
    - setData({ min, max, data }): Set data points
    - addData({ x, y, value }): Add individual point
    - repaint(): Force redraw
  integration:
    - Create heatmap container as sibling to SyncFusion canvas
    - Position absolutely over diagram with pointer-events: none
    - Synchronize pan/zoom with SyncFusion diagram viewport
```

### Implementation Examples

#### Example: SyncFusion Diagram Initialization

**Why this example**: Shows how to set up SyncFusion Diagram with floor plan specific configuration.

```javascript
/**
 * Initialize SyncFusion Diagram for floor plan editing
 */
class FloorPlanDesigner {
    constructor(containerId, options = {}) {
        this.containerId = containerId;
        this.typeNum = options.typeNum;
        this.csrfToken = document.querySelector('meta[name="csrf_token"]').content;
        this.diagram = null;
        this.palette = null;
    }

    async init() {
        // Initialize diagram
        this.diagram = new ej.diagrams.Diagram({
            width: '100%',
            height: '600px',
            // Grid and snapping for alignment
            snapSettings: {
                horizontalGridlines: { lineColor: '#e0e0e0', lineIntervals: [1, 9, 0.25, 9.75, 0.25] },
                verticalGridlines: { lineColor: '#e0e0e0', lineIntervals: [1, 9, 0.25, 9.75, 0.25] },
                constraints: ej.diagrams.SnapConstraints.ShowLines | ej.diagrams.SnapConstraints.SnapToLines
            },
            // Rulers for measurement
            rulerSettings: {
                showRulers: true,
                horizontalRuler: { thickness: 25, segmentWidth: 50 },
                verticalRuler: { thickness: 25, segmentWidth: 50 }
            },
            // Optimize serialization
            serializationSettings: { preventDefaults: true },
            // Event handlers
            selectionChange: (args) => this.onSelectionChange(args),
            positionChange: (args) => this.onNodePositionChange(args),
            sizeChange: (args) => this.onNodeSizeChange(args),
        });

        this.diagram.appendTo(`#${this.containerId}`);

        // Initialize symbol palette
        await this.initSymbolPalette();
    }

    async initSymbolPalette() {
        const rackTypes = await this.fetchRackTypes();
        const palettes = this.buildPalettes(rackTypes);

        this.palette = new ej.diagrams.SymbolPalette({
            width: '100%',
            height: '100%',
            palettes: palettes,
            symbolHeight: 60,
            symbolWidth: 60,
            symbolMargin: { left: 10, right: 10, top: 10, bottom: 10 },
            getSymbolInfo: (symbol) => ({
                description: { text: symbol.id }
            })
        });

        this.palette.appendTo('#symbolPalette');
    }

    buildPalettes(rackTypes) {
        // Group rack types by category
        const categories = {};
        rackTypes.forEach(type => {
            if (!categories[type.category]) {
                categories[type.category] = [];
            }
            categories[type.category].push(this.createRackSymbol(type));
        });

        return Object.keys(categories).map(category => ({
            id: category.toLowerCase().replace(/\s+/g, '-'),
            expanded: category === 'Freestanding',
            symbols: categories[category],
            title: category
        }));
    }

    createRackSymbol(rackType) {
        const shape = this.getShapeConfig(rackType);
        return {
            id: `rack-type-${rackType.id}`,
            width: rackType.defaultWidth,
            height: rackType.defaultHeight,
            shape: shape,
            style: { fill: '#f0f4f8', strokeColor: '#4a5568', strokeWidth: 2 },
            addInfo: { rackTypeId: rackType.id, rackTypeName: rackType.name }
        };
    }

    getShapeConfig(rackType) {
        switch (rackType.shape) {
            case 'circle':
                return { type: 'Basic', shape: 'Ellipse' };
            case 'cross':
                return { type: 'Basic', shape: 'Plus' };
            case 'custom':
                return { type: 'Path', data: rackType.svgPath };
            default:
                return { type: 'Basic', shape: 'Rectangle' };
        }
    }

    // Save diagram to server
    async save() {
        const diagramData = this.diagram.saveDiagram();
        const response = await fetch(`/api/${this.typeNum}/floor-plan/plans/${this.planId}`, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': this.csrfToken
            },
            body: JSON.stringify({ diagramData })
        });
        return response.json();
    }

    // Load diagram from server
    async load(planId) {
        this.planId = planId;
        const response = await fetch(`/api/${this.typeNum}/floor-plan/plans/${planId}`);
        const data = await response.json();
        if (data.success && data.data.diagramData) {
            this.diagram.loadDiagram(data.data.diagramData);
        }
    }
}
```

#### Example: Maintenance Priority Score Calculation

**Why this example**: The combined score algorithm is critical business logic that weighs time and traffic factors per PRD requirements.

```php
/**
 * Calculate maintenance priority score for a rack.
 * Higher score = more urgent maintenance needed.
 *
 * Formula: baseScore × (1 + trafficMultiplier × weightFactor)
 * Where:
 *   baseScore = daysSinceLastMaintenance / maintenanceInterval
 *   trafficMultiplier = socketSalesVolume / averageSalesVolume
 *   weightFactor = configurable store setting (default 0.5)
 */
public function calculateMaintenanceScore(Rack $rack, float $weightFactor = 0.5): float
{
    $interval = $rack->getRackType()->maintenanceIntervalDays;
    $daysSince = $rack->getDaysSinceLastMaintenance();

    // Base score: how overdue is this rack?
    $baseScore = $daysSince / max($interval, 1);

    // Get traffic data for this rack's sockets
    $socketSales = $this->heatmapService->getSocketSalesTotal(
        $rack->getSockets(),
        $this->getCurrentLayoutId()
    );
    $averageSales = $this->heatmapService->getAverageSocketSales();

    // Traffic multiplier: normalized sales volume (capped at 2.0)
    $trafficMultiplier = $averageSales > 0
        ? min($socketSales / $averageSales, 2.0)
        : 0;

    // Combined score
    return $baseScore * (1 + $trafficMultiplier * $weightFactor);
}
```

#### Example: Layout Diff Algorithm

**Why this example**: The diff calculation drives task generation - critical for layout transitions.

```php
/**
 * Compare two layouts and identify all assignment changes.
 * Returns additions, removals, and moves at the socket level.
 */
public function calculateDiff(int $currentLayoutId, int $wantedLayoutId): array
{
    // Multi-value maps: subcategoryCode => [socketId, socketId, ...]
    $currentAssignments = $this->getAssignmentMap($currentLayoutId);
    $wantedAssignments = $this->getAssignmentMap($wantedLayoutId);

    $additions = [];
    $removals = [];
    $moves = [];

    // Get all subcategory codes from both layouts
    $allSubcategoryCodes = array_unique(array_merge(
        array_keys($currentAssignments),
        array_keys($wantedAssignments)
    ));

    foreach ($allSubcategoryCodes as $subcatCode) {
        $currentSockets = $currentAssignments[$subcatCode] ?? [];
        $wantedSockets = $wantedAssignments[$subcatCode] ?? [];

        // Sockets that are new (in wanted but not current)
        $addedSockets = array_diff($wantedSockets, $currentSockets);
        // Sockets being removed (in current but not wanted)
        $removedSockets = array_diff($currentSockets, $wantedSockets);

        // Pair up removals with additions as "moves" where possible
        $removedList = array_values($removedSockets);
        $addedList = array_values($addedSockets);

        $moveCount = min(count($removedList), count($addedList));
        for ($i = 0; $i < $moveCount; $i++) {
            $moves[] = [
                'subcategoryCode' => $subcatCode,
                'fromSocketId' => $removedList[$i],
                'toSocketId' => $addedList[$i]
            ];
        }

        // Remaining additions
        for ($i = $moveCount; $i < count($addedList); $i++) {
            $additions[] = [
                'subcategoryCode' => $subcatCode,
                'toSocketId' => $addedList[$i],
                'fromSocketId' => null
            ];
        }

        // Remaining removals
        for ($i = $moveCount; $i < count($removedList); $i++) {
            $removals[] = [
                'subcategoryCode' => $subcatCode,
                'fromSocketId' => $removedList[$i],
                'toSocketId' => null
            ];
        }
    }

    return [
        'additions' => $additions,
        'removals' => $removals,
        'moves' => $moves,
        'summary' => [
            'addCount' => count($additions),
            'removeCount' => count($removals),
            'moveCount' => count($moves)
        ]
    ];
}
```

## Runtime View

### Primary Flow: Floor Plan Creation with SyncFusion

1. Owner opens Floor Plan Designer in Admin Panel
2. System loads SyncFusion Diagram and Symbol Palette
3. Owner selects canvas size preset (Small/Medium/Large)
4. System creates floor plan record with dimensions
5. Owner drags rack symbols from palette onto canvas
6. System creates rack + socket records for each placement
7. Owner assigns categories to sockets via dropdown panel
8. System creates socket assignment records for "current" layout
9. Owner clicks Save; system calls `saveDiagram()` and persists JSON
10. Subsequent loads restore diagram via `loadDiagram()`

```mermaid
sequenceDiagram
    actor Owner
    participant Designer as Floor Plan Designer
    participant Diagram as SyncFusion Diagram
    participant API as FloorPlanApiController
    participant Service as FloorPlanService
    participant DB as Per-Store DB

    Owner->>Designer: Open Designer Page
    Designer->>Diagram: Initialize diagram + palette
    Diagram-->>Designer: Ready

    Owner->>Diagram: Drag rack from palette
    Diagram-->>Designer: Node added event
    Designer->>API: POST /sync-racks
    API->>Service: syncRacks()
    Service->>DB: INSERT fpRacks, fpRackSockets
    DB-->>Service: rack with sockets
    Service-->>API: Rack object
    API-->>Designer: {success: true}

    Owner->>Designer: Save floor plan
    Designer->>Diagram: saveDiagram()
    Diagram-->>Designer: JSON string
    Designer->>API: PUT /plans/:id {diagramData}
    API->>Service: updateFloorPlan()
    Service->>DB: UPDATE fpFloorPlans
    DB-->>Service: updated
    Service-->>API: FloorPlan object
    API-->>Designer: {success: true}
```

### Error Handling

| Error Type | HTTP Code | Error Code | User Message | Recovery |
|------------|-----------|------------|--------------|----------|
| Missing required field | 400 | VALIDATION_ERROR | "Floor plan name is required" | Highlight field, show error |
| Floor plan not found | 404 | NOT_FOUND | "Floor plan not found" | Redirect to list |
| Cannot delete current layout | 400 | BUSINESS_RULE_VIOLATION | "Cannot delete the current active layout" | Disable delete button |
| Duplicate socket name | 400 | DUPLICATE_ENTRY | "Socket name already exists on this rack" | Clear field, show error |
| Layout already active | 400 | INVALID_STATE | "This layout is already active" | Refresh page |
| Permission denied | 403 | FORBIDDEN | "You don't have permission to modify floor plans" | Show read-only view |
| Dropped categories unresolved | 400 | DROPPED_CATEGORIES_UNRESOLVED | "Resolve removed categories before generating tasks" | Show resolution dialog, retry |
| Database error | 500 | INTERNAL_ERROR | "An error occurred. Please try again." | Log details, show generic error |
| Sales data unavailable | 200 | PARTIAL_DATA | "Heatmap unavailable - collecting sales data" | Show floor plan without heatmap |
| SyncFusion load error | 400 | DIAGRAM_ERROR | "Unable to load floor plan diagram" | Show error, offer to reset |

### Complex Logic: Heatmap Visualization (Dual Mode)

**User can toggle between two visualization modes:**

#### Mode 1: Socket Coloring (Discrete)
Colors each rack/socket directly based on category sales. Best for seeing individual socket performance.

```
ALGORITHM: Calculate Socket Colors
INPUT: socketSalesData[], colorScale (cold→hot)
OUTPUT: colorMap {socketId → color}

1. EXTRACT: Get all non-zero sales values
2. CALCULATE_RANGE:
   - Use percentile-based range (5th to 95th) to handle outliers
   - min = 5th percentile of sales values
   - max = 95th percentile of sales values
3. NORMALIZE: For each socket with sales:
   - normalizedValue = clamp((sales - min) / (max - min), 0, 1)
4. MAP_TO_COLOR: Using gradient scale:
   - 0.0 = cold (blue: #3b82f6)
   - 0.5 = neutral (yellow: #f59e0b)
   - 1.0 = hot (red: #ef4444)
5. SPECIAL_CASES:
   - No categories assigned: distinct "N/A" color (#e2e8f0 hatched)
   - Zero sales: cold color (not N/A)
   - No sales data period: hide heatmap entirely
6. APPLY: Set fill color directly on SyncFusion diagram nodes
```

#### Mode 2: Gradient Overlay (Continuous)
Uses **heatmap.js** library to render a continuous gradient overlay. Best for visualizing "hot zones" on the floor.

```
ALGORITHM: Calculate Gradient Heatmap
INPUT: socketSalesData[], canvasDimensions
OUTPUT: heatmap.js data points

1. EXTRACT: For each socket with sales data:
   - Get rack position (x, y) from SyncFusion node
   - Get sales value for the socket
2. NORMALIZE: Scale sales values to 0-100 intensity range
   - intensity = (sales - min) / (max - min) * 100
3. CREATE_POINTS: Generate heatmap.js data points:
   - { x: rackCenterX, y: rackCenterY, value: intensity }
4. CONFIGURE: Set heatmap.js options:
   - radius: rackWidth * 1.5 (spread based on rack size)
   - blur: 0.8 (smooth gradients)
   - opacity: 0.6 (semi-transparent to see labels)
5. RENDER: Overlay heatmap.js canvas on SyncFusion diagram
6. LAYER: Ensure rack labels render ABOVE the heatmap overlay
```

**Library**: heatmap.js (MIT license, npm: `heatmap.js`)

**UI Toggle**: User selects visualization mode via dropdown:
- "Socket Coloring" (default)
- "Gradient Heatmap"
- "Both" (socket colors + gradient overlay at reduced opacity)

## Deployment View

**No changes to existing deployment infrastructure.**

### Deployment Sequence
1. **Database Migrations**: Run `php userfrosting/conductor run` to create new tables
2. **Code Deploy**: Standard deployment via `./deploy.sh`
3. **CSS Build**: Regenerate CSS with `php userfrosting/conductor build-css --minify`
4. **Seed Data**: System rack types inserted via migration
5. **SyncFusion Assets**: Either via CDN or bundled in `public_html/vendor/syncfusion/`

### Configuration
- **SyncFusion License Key**: Add to environment or config file
- Store-level settings managed via `fpSettings` table
- Permissions added via migration to existing permission tables

### Feature Activation
- Feature available immediately after migration
- No feature flags needed - permission-based access control
- Stores without floor plans see empty state with "Create Floor Plan" CTA

### Rollback Strategy
- Database: Migrations include DROP TABLE statements for rollback
- Code: Standard git revert via deployment system
- No external service dependencies to coordinate (SyncFusion is client-side)

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used
- pattern: Service Layer Pattern
  relevance: HIGH
  why: "Business logic separation from controllers"

- pattern: Event Integration Adapter Pattern
  relevance: HIGH
  why: "Required for event system compatibility"

- pattern: Per-Store Database Pattern
  relevance: HIGH
  why: "Maintain data isolation between stores"

# New patterns created
- pattern: SyncFusion Diagram Integration Pattern (NEW)
  relevance: HIGH
  why: "Standard way to integrate SyncFusion Diagram with backend persistence"
```

### System-Wide Patterns

- **Security**: Session-based auth, permission checks in controllers, CSRF tokens for mutations
- **Error Handling**: Try/catch in controllers, structured error responses, error logging via `error_log()`
- **Performance**: Lazy-load heatmap data, paginate maintenance history, SyncFusion `preventDefaults` for smaller JSON
- **Logging/Auditing**: All mutations logged to `fpAuditLog` table with employee ID and timestamp

### Implementation Patterns

#### Code Patterns and Conventions
- PHP 8.x with strict types where practical
- PSR-4 autoloading under `BuyerKiosk\FloorPlan\` namespace
- camelCase for all variables, methods, and database columns
- Type hints on method parameters and return types
- DocBlock comments for public methods

#### Rack Asset Policy (Images/SVG)
- **Allowed in-repo**: Only **system** rack assets intended for all stores (generic SVG/PNG icons, palette thumbnails, default shapes).
- **Not allowed in-repo**: Store-specific/custom images (per-store fixture photos, logos, etc.) should not be committed.
- **MVP approach for custom fixtures**:
  - Prefer `fpRackTypes.svgPath` for custom vector shapes stored in the DB (small, portable, no repo churn).
  - Optionally use `fpRackTypes.icon` for palette labeling (e.g., Font Awesome class) where an image is not needed.
  - If true custom images are required later, add an upload flow and store files outside git (and reference via URL/key), with explicit moderation/size constraints.
- **Licensing**: Any shipped assets must be compatible with distribution (no store-provided copyrighted images committed to the repo).

#### Socket Model vs SyncFusion Diagram
The socket system is compatible with SyncFusion, but sockets are a **domain concept** implemented on top of the Diagram model (SyncFusion does not provide “rack sockets” as a native floor-planning primitive).

- **MVP definition**: Sockets are **logical partitions** of a rack (named + ordered) used to group category assignments (e.g., “Top Shelf”, “Lower Bar”).
  - Sockets are not required to have precise geometric coordinates within the rack in MVP.
  - UX can present sockets as a list in a side panel / popover when a rack is selected.
  - Visual feedback on the canvas can be: rack label + category badges (optionally grouped by socket name) rendered as annotations/callouts, not as spatially accurate “slots”.
- **Recommended SyncFusion implementation options**:
  - **Option A (MVP, simplest)**: Keep sockets out of the diagram; store them in DB only and render assignments via rack annotations/callouts.
  - **Option B (future, spatial sockets)**: Represent a rack as a **group node** with child nodes for sockets (or ports), and persist relative socket geometry (would require adding socket position fields to `fpRackSockets`).

#### SyncFusion Integration Pattern

```javascript
// Standard pattern for SyncFusion Diagram with backend sync
const FloorPlanDiagramPattern = {
    // 1. Initialize with config
    init(containerId, config) {
        this.diagram = new ej.diagrams.Diagram(config);
        this.diagram.appendTo(`#${containerId}`);
    },

    // 2. Save diagram state to backend
    async save(planId) {
        const diagramData = this.diagram.saveDiagram();
        return await this.api.put(`/plans/${planId}`, { diagramData });
    },

    // 3. Load diagram state from backend
    async load(planId) {
        const response = await this.api.get(`/plans/${planId}`);
        if (response.data.diagramData) {
            this.diagram.loadDiagram(response.data.diagramData);
        }
    },

    // 4. Sync rack positions after diagram changes
    async syncRacks() {
        const racks = this.diagram.nodes.map(node => ({
            syncfusionNodeId: node.id,
            rackTypeId: node.addInfo?.rackTypeId,
            positionX: node.offsetX,
            positionY: node.offsetY,
            width: node.width,
            height: node.height,
            rotation: node.rotateAngle
        }));
        return await this.api.post(`/plans/${this.planId}/sync-racks`, { racks });
    }
};
```

## Architecture Decisions

- [x] **ADR-1 Canvas Library**: Use SyncFusion EJ2 Diagram instead of Fabric.js
  - Rationale: Purpose-built floor planning features (symbol palette, snap-to-grid, rulers), better documentation, professional showcase examples
  - Trade-offs: Licensing cost for commercial use, larger bundle size, learning curve for team
  - User confirmed: ✅ Yes - User requested SyncFusion specifically

- [x] **ADR-2 Data Storage**: Dual storage - SyncFusion JSON for visual + relational tables for business logic
  - Rationale: SyncFusion JSON captures complete visual state; relational tables enable SQL queries for heatmaps, reporting, and integration
  - Trade-offs: Data synchronization complexity, potential for drift between JSON and relational data
  - Mitigation: Sync mechanism on save, use relational data as source of truth for business logic

- [x] **ADR-3 Layout Versioning**: Separate layouts table with assignment snapshots
  - Rationale: Enables diff calculation, scheduled activation, audit trail
  - Trade-offs: Data duplication when copying layouts, complexity in diff algorithm
  - Alternatives considered: Single layout with history table, git-like branching

- [x] **ADR-4 Event Integration**: Implement IntegrationAdapterInterface
  - Rationale: Consistent with existing event system architecture, enables scheduled activation
  - Trade-offs: Adapter implementation complexity, tight coupling to event system
  - Alternatives considered: Standalone cron job, webhook-based integration

- [x] **ADR-5 Heatmap Calculation**: Server-side aggregation, client-side rendering
  - Rationale: Leverage SQL aggregation for sales data, reduce payload size
  - Trade-offs: Requires API call for each date range change
  - Alternatives considered: Client-side calculation (too much data), pre-calculated cache (stale data)

## Quality Requirements

| Category | Requirement | Target | Measurement |
|----------|-------------|--------|-------------|
| **Performance** | Floor plan load time | < 500ms | Time to interactive canvas |
| **Performance** | SyncFusion diagram init | < 300ms | Diagram ready event |
| **Performance** | Rack placement response | < 100ms | API response time |
| **Performance** | Heatmap calculation | < 2s | 30-day sales aggregation |
| **Performance** | Canvas with 100 racks | No degradation | Frame rate > 30fps |
| **Usability** | First floor plan creation | < 10 minutes | User testing metric |
| **Usability** | Mobile workbook view | Readable on tablet | 768px minimum width |
| **Security** | Permission enforcement | 100% | All endpoints check auth |
| **Security** | Audit coverage | 100% | All mutations logged |
| **Reliability** | Layout activation | No data loss | Transaction-wrapped |
| **Reliability** | Diagram save/load | 100% fidelity | Round-trip integrity test |

## Risks and Technical Debt

### Known Technical Issues

- **SyncFusion Version Compatibility**: Must verify SyncFusion EJ2 version compatibility with existing Bootstrap 5
- **JSON Column Size**: Large floor plans may exceed MySQL TEXT limits; use LONGTEXT
- **Sales Data Mapping**: Some POS categories may not map to subcategory codes cleanly

### Technical Debt

- **No layout version history**: MVP doesn't track historical layouts (Won't Have per PRD)
- **Single floor only**: Multi-floor stores need separate floor plans (Won't Have per PRD)
- **No real-time sync**: Multiple users editing same floor plan could conflict

### Implementation Gotchas

- **Slim 2.x vs 3+**: This project uses Slim 2.x routing/controllers. Avoid Slim 3+ patterns (PSR-7 `Request/Response` injection, `$request->getParsedBody()`, middleware signatures, container usage). Mirror existing route/controller patterns in `userfrosting/routes/` and existing BuyerKiosk controllers.
- **Twig Version**: Templates use Twig 1.44. Avoid Twig 2/3+ syntax/features/filters/functions and modern extensions; mirror existing templates under `userfrosting/templates/themes/default/`. If embedding Handlebars, wrap in `{% raw %}...{% endraw %}` to prevent `{{ }}` collisions.
- **Frontend/Backend Naming Drift**: Enforce a single canonical naming scheme for identifiers across JS ↔ API ↔ DB. Use camelCase in JSON payloads/response keys (e.g., `floorPlanId`, `layoutId`, `syncfusionNodeId`, `pixelsPerUnit`) and keep mapping consistent in controllers/services; add integration tests that assert key names and required fields.
- **Database Naming & Case**: New tables/columns use camelCase, but timestamps remain `created_at`/`updated_at` (snake_case). Be consistent in migrations, models, and queries; avoid accidental case changes (especially on case-insensitive dev filesystems vs case-sensitive production DB behavior).
- **SyncFusion/Database Sync**: Rack positions must stay synchronized between SyncFusion canvas state and database records - use sync endpoint after significant changes
- **Sync Events Flood**: SyncFusion emits many events while dragging; sync only on "drop/end" and debounce bursts to avoid API spam
- **Layout Activation Timing**: Cron job required for auto-activation; ensure timezone handling is correct
- **Subcategory Code Format**: Codes come from POS; verify format matches `catID` in sales table
- **Heatmap Edge Cases**: Handle stores with no sales data gracefully (hide heatmap, not error)
- **Permission Inheritance**: Workbook endpoints should rely on existing workbook permissions (read: `task-lists`; task completion: `workbook_complete_tasks`) rather than `uri_floor_plans`
- **Non-PC Stores**: `drsCategories/drsSubCategories` are storeType=pc only; UI/API should degrade gracefully (empty lists + messaging) rather than throwing SQL errors on other store types
- **Payload Limits**: SyncFusion `diagramData` and thumbnails can be large; watch PHP request limits and DB packet limits; fail gracefully with clear error messages
- **Background Image Export**: PNG export can fail if the background image is cross-origin (tainted canvas); keep background served same-origin
- **Audit/User Attribution**: If audit views need user display names from `kiosk_users.users`, cross-db reads can be slow/brittle; consider snapshotting displayName into audit log rows at write time
- **SyncFusion License**: Ensure license key is configured before production deployment

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Floor Plan Creation with SyncFusion**
```gherkin
Given: Owner is authenticated with uri_floor_plans_manage permission
And: Store has no existing floor plan
When: Owner creates floor plan with name "Main Floor" and gridSize "medium"
And: Owner drags a "Rounder" rack from palette to canvas
Then: Floor plan record created in fpFloorPlans
And: Rack record created in fpRacks with syncfusionNodeId
And: Default sockets created in fpRackSockets
And: Current layout auto-created in fpLayouts
And: SyncFusion diagram renders rack at drop position
```

**Scenario 1b: Unique Rack Auto-Naming**
```gherkin
Given: Owner creates a new floor plan
When: Owner drags 3 racks onto the canvas
Then: Each rack is created with a unique name (e.g., R1, R2, R3)
And: Attempting to rename R2 to R1 fails with a validation error
```

**Scenario 2: Diagram Save/Load Integrity**
```gherkin
Given: Floor plan exists with 10 placed racks
When: Owner calls save (saveDiagram)
And: Owner refreshes page (loadDiagram)
Then: All 10 racks appear in exact same positions
And: All rack rotations preserved
And: All rack sizes preserved
And: No visual differences from before save
```

**Scenario 3: Layout Diff Calculation (Multi-Socket Category)**
```gherkin
Given: Current layout has category "WD" assigned to sockets 1, 2, and 3
And: Wanted layout has category "WD" assigned to sockets 2, 4, and 5
When: User requests diff between layouts
Then: Diff shows 2 moves:
  | subcategoryCode | fromSocketId | toSocketId |
  | WD              | 1            | 4          |
  | WD              | 3            | 5          |
And: Socket 2 assignment unchanged (no task)
And: Summary shows moveCount=2, addCount=0, removeCount=0
```

**Scenario 3b: Dropped Category Requires Resolution**
```gherkin
Given: Current layout has category "WD" assigned to socket 1
And: Wanted layout has category "WD" assigned to no sockets (dropped)
When: User requests diff between layouts
Then: Diff includes "WD" in dropped list
When: User attempts to generate tasks without droppedResolutions
Then: Response is 400 with error code "DROPPED_CATEGORIES_UNRESOLVED"
When: User provides droppedResolutions indicating "WD" is marked gone with reason "sold out"
Then: System writes an fpAuditLog entry recording the removal reason
And: System generates 0 tasks
When: User provides droppedResolutions indicating "WD" is moving to backstock with no bin selected
Then: System generates a remove task with disposition=backstock and backstockBinId=NULL
And: Workbook may allow completing without bin (bin can be assigned later)
```

**Scenario 3c: Draft vs Published Current Layout**
```gherkin
Given: Current published layout exists
When: Owner creates a draft of current and makes assignment changes
Then: Workbook floor view still returns the published current layout
When: Owner publishes the draft
Then: Workbook floor view returns the newly published layout
```

**Scenario 4: Maintenance Score Calculation**
```gherkin
Given: Rack has 5 days since last maintenance
And: Rack type has 3-day maintenance interval
And: Socket sales are 2x average
And: Weight factor is 0.5
When: Maintenance score calculated
Then: Base score = 5/3 = 1.67
And: Traffic multiplier = 2.0
And: Combined score = 1.67 × (1 + 2.0 × 0.5) = 3.34
```

**Scenario 5: Permission Denied for Employee Edit**
```gherkin
Given: User has uri_floor_plans permission (read-only)
And: User does NOT have uri_floor_plans_manage permission
When: User attempts to create floor plan via API
Then: Response is 403 Forbidden
And: Error code is "FORBIDDEN"
And: No database changes occur
```

### Test Coverage Requirements

- **Unit Tests**: Services (FloorPlanService, LayoutDiffService, HeatmapService, MaintenanceService)
- **Unit Tests**: Models (toArray, createFromRow, business methods)
- **Integration Tests**: API endpoints with authentication
- **Integration Tests**: Database operations (CRUD, foreign key constraints)
- **Frontend Tests**: SyncFusion diagram save/load roundtrip
- **Edge Cases**: Empty floor plans, zero sales data, deleted sockets in diff
- **Performance Tests**: Heatmap calculation with 1000+ sales records
- **Security Tests**: Permission checks on all write endpoints

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Floor Plan | Visual representation of the store's sales floor layout | The SyncFusion canvas showing walls and rack positions |
| Rack | A physical fixture on the sales floor that holds merchandise | Rounder, Gondola, Wall Bay, etc. |
| Socket | A specific location on a rack where a category is assigned | "Top Shelf", "Shoe Deck", "Side Panel" |
| Layout | A configuration of category-to-socket assignments | "Current Layout", "Holiday 2025 Layout" |
| Wanted Layout | A planned future layout scheduled for activation | Created for events or seasonal changes |
| Current Layout | The active layout representing today's floor arrangement | Only one current layout per floor plan |
| Category Assignment | The link between a POS subcategory and a rack socket | "Women's Dresses assigned to Rounder 3" |
| Subcategory | A merchandise classification from the POS system | Store-specific list (e.g., 113 for Plato's Closet) |
| Heatmap | Visual overlay showing performance data by color intensity | Sales heatmap, Maintenance heatmap |
| Move Task | A generated task to relocate a category during layout transition | "Move Women's Shoes from Gondola 1 to Wall Bay 5" |
| Symbol Palette | The SyncFusion component showing draggable rack templates | Grouped by category (Gondola, Freestanding, etc.) |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| SyncFusion EJ2 | Syncfusion's Essential JS 2 UI component library | Powers the floor plan diagram editor |
| saveDiagram() | SyncFusion method to serialize diagram to JSON | Used for persistence |
| loadDiagram() | SyncFusion method to restore diagram from JSON | Used for loading saved floor plans |
| typeNum | Store identifier in format `[a-z][a-z]\d+` | Used in API routes: `/api/:typeNum/...` |
| Combined Score | Maintenance priority = base × (1 + traffic × weight) | Algorithm for maintenance prioritization |
| syncfusionNodeId | Unique identifier for each node in SyncFusion diagram | Links diagram nodes to database racks |

### API/Interface Terms

| Term | Definition | Context |
|------|------------|---------|
| fpFloorPlans | Database table storing floor plan metadata + diagram JSON | Core entity table |
| fpRackSockets | Database table for socket definitions | Many-to-one with fpRacks |
| fpSocketAssignments | Database table linking categories to sockets per layout | Many-to-many relationship |
| task-lists | Workbook access permission | Required for workbook module access (floor view, maintenance list) |
| workbook_complete_tasks | Workbook task completion permission | Required to complete floor plan move tasks and set backstock bin later |
| uri_floor_plans | Permission for admin read access | Required for floor plan admin pages and reports |
| uri_floor_plans_manage | Permission for write access | Required for owners/managers to edit |
| diagramData | The JSON string from SyncFusion's saveDiagram() | Stored in fpFloorPlans.diagramData |
