# 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
- Bootstrap 5.3.3 for CSS framework (NOT Bootstrap 3)
- Fabric.js for canvas/drawing functionality (existing whiteboard pattern)
- 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
- 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: `uri_floor_plans` (read), `uri_floor_plans_manage` (write)
- Store group validation via `checkStoreGroup($typeNum)`
- CSRF token required for all mutations (from `meta[name="csrf_token"]`)
- No cross-store data access allowed

**CON-4 Performance Targets**
- Floor plan canvas render: < 500ms initial load
- 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

## 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/20250304_001_daybook_whiteboard_system.json
  relevance: HIGH
  why: "Canvas/spatial data storage pattern (Fabric.js JSON)"
```

- **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/routes/groups/backstock.php
  relevance: MEDIUM
  why: "Route grouping and REST endpoint patterns"

- file: userfrosting/src/BuyerKiosk/Backstock/EventService.php
  relevance: MEDIUM
  why: "Service layer pattern for business logic"
```

- **ICO-3 Frontend Patterns**
```yaml
- file: public_html/js/workspace/modules/workbook/whiteboard-display.js
  relevance: HIGH
  why: "Fabric.js canvas implementation with zoom/pan"

- file: public_html/js/workspace/modules/workbook/layout-manager.js
  relevance: MEDIUM
  why: "Panel registration and drag-drop patterns"

- 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/Models/EventIntegration.php
  relevance: MEDIUM
  why: "Integration type constants and model structure"
```

- **ICO-5 External Libraries**
```yaml
- url: http://fabricjs.com/docs/
  relevance: HIGH
  sections: [Canvas API, Object serialization, Event handling]
  why: "Canvas manipulation library for floor plan editor"
```

### Implementation Boundaries

- **Must Preserve**:
  - Existing POS category/subcategory structure (~80 subcategories)
  - Existing Event Management integration adapter pattern
  - Existing workbook panel registration system
  - Permission model (Owner > Manager > Employee hierarchy)

- **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

- **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)

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    Owner[Store Owner<br/>Admin Panel] --> FloorPlan[Floor Plan<br/>Management]
    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
```

#### 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

# 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

### 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)
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

**Implementation Note**: The `getSocketSalesTotal()` and `getAverageSocketSales()` methods aggregate sales
by subcategory and project onto sockets. Method names reference "socket" for UI display purposes, but
the underlying data is category-level.

---

## 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)

- **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
  - Fabric.js reuse from whiteboard feature reduces complexity
  - Event adapter pattern enables scheduled layout activations

- **Key Decisions**:
  1. Store floor plan canvas data as JSON (Fabric.js serialized) - matches whiteboard pattern
  2. Separate tables for racks, sockets, and assignments - enables relational queries for heatmaps
  3. Layouts stored as references to assignments, not duplicated data - reduces storage
  4. 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 Editor | `fpFloorPlans`, `fpRackTypes`, `fpRacks`, `fpRackSockets` | CRUD for plans, racks, sockets | Designer canvas, Rack library |
| 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 canvas editor
- Admin can place racks 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 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/>Canvas Editor]
        LayoutMgr[Layout Manager<br/>Current + Scheduled]
        HeatmapView[Heatmap Reports<br/>Sales + Maintenance]
        RackMgmt[Rack Template<br/>Management]
    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
    LayoutMgr --> LayoutAPI
    HeatmapView --> HeatmapAPI
    RackMgmt --> FloorPlanAPI
    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
│   │   ├── Layout.php                           # NEW: Layout version
│   │   ├── SocketAssignment.php                 # NEW: Category-socket link
│   │   └── 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
│   └── Adapters/
│       └── FloorPlanAdapter.php                 # NEW: Event integration
│
├── routes/
│   └── groups/floorplan.php                     # NEW: API routes
│
├── migrations/input/
│   ├── 20251210_001_floorplan_tables.json       # NEW: Core tables
│   ├── 20251210_002_floorplan_maintenance.json  # NEW: Maintenance tables
│   └── 20251210_003_floorplan_permissions.json  # NEW: Permissions
│
└── templates/themes/default/
    ├── admin/floorplan/
    │   ├── home.html                            # NEW: Admin dashboard
    │   ├── designer.html                        # NEW: 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: Canvas editor logic
│   │   ├── layout-manager.js                    # NEW: Layout UI
│   │   └── heatmap.js                           # NEW: Heatmap rendering
│   └── workspace/modules/floorplan/
│       ├── floor-panel.js                       # NEW: Workbook floor display
│       └── maintenance-panel.js                 # NEW: Maintenance list
│
└── css/admin/modules/
    └── floorplan.css                            # NEW: Feature styles
```

### 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 canvas structure"
  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'
    wallData: LONGTEXT NULL COMMENT 'JSON - wall polygons (Fabric.js objects)'
    thumbnail: LONGTEXT NULL COMMENT 'Base64 encoded preview image'
    isActive: TINYINT(1) UNSIGNED DEFAULT 1
    createdBy: INT UNSIGNED NULL COMMENT 'FK to employees'
    createdAt: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    updatedAt: 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., Round Rack, Straight Rack'
    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','custom') DEFAULT 'rectangle'
    defaultWidth: INT UNSIGNED DEFAULT 100 COMMENT 'Default width in pixels'
    defaultHeight: INT UNSIGNED DEFAULT 80 COMMENT 'Default height in pixels'
    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
    createdAt: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  Indexes:
    - idx_isActive (isActive)
    - idx_isSystem (isSystem)

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) NULL COMMENT 'Custom name for this rack instance'
    positionX: INT NOT NULL DEFAULT 0 COMMENT 'X position on canvas'
    positionY: INT 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'
    fabricObjectId: VARCHAR(50) NULL COMMENT 'Fabric.js object ID for sync'
    createdAt: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    updatedAt: TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
  Indexes:
    - idx_floorPlanId (floorPlanId)
    - idx_rackTypeId (rackTypeId)
  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'
    createdAt: 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)'
    activatedAt: TIMESTAMP NULL COMMENT 'When layout became active'
    archivedAt: TIMESTAMP NULL
    createdBy: INT UNSIGNED NULL
    createdAt: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    updatedAt: TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
  Indexes:
    - idx_floorPlanId (floorPlanId)
    - idx_status (status)
    - idx_layoutType (layoutType)
    - idx_activationDate (activationDate)
    - idx_eventId (eventId)
  ForeignKeys:
    - fk_fpLayouts_floorPlanId REFERENCES fpFloorPlans(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'
    createdAt: 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

# ============================================================
# 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'
    employeeId: INT UNSIGNED NOT NULL COMMENT 'FK to employees'
    action: ENUM('done','double_checked') DEFAULT 'done'
    notes: TEXT NULL
    completedAt: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  Indexes:
    - idx_rackId (rackId)
    - idx_employeeId (employeeId)
    - idx_completedAt (completedAt)
    - idx_rack_completed (rackId, completedAt)
  ForeignKeys:
    - fk_fpMaintenanceLogs_rackId REFERENCES fpRacks(id) ON DELETE CASCADE

Table: fpMoveTasks (NEW)
  Purpose: "Generated tasks for layout transitions - ONE TASK PER SOCKET CHANGE"

  IMPORTANT Design Note: Tasks are generated at the socket level, not category level.
  If a category is on 3 sockets and 2 of them change, this generates 2 tasks.
  This ensures clear, actionable instructions: "Move WD from Rack 1 to Rack 5"
  rather than ambiguous "Move WD" when it spans multiple locations.

  Columns:
    id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
    layoutId: INT UNSIGNED NOT NULL COMMENT 'FK to target fpLayouts'
    subcategoryCode: VARCHAR(10) NOT NULL
    fromSocketId: INT UNSIGNED NULL COMMENT 'Source socket (NULL if adding new location)'
    toSocketId: INT UNSIGNED NULL COMMENT 'Target socket (NULL if removing from floor)'
    status: ENUM('pending','completed') DEFAULT 'pending'
    completedBy: INT UNSIGNED NULL COMMENT 'FK to employees'
    completedAt: TIMESTAMP NULL
    createdAt: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  Indexes:
    - idx_layoutId (layoutId)
    - idx_status (status)
    - idx_subcategoryCode (subcategoryCode)
  ForeignKeys:
    - fk_fpMoveTasks_layoutId REFERENCES fpLayouts(id) ON DELETE CASCADE

  Example Task Scenarios:
    # Category moving from one socket to another
    { subcategoryCode: 'WD', fromSocketId: 1, toSocketId: 5 }  # "Move Women's Dresses from Rack 1 to Rack 5"

    # Category being added to floor (not previously assigned anywhere)
    { subcategoryCode: 'MS', fromSocketId: NULL, toSocketId: 3 }  # "Add Men's Shoes to Rack 3"

    # Category being removed from floor entirely
    { subcategoryCode: 'KS', fromSocketId: 7, toSocketId: NULL }  # "Remove Kids' Shoes from Rack 7"

    # Category expanding to additional socket (already on socket 2, adding socket 4)
    { subcategoryCode: 'WD', fromSocketId: NULL, toSocketId: 4 }  # "Expand Women's Dresses to Rack 4"

    # Category contracting from socket (staying on socket 2, removing from socket 6)
    { subcategoryCode: 'WD', fromSocketId: 6, toSocketId: NULL }  # "Remove Women's Dresses from Rack 6"

# ============================================================
# 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')
    entityId: INT UNSIGNED NOT NULL
    action: VARCHAR(50) NOT NULL COMMENT 'created, updated, deleted, activated, etc.'
    oldValue: JSON NULL
    newValue: JSON NULL
    employeeId: INT UNSIGNED NULL
    ipAddress: VARCHAR(45) NULL
    createdAt: TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  Indexes:
    - idx_entityType_entityId (entityType, entityId)
    - idx_employeeId (employeeId)
    - idx_createdAt (createdAt)

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
    updatedAt: 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
  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

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)
    wallData: json (optional, Fabric.js polygons)
    thumbnail: string (optional, base64)
  Response:
    success: true
    data: FloorPlan object

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
  Response:
    success: true
    data: [RackType objects]

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

Endpoint: Add Rack to Floor Plan
  Method: POST
  Path: /api/:typeNum/floor-plan/plans/:planId/racks
  Permission: uri_floor_plans_manage
  Request:
    rackTypeId: number (required)
    name: string (optional)
    positionX: number (required)
    positionY: number (required)
    rotation: number (0-360, default 0)
  Response:
    success: true
    data: Rack object with generated sockets

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

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: 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 assignment count

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, toSocket }]
      removals: [{ subcategoryCode, fromSocket }]
      moves: [{ subcategoryCode, fromSocket, toSocket }]
      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 move tasks from diff
  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: uri_floor_plans (employee can complete)
  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 (becomes current)
  Response:
    success: true
    data: Layout object
    message: "Layout activated"

# ============================================================
# 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, 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 }]
      overdue: number

# ============================================================
# 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, socket, 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

Endpoint: Get Rack Maintenance History
  Method: GET
  Path: /api/:typeNum/floor-plan/racks/:rackId/maintenance-history
  Permission: uri_floor_plans
  Query:
    limit: number (default 10)
  Response:
    success: true
    data: [MaintenanceLog objects]

# ============================================================
# WORKBOOK ENDPOINTS (Read-Only Floor View)
# ============================================================

Endpoint: Get Workbook Floor View
  Method: GET
  Path: /api/:typeNum/floor-plan/workbook/floor
  Permission: uri_floor_plans
  Description: Simplified view for employee workbook
  Response:
    success: true
    data:
      floorPlan: { id, name, thumbnail }
      racks: [{ id, name, position, sockets }]
      assignments: [{ socketId, subcategories }]

Endpoint: Get Workbook Maintenance List
  Method: GET
  Path: /api/:typeNum/floor-plan/workbook/maintenance
  Permission: uri_floor_plans
  Description: Combined maintenance + move tasks for employee
  Response:
    success: true
    data:
      maintenanceTasks: [{ rackId, name, priority, daysSince }]
      moveTasks: [{ id, category, from, to, status }]
```

#### Application Data Models

```pseudocode
ENTITY: FloorPlan (NEW)
  FIELDS:
    id: int
    name: string
    description: string|null
    canvasWidth: int
    canvasHeight: int
    gridSize: enum(small|medium|large)
    wallData: json|null
    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
    description: string|null
    defaultSocketCount: int
    defaultSocketNames: array
    icon: string|null
    shape: enum(rectangle|circle|custom)
    defaultWidth: int
    defaultHeight: int
    maintenanceIntervalDays: int
    isSystem: bool
    isActive: bool

  BEHAVIORS:
    toArray(): array
    createFromRow(array): void
    generateSockets(Rack): array<RackSocket> - Create default sockets

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

  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 POS data

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

  BEHAVIORS:
    toArray(): array
    getEmployeeName(): string

ENTITY: MoveTask (NEW)
  FIELDS:
    id: int
    layoutId: int
    subcategoryCode: string
    fromSocketId: int|null
    toSocketId: int|null
    status: enum(pending|completed)
    completedBy: int|null
    completedAt: datetime|null

  BEHAVIORS:
    toArray(): array
    complete(int employeeId): void
    getTaskDescription(): string - Human-readable move instruction
```

#### 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, activateOnPhase: 'build_up'|'active' }

# POS Sales Data Integration
SalesData:
  source: kiosk_sales.sales table
  access: Read-only via SQL query
  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: Existing subcategory lookup tables
  access: Read-only
  data_flow: Subcategory codes and display names for assignment UI
```

### Implementation Examples

#### 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
    $trafficMultiplier = $averageSales > 0
        ? $socketSales / $averageSales
        : 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.

**IMPORTANT Design Decision**: The schema (via `uk_layout_socket_subcategory` unique constraint) supports
many-to-many relationships where a subcategory CAN be assigned to multiple sockets within the same layout.
This is intentional - some stores split high-volume categories like "Women's Dresses" across multiple
racks in different floor areas.

The diff algorithm must handle this by:
1. Using multi-value maps (`subcategoryCode => [socketIds...]`) instead of single-value maps
2. Generating **one task per socket change**, NOT one task per category
3. Comparing socket sets to identify individual additions/removals/moves

```php
/**
 * Compare two layouts and identify all assignment changes.
 * Returns additions, removals, and moves at the socket level.
 *
 * IMPORTANT: Subcategories can be on multiple sockets. This algorithm
 * generates one task per socket-level change, not one per category.
 */
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);

        // Pure additions: category wasn't on floor at all
        if (empty($currentSockets) && !empty($wantedSockets)) {
            foreach ($addedSockets as $socketId) {
                $additions[] = [
                    'subcategoryCode' => $subcatCode,
                    'toSocketId' => $socketId,
                    'fromSocketId' => null
                ];
            }
            continue;
        }

        // Pure removals: category being removed from floor entirely
        if (empty($wantedSockets) && !empty($currentSockets)) {
            foreach ($removedSockets as $socketId) {
                $removals[] = [
                    'subcategoryCode' => $subcatCode,
                    'fromSocketId' => $socketId,
                    'toSocketId' => null
                ];
            }
            continue;
        }

        // Mixed: some sockets added, some removed = moves + partial changes
        // 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 (expanding to more sockets)
        for ($i = $moveCount; $i < count($addedList); $i++) {
            $additions[] = [
                'subcategoryCode' => $subcatCode,
                'toSocketId' => $addedList[$i],
                'fromSocketId' => null
            ];
        }

        // Remaining removals (contracting to fewer sockets)
        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)
        ]
    ];
}

/**
 * Build multi-value assignment map for a layout.
 *
 * @return array<string, int[]> Map of subcategoryCode => [socketId, ...]
 */
private function getAssignmentMap(int $layoutId): array
{
    $sql = "SELECT subcategoryCode, socketId FROM fpSocketAssignments WHERE layoutId = ?";
    $rows = $this->db->fetchAll($sql, [$layoutId]);

    $map = [];
    foreach ($rows as $row) {
        $code = $row['subcategoryCode'];
        if (!isset($map[$code])) {
            $map[$code] = [];
        }
        $map[$code][] = (int)$row['socketId'];
    }
    return $map;
}
```

**Example Scenario**: Women's Dresses (WD) is on sockets 1, 2, 3 in current layout.
Wanted layout has WD on sockets 2, 4, 5.

Diff result:
- 1 move: WD from socket 1 → socket 4
- 1 move: WD from socket 3 → socket 5
- Socket 2 unchanged (no task generated)

This generates **2 move tasks**, not 1 "category moved" task.

#### Example: Event Integration Adapter

**Why this example**: Shows required IntegrationAdapterInterface implementation for event system compatibility.

```php
namespace BuyerKiosk\FloorPlan\Adapters;

use BuyerKiosk\EventManagement\Adapters\IntegrationAdapterInterface;
use BuyerKiosk\EventManagement\Models\Event;
use BuyerKiosk\EventManagement\Models\EventIntegration;

class FloorPlanAdapter implements IntegrationAdapterInterface
{
    public function validateConfig(array $config): array
    {
        $errors = [];
        if (empty($config['layoutId'])) {
            $errors[] = 'Layout ID is required';
        }
        return $errors;
    }

    public function create(Event $event, array $config): int
    {
        // Link layout to event
        $layout = $this->layoutService->getById($config['layoutId']);
        $layout->eventId = $event->id;
        $layout->activationDate = $event->startDate;
        $layout->status = 'scheduled';
        $this->layoutService->save($layout);

        return $layout->id;
    }

    public function syncDate(Event $event, EventIntegration $integration): bool
    {
        // Update layout activation date when event dates change
        $layout = $this->layoutService->getById($integration->foreignId);
        $layout->activationDate = $event->startDate;
        $this->layoutService->save($layout);
        return true;
    }

    public function activatePending(Event $event): bool
    {
        // Auto-generate tasks and activate layout on event start
        $integration = $this->getIntegrationForEvent($event);
        $layout = $this->layoutService->getById($integration->foreignId);

        if ($integration->config['autoGenerateTasks'] ?? true) {
            $this->layoutDiffService->generateMoveTasks($layout->id);
        }

        $this->layoutService->activateLayout($layout->id);
        return true;
    }

    public function deactivate(EventIntegration $integration): bool
    {
        // Layout remains but unlinks from event
        $layout = $this->layoutService->getById($integration->foreignId);
        $layout->eventId = null;
        $this->layoutService->save($layout);
        return true;
    }

    public function delete(EventIntegration $integration): void
    {
        // Just unlink, don't delete the layout itself
        $this->deactivate($integration);
    }

    public function update(Event $event, EventIntegration $integration, array $config): void
    {
        // Update config and re-link if layoutId changed
        if (isset($config['layoutId']) && $config['layoutId'] !== $integration->foreignId) {
            $this->deactivate($integration);
            $integration->foreignId = $this->create($event, $config);
        }
    }
}
```

## Runtime View

### Primary Flow: Floor Plan Creation & Layout Setup

1. Owner opens Floor Plan Designer in Admin Panel
2. Owner selects canvas size preset (Small/Medium/Large)
3. System creates floor plan record with dimensions
4. Owner draws walls using polygon tool (stored as Fabric.js JSON)
5. Owner drags rack templates onto canvas
6. System creates rack + socket records for each placement
7. Owner assigns categories to sockets via dropdown
8. System creates socket assignment records for "current" layout
9. Owner saves and views floor plan with assignments

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

    Owner->>Designer: Select canvas size
    Designer->>API: POST /floor-plan/plans {name, gridSize}
    API->>Service: createFloorPlan()
    Service->>DB: INSERT fpFloorPlans
    Service->>DB: INSERT fpLayouts (current)
    DB-->>Service: planId, layoutId
    Service-->>API: FloorPlan object
    API-->>Designer: {success: true, data: FloorPlan}

    Owner->>Designer: Drag rack to canvas
    Designer->>API: POST /plans/:id/racks {rackTypeId, position}
    API->>Service: addRack()
    Service->>DB: INSERT fpRacks
    Service->>DB: INSERT fpRackSockets (from template)
    DB-->>Service: rack with sockets
    Service-->>API: Rack object
    API-->>Designer: {success: true, data: Rack}

    Owner->>Designer: Assign category to socket
    Designer->>API: POST /layouts/:id/assignments {socketId, subcatCode}
    API->>Service: assignCategory()
    Service->>DB: INSERT fpSocketAssignments
    DB-->>Service: assignment
    Service-->>API: Assignment object
    API-->>Designer: {success: true, data: Assignment}
```

### Secondary Flow: Layout Transition Planning

1. Owner creates a "wanted" layout for upcoming event
2. Owner modifies category assignments in wanted layout
3. Owner requests diff comparison to current layout
4. System calculates additions, removals, and moves
5. Owner confirms and generates move tasks
6. Tasks appear in employee workbook
7. On activation date, layout becomes current

```mermaid
sequenceDiagram
    actor Owner
    participant Admin as Admin Panel
    participant API as LayoutApiController
    participant Diff as LayoutDiffService
    participant DB as Per-Store DB

    Owner->>Admin: Create wanted layout
    Admin->>API: POST /plans/:id/layouts {name, activationDate}
    API->>DB: INSERT fpLayouts (wanted)
    DB-->>API: layoutId
    API-->>Admin: Layout object

    Owner->>Admin: View diff
    Admin->>API: GET /layouts/:id/diff
    API->>Diff: calculateDiff(currentId, wantedId)
    Diff->>DB: SELECT assignments for both layouts
    Diff-->>API: {additions, removals, moves}
    API-->>Admin: Diff result

    Owner->>Admin: Generate tasks
    Admin->>API: POST /layouts/:id/generate-tasks
    API->>Diff: generateMoveTasks()
    Diff->>DB: INSERT fpMoveTasks
    DB-->>Diff: taskCount
    Diff-->>API: {taskCount, tasks}
    API-->>Admin: Tasks created
```

### 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 |
| 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 |

### Complex Logic: Heatmap Color Calculation

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

1. EXTRACT: Get all non-zero sales values
2. CALCULATE_RANGE:
   - min = minimum sales value
   - max = maximum sales value
   - range = max - min (handle division by zero)
3. NORMALIZE: For each socket with sales:
   - normalizedValue = (sales - min) / range (0.0 to 1.0)
4. MAP_TO_COLOR: Using gradient scale:
   - 0.0 = cold (blue/gray: #64748b)
   - 0.5 = neutral (yellow: #f59e0b)
   - 1.0 = hot (red/orange: #f43f5e)
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. RETURN: colorMap for canvas overlay rendering
```

### Complex Logic: Scheduled Layout Activation

```
ALGORITHM: Auto-Activate Scheduled Layouts
INPUT: currentDate
OUTPUT: activatedLayouts[]

1. QUERY: SELECT layouts WHERE
   - layoutType = 'wanted'
   - status = 'scheduled'
   - activationDate <= currentDate
   ORDER BY activationDate ASC

2. FOR EACH layout:
   a. FIND current layout for same floorPlanId
   b. IF current exists:
      - Archive current layout (status = 'archived')
   c. UPDATE layout:
      - layoutType = 'current'
      - status = 'active'
      - activatedAt = NOW()
   d. IF moveTasks not generated:
      - Generate tasks from diff (won't overwrite existing)
   e. LOG: audit entry for activation

3. RETURN: list of activated layouts for notification
```

## 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

### Configuration
- No new environment variables required
- 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

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used
- pattern: Fabric.js Canvas Pattern (from whiteboard)
  relevance: HIGH
  why: "Reuse canvas rendering, zoom/pan, object serialization"

- 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"

# New patterns (if documentation created)
- pattern: Heatmap Overlay Pattern (NEW)
  relevance: MEDIUM
  why: "Reusable pattern for data visualization on canvas"
```

### 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, canvas virtualization for large plans
- **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

#### Frontend Component Structure

```javascript
// Floor Plan Designer Component Pattern
class FloorPlanDesigner {
    constructor(options) {
        this.typeNum = options.typeNum;
        this.csrfToken = this._getCSRFToken();
        this.canvas = null;
        this.state = {
            planId: null,
            selectedRack: null,
            mode: 'select' // select, wall, rack
        };
    }

    async init() {
        await this.initCanvas();
        this.bindToolbar();
        this.bindCanvasEvents();
        await this.loadFloorPlan();
    }

    async loadFloorPlan() {
        const response = await this.api.get(`/floor-plan/plans/${this.state.planId}`);
        if (response.success) {
            this.renderFloorPlan(response.data);
        }
    }
}
```

#### Controller Pattern

```php
// Standard controller method pattern
public function create()
{
    if (!$this->checkWriteAuth()) return;

    try {
        $data = json_decode($this->app->request->getBody(), true);
        if (!$data) {
            return $this->sendErrorResponse('Invalid JSON', 400, 'INVALID_JSON');
        }

        // Validate
        $errors = $this->service->validateInput($data);
        if (!empty($errors)) {
            return $this->sendErrorResponse($errors[0], 400, 'VALIDATION_ERROR');
        }

        // Execute
        $result = $this->service->create($data, $this->app->user->id);

        // Audit
        $this->auditService->log('created', 'floorplan', $result->id);

        // Respond
        $this->app->response->setStatus(201);
        $this->sendJsonResponse(['success' => true, 'data' => $result->toArray()]);

    } catch (Exception $e) {
        error_log("FloorPlanApiController::create: " . $e->getMessage());
        $this->sendErrorResponse('Failed to create floor plan', 500);
    }
}
```

### Integration Points Summary

- **Event System**: FloorPlanAdapter registered in IntegrationService
- **Sales Data**: Read-only queries to kiosk_sales for heatmaps
- **POS Categories**: Read-only access to subcategory lookup
- **Workbook Panels**: New panels registered in panel registry
- **Permissions**: New permissions added to existing role hierarchy

## Architecture Decisions

- [x] **ADR-1 Canvas Library**: Use Fabric.js for floor plan editor
  - Rationale: Already in use for whiteboard, proven pattern, good object model
  - Trade-offs: Larger bundle size than pure Canvas API, learning curve for custom shapes
  - Alternatives considered: Konva.js, pure Canvas API, SVG manipulation

- [x] **ADR-2 Data Storage**: Store rack positions in relational tables, walls as JSON
  - Rationale: Racks need relational queries (for heatmaps, maintenance), walls are static geometry
  - Trade-offs: Two data models to maintain, sync complexity between canvas and DB
  - Alternatives considered: All JSON (harder queries), All relational (over-normalized)

- [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** | 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** | Concurrent edits | Last-write-wins | No silent overwrites |

## Risks and Technical Debt

### Known Technical Issues

- **Fabric.js version**: Current whiteboard uses Fabric.js 5.x; ensure consistent version
- **Large canvas performance**: Fabric.js can slow with many objects; may need object grouping
- **Sales data mapping**: Some POS categories may not map to subcategory codes cleanly

### Database Compatibility

**JSON Column Requirements**:
The `fpAuditLog.oldValue` and `fpAuditLog.newValue` columns use the native JSON data type. This requires:
- **MySQL**: Version 5.7.8 or later
- **MariaDB**: Version 10.2.7 or later

**Fallback Strategy** (if supporting older installations):
If any production environments run older MySQL versions, migrations should be modified to:
1. Use `LONGTEXT` instead of `JSON` for these columns
2. Add application-level JSON encoding/decoding in the `AuditService`
3. Validate JSON structure before insert (since DB won't enforce)

```php
// Fallback pattern if needed
$this->db->insert('fpAuditLog', [
    'oldValue' => json_encode($oldValue, JSON_THROW_ON_ERROR),
    'newValue' => json_encode($newValue, JSON_THROW_ON_ERROR),
    // ... other columns
]);
```

**Recommendation**: Audit all production environments for MySQL version before deployment.
The JSON type provides better validation and query capabilities, so prefer native JSON where supported.

### 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)
- **Manual rack templates**: System templates hardcoded; no admin UI for global templates

### Implementation Gotchas

- **Canvas/DB sync**: Rack positions must stay synchronized between Fabric.js canvas state and database records
- **Layout activation timing**: Cron job required for auto-activation; ensure timezone handling is correct
- **Subcategory code format**: Codes come from DRS export; verify format matches `catID` in sales table
- **Heatmap edge cases**: Handle stores with no sales data gracefully (hide heatmap, not error)
- **Permission inheritance**: Employees need `uri_floor_plans` for workbook view, but not manage permission

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Floor Plan Creation**
```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"
Then: Floor plan record is created in fpFloorPlans
And: Current layout is auto-created in fpLayouts
And: Response includes planId and layoutId
And: Audit log entry created
```

**Scenario 2: Rack Placement with Sockets**
```gherkin
Given: Floor plan exists with id 1
And: Rack type "Round Rack" has 3 default sockets
When: Owner adds rack at position (100, 200)
Then: Rack record created in fpRacks
And: 3 socket records created in fpRackSockets
And: Socket names match rack type defaults
And: Canvas position stored correctly
```

**Scenario 3: Layout Diff Calculation (Simple)**
```gherkin
Given: Current layout has category "WD" assigned to socket 1
And: Wanted layout has category "WD" assigned to socket 2
And: Wanted layout has category "MD" assigned to socket 3 (new)
When: User requests diff between layouts
Then: Diff shows 1 move (WD: socket 1 → socket 2)
And: Diff shows 1 addition (MD: → socket 3)
And: Summary shows moveCount=1, addCount=1, removeCount=0
```

**Scenario 3b: 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 is unchanged (no task)
And: Summary shows moveCount=2, addCount=0, removeCount=0
And: When tasks are generated, 2 separate fpMoveTasks records are created
```

**Scenario 3c: Layout Diff - Category Expansion**
```gherkin
Given: Current layout has category "WD" assigned to socket 1 only
And: Wanted layout has category "WD" assigned to sockets 1, 2, and 3
When: User requests diff between layouts
Then: Diff shows 2 additions (expanding to more sockets):
  | subcategoryCode | fromSocketId | toSocketId |
  | WD              | NULL         | 2          |
  | WD              | NULL         | 3          |
And: Socket 1 assignment is unchanged
And: Summary shows moveCount=0, addCount=2, removeCount=0
```

**Scenario 4: Scheduled Layout Activation**
```gherkin
Given: Wanted layout has activationDate of today
And: Status is "scheduled"
When: Auto-activation cron job runs
Then: Layout status changes to "active"
And: Layout type changes to "current"
And: Previous current layout archived
And: Move tasks generated if not already present
And: Audit log records activation
```

**Scenario 5: 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 6: Permission Denied**
```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
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)
- **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 canvas showing walls and rack positions |
| Rack | A physical fixture on the sales floor that holds merchandise | Round racks, straight racks, display tables |
| Socket | A specific location on a rack where a category is assigned | "Top Shelf", "Middle Section", "Shoe Display" |
| 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 Rack 3, Top" |
| Subcategory | A merchandise classification from the POS system | ~80 types like "WD" (Women's Dresses) |
| 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 Rack 1 to Rack 5" |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| Fabric.js | JavaScript canvas library for object manipulation | Powers the floor plan editor canvas |
| typeNum | Store identifier in format `[a-z][a-z]\d+` | Used in API routes: `/api/:typeNum/...` |
| IntegrationAdapterInterface | PHP interface for event system integrations | FloorPlanAdapter implements this |
| Combined Score | Maintenance priority = base × (1 + traffic × weight) | Algorithm for maintenance prioritization |

### API/Interface Terms

| Term | Definition | Context |
|------|------------|---------|
| fpFloorPlans | Database table storing floor plan metadata | 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 |
| uri_floor_plans | Permission for read access | Required for all users viewing floor plans |
| uri_floor_plans_manage | Permission for write access | Required for owners/managers to edit |
