# 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** (autopilot mode - ADRs 1-5 accepted)
- [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
- MySQL multi-store architecture (central DB + per-store DBs)
- SyncFusion EJ2 Diagram for floor plan visual editor
- Bootstrap 5.3.3 for UI components
- Must maintain backward compatibility with existing diagram JSON structure

**CON-2 Coding Standards**
- PSR-4 autoloading under `BuyerKiosk\` namespace
- camelCase for database columns (e.g., `socketId`, `layoutId`)
- Existing permission model: `uri_floor_plans` (read), `uri_floor_plans_manage` (write)
  - Note: PRD references `uri_floor_plan_edit`/`uri_floor_plan_view` which do not exist in codebase. Use the actual permission names above.
- Must use migration system (via `userfrosting/conductor`) for any schema changes

**CON-3 Performance Requirements**
- Assignment load: < 500ms target for page load hydration
- Assignment save: < 300ms target (auto-save debounce at 300ms)
- Must handle stores with up to 200 zone assignments

**CON-4 Data Integrity**
- Database is single source of truth after implementation
- Migration must be non-destructive (preserve existing diagram JSON)
- Must be idempotent (safe to run multiple times)

## Implementation Context

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

### Required Context Sources

- ICO-1 [Floor Plan System Documentation]
```yaml
# Product Requirements
- doc: docs/specs/032-floor-plan-zone-assignment-simplification/product-requirements.md
  relevance: CRITICAL
  why: "Defines all user requirements, acceptance criteria, and business rules"

# Project Configuration
- doc: CLAUDE.md
  relevance: HIGH
  why: "Project conventions, commands, and patterns"
```

- ICO-2 [Backend Services]
```yaml
- file: userfrosting/src/BuyerKiosk/FloorPlan/Services/LayoutService.php
  relevance: CRITICAL
  sections: [getAssignments (line 594-639), bulkUpdateAssignments (line 780-821)]
  why: "Already implements database save - key method to leverage"

- file: userfrosting/src/BuyerKiosk/FloorPlan/Services/FloorPlanService.php
  relevance: HIGH
  sections: [syncDiagramRacks (line 651-701)]
  why: "Syncs racks only, NOT assignments - confirms gap in current system"

- file: userfrosting/src/BuyerKiosk/FloorPlan/Services/HeatmapService.php
  relevance: MEDIUM
  sections: [getSocketAssignments (line 169-214)]
  why: "Report queries - already uses database tables correctly"

- file: userfrosting/src/BuyerKiosk/FloorPlan/Controllers/FloorPlanApiController.php
  relevance: HIGH
  sections: [getAssignments (line 1359-1381), bulkUpdateAssignments (line 1440-1542)]
  why: "API controllers - already have working endpoint implementations"
```

- ICO-3 [Frontend Designer]
```yaml
- file: userfrosting/templates/themes/default/admin/floor-plan/designer.html
  relevance: CRITICAL
  sections: [line 2099-2117 (missing hydration), line 3577-3668 (save implementation)]
  why: "Frontend component requiring modification - currently doesn't load from DB"
```

- ICO-4 [Database Schema]
```yaml
- file: userfrosting/migrations/input/20251217_002_floorplan_layouts.json
  relevance: HIGH
  why: "fpSocketAssignments table structure - no changes needed"

- file: userfrosting/migrations/input/20251217_001_floorplan_core.json
  relevance: MEDIUM
  why: "fpRacks and fpRackSockets table structure"
```

- ICO-5 [API Routes]
```yaml
- file: userfrosting/routes/floor-plan/api.php
  relevance: HIGH
  sections: [line 787-897 (assignment routes)]
  why: "Existing API route definitions - endpoints already exist"
```

### Implementation Boundaries

- **Must Preserve**:
  - Diagram JSON structure for visual rendering (nodes, connectors, positions)
  - Existing `bulkUpdateAssignments` API contract
  - SyncFusion diagram functionality and rendering
  - All existing rack/socket relationships

- **Can Modify**:
  - `designer.html` JavaScript initialization
  - Add new API endpoint for loading assignments
  - Frontend assignment state management

- **Must Not Touch**:
  - `fpSocketAssignments` database table structure (already correct)
  - `HeatmapService` report queries (already correct)
  - Existing layout lifecycle (draft/scheduled/active/archived)

### Out of Scope for This Phase

The following features from the PRD are explicitly deferred to future phases:

| Feature | PRD Priority | Reason for Deferral |
|---------|--------------|---------------------|
| Bulk Assignment Import | Should Have | Requires additional UI for layout selection and merge/replace options |
| Assignment History | Should Have | Requires audit log UI and historical data retrieval endpoints |
| Subcategory Search/Filter | Could Have | Enhancement to existing dropdown - can be added incrementally |
| Visual Zone Assignment Indicators | Could Have | Diagram node customization - requires SyncFusion styling work |

These features can be implemented as follow-on work after the core single-source-of-truth architecture is proven.

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    StoreManager[Store Manager] --> Designer[Floor Plan Designer]
    StoreManager --> Reports[Heatmap Reports]

    Designer --> FloorPlanAPI[Floor Plan API]
    Reports --> HeatmapAPI[Heatmap API]

    FloorPlanAPI --> LayoutService[LayoutService]
    HeatmapAPI --> HeatmapService[HeatmapService]

    LayoutService --> StoreDB[(Store Database)]
    HeatmapService --> StoreDB

    Designer --> SyncFusionDiagram[SyncFusion EJ2 Diagram]
```

#### Interface Specifications

```yaml
# Inbound Interfaces
inbound:
  - name: "Floor Plan Designer UI"
    type: HTTPS
    format: HTML + JavaScript
    authentication: Session (UserFrosting)
    data_flow: "User interactions for zone assignment management"

  - name: "Floor Plan Reports UI"
    type: HTTPS
    format: HTML + JavaScript
    authentication: Session (UserFrosting)
    data_flow: "Heatmap visualization requests"

# Outbound Interfaces (API calls from frontend)
outbound:
  - name: "Get Assignments API"
    type: HTTPS
    format: REST/JSON
    path: GET /api/:typeNum/floor-plan/layouts/:layoutId/assignments
    authentication: Session + CSRF
    data_flow: "Load assignments from database on page load"
    criticality: HIGH

  - name: "Bulk Update Assignments API"
    type: HTTPS
    format: REST/JSON
    path: PUT /api/:typeNum/floor-plan/layouts/:layoutId/assignments/bulk
    authentication: Session + CSRF
    data_flow: "Save zone assignments to database"
    criticality: HIGH
    existing: true

# Data Interfaces
data:
  - name: "Store Database"
    type: MySQL
    tables: [fpSocketAssignments, fpRackSockets, fpRacks, fpLayouts]
    doc: userfrosting/migrations/input/20251217_002_floorplan_layouts.json
    data_flow: "Assignment persistence and retrieval"
```

### Cross-Component Boundaries

- **API Contracts**: The `GET /api/:typeNum/floor-plan/layouts/:layoutId/assignments` endpoint response format is a PUBLIC contract used by frontend
- **Team Ownership**: Floor Plan module owned by backend team; frontend templates shared ownership
- **Shared Resources**: Store database accessed by both LayoutService and HeatmapService
- **Breaking Change Policy**: Frontend expects `assignments` keyed by socketId; this format must be maintained

### Project Commands

```bash
# Component: BuyerKiosk Backend
Location: /Users/rvanvuren/Projects/buyerkiosk-web

## Environment Setup
Install Dependencies: cd userfrosting && composer install
Start Development: Uses Apache/nginx with PHP-FPM (configured externally)

# Testing Commands
Unit Tests: ./test.sh --testsuite unit
Integration Tests: ./test.sh --testsuite integration
All Tests: ./test.sh
Test with Coverage: ./test.sh --coverage
Static Analysis: cd userfrosting && ./vendor/bin/phpstan analyse

# Database Operations
Run Migrations: php userfrosting/conductor run
Create Migration: Create JSON file in userfrosting/migrations/input/

# CSS Build
Development Build: php userfrosting/conductor build-css
Production Build: php userfrosting/conductor build-css --minify
Watch Mode: php userfrosting/conductor build-css --watch

# Deployment
Full Deploy: ./deploy.sh (runs tests then deploys)
```

## Solution Strategy

- **Architecture Pattern**: Layered architecture with single source of truth pattern
  - Database layer is the ONLY authoritative source for zone assignments
  - Frontend loads from API on initialization, saves directly to database
  - No intermediate caching in diagram JSON for assignments

- **Integration Approach**:
  - Leverage existing `LayoutService::getAssignments()` and `bulkUpdateAssignments()` methods
  - Add frontend hydration call on page load
  - Diagram JSON assignments are IGNORED (not stripped) - see ADR-2

- **Justification**:
  - Minimal code changes - backend already has correct implementation
  - Reports already query database - ensures consistency
  - Eliminates dual-storage confusion entirely

- **Key Decisions**:
  1. Database is single source of truth (not diagram JSON)
  2. Frontend fetches assignments via API on page load
  3. Auto-save with 300ms debounce for immediate persistence
  4. One-time migration job to migrate existing diagram JSON assignments
  5. Diagram JSON is NOT modified (assignment data simply ignored)

## Building Block View

### Components

```mermaid
graph LR
    subgraph Frontend["Frontend (designer.html)"]
        DiagramInit[Diagram Initialization]
        AssignmentLoader[Assignment Loader]
        ZonePanel[Zone Assignment Panel]
        AssignmentSaver[Assignment Saver]
    end

    subgraph API["API Layer"]
        GetAssignmentsAPI[GET /assignments]
        BulkUpdateAPI[PUT /assignments/bulk]
    end

    subgraph Services["Service Layer"]
        LayoutService[LayoutService]
        FloorPlanService[FloorPlanService]
    end

    subgraph Database["Database"]
        fpSocketAssignments[(fpSocketAssignments)]
        fpRackSockets[(fpRackSockets)]
    end

    DiagramInit --> AssignmentLoader
    AssignmentLoader -->|fetch| GetAssignmentsAPI
    GetAssignmentsAPI --> LayoutService
    LayoutService --> fpSocketAssignments

    ZonePanel -->|onChange| AssignmentSaver
    AssignmentSaver -->|debounced save| BulkUpdateAPI
    BulkUpdateAPI --> LayoutService
    LayoutService --> fpSocketAssignments
```

### Directory Map

**Component**: Backend Services (NO NEW FILES - modifications only)
```
userfrosting/
├── src/BuyerKiosk/FloorPlan/
│   ├── Controllers/
│   │   └── FloorPlanApiController.php    # MODIFY: Enhance getAssignments response format
│   └── Services/
│       └── LayoutService.php              # EXISTING: No changes needed
├── routes/floor-plan/
│   └── api.php                            # EXISTING: Routes already defined
```

**Component**: Frontend (designer.html - modifications only)
```
userfrosting/templates/themes/default/admin/floor-plan/
├── designer.html                          # MODIFY: Add assignment hydration on load
```

**Component**: Migration Job (NEW)
```
userfrosting/
├── src/BuyerKiosk/FloorPlan/
│   └── Jobs/
│       └── MigrateJsonAssignmentsJob.php  # NEW: One-time migration job
├── bin/
│   └── migrate-floor-plan-assignments.php # NEW: CLI script to run migration
```

### Interface Specifications

#### Data Storage Changes

```yaml
# NO SCHEMA CHANGES REQUIRED
# The fpSocketAssignments table already has the correct structure:
#   - id: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
#   - layoutId: INT UNSIGNED (FK to fpLayouts)
#   - socketId: INT UNSIGNED (FK to fpRackSockets)
#   - subcategoryCode: VARCHAR(10)
#   - sortOrder: INT UNSIGNED
#   - created_at: TIMESTAMP
```

#### Internal API Changes

```yaml
# EXISTING ENDPOINT - Enhanced Response
Endpoint: Get Layout Assignments
  Method: GET
  Path: /api/:typeNum/floor-plan/layouts/:layoutId/assignments
  Authentication: Session + uri_floor_plans permission

  Request:
    layoutId: int (path parameter, required)

  Response:
    success:
      success: true
      assignments: object
        # Keyed by socketId for direct lookup
        [socketId]: array
          - id: int (assignment ID)
            layoutId: int
            socketId: int
            subcategoryCode: string
            sortOrder: int
            subcategoryDescription: string|null
            catCode: string|null
            catDescription: string|null
      racks: array (NEW - include rack/socket mapping for frontend)
        - id: int (rackId)
          syncfusionNodeId: string
          sockets: array
            - id: int (socketId)
              name: string
              sortOrder: int
    error:
      success: false
      error: string
      errorCode: string (optional)

# EXISTING ENDPOINT - No Changes
Endpoint: Bulk Update Assignments
  Method: PUT
  Path: /api/:typeNum/floor-plan/layouts/:layoutId/assignments/bulk
  Authentication: Session + uri_floor_plans_manage permission

  Request:
    rackId: int (required)
    assignments: object
      [zoneNumber]: array
        - code: string (subcategoryCode)
          name: string (optional, for display)

  Response:
    success:
      success: true
      updated: int (count of assignments updated)
      warnings: object (optional)
        skippedZones: array
        message: string
    error:
      success: false
      error: string
```

#### Application Data Models

```pseudocode
# No new models required
# Existing models are sufficient:

ENTITY: SocketAssignment (EXISTING - no changes)
  FIELDS:
    id: int
    layoutId: int
    socketId: int
    subcategoryCode: string
    sortOrder: int
    subcategoryDescription: string (joined from drsSubCategories)
    catCode: string (joined from drsSubCategories)
    catDescription: string (joined from drsCategories)

# Frontend state management (in designer.html)
STATE: AssignmentState (MODIFY - move from node.data to separate object)
  FIELDS:
    assignmentsBySocket: Map<socketId, SocketAssignment[]>
    assignmentsByRack: Map<rackId, Map<zoneNumber, SocketAssignment[]>>
    isLoading: boolean
    lastSaveTime: Date|null
    isDirty: boolean
    previousValue: SocketAssignment|null  # For revert on save failure

# Dirty State Indicator Behavior
VISUAL: Orange dot displayed next to "Save" button (if present) or in toolbar status area
RULES:
  - Set isDirty=true when user changes any assignment
  - Set isDirty=false when auto-save completes successfully
  - If save fails after 3 retries: isDirty remains true, prompting user to retry
  - Indicator clears immediately on successful save response
```

#### Integration Points

```yaml
# Internal Integration (within Floor Plan module)
- from: designer.html (Frontend)
  to: FloorPlanApiController (Backend)
    - protocol: REST over HTTPS
    - endpoints: [GET /assignments, PUT /assignments/bulk]
    - data_flow: "Load assignments on init, save on change"

# Existing Integration (Reports - no changes)
- from: reports.html (Frontend)
  to: HeatmapService (Backend)
    - protocol: REST over HTTPS
    - endpoints: [GET /heatmap/sales, GET /heatmap/velocity]
    - data_flow: "Already queries database correctly"
```

### Implementation Examples

#### Example: Frontend Assignment Hydration

**Why this example**: Shows the critical initialization flow that currently doesn't exist - loading assignments from database on page load.

```javascript
// Example: Load assignments from database on designer initialization
// This demonstrates the new hydration pattern
async function loadAssignmentsFromDatabase(layoutId) {
    if (!layoutId) {
        console.warn('No layout ID, skipping assignment load');
        return {};
    }

    try {
        const response = await fetch(
            `${CONFIG.apiBase}/layouts/${layoutId}/assignments`,
            {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-Token': CONFIG.csrfToken
                }
            }
        );

        const result = await response.json();

        if (!result.success) {
            console.error('Failed to load assignments:', result.error);
            return {};
        }

        // result.assignments is keyed by socketId
        // Need to map to rackId -> zoneNumber -> assignments for UI
        return mapAssignmentsToRacks(result.assignments, result.racks);

    } catch (error) {
        console.error('Error loading assignments:', error);
        showToast('Failed to load zone assignments', 'error');
        return {};
    }
}

// Map socket-based assignments to rack/zone structure for UI
function mapAssignmentsToRacks(assignmentsBySocket, racks) {
    const assignmentsByRack = {};

    for (const rack of racks) {
        assignmentsByRack[rack.id] = {};

        // Sockets are ordered by sortOrder - map to zone numbers (1-based)
        rack.sockets.forEach((socket, index) => {
            const zoneNumber = index + 1;
            const socketAssignments = assignmentsBySocket[socket.id] || [];

            assignmentsByRack[rack.id][zoneNumber] = socketAssignments.map(a => ({
                code: a.subcategoryCode,
                name: a.subcategoryDescription,
                categoryCode: a.catCode,
                categoryName: a.catDescription
            }));
        });
    }

    return assignmentsByRack;
}
```

#### Example: Populate Diagram Nodes with Database Assignments

**Why this example**: Shows how to apply loaded assignments to SyncFusion diagram nodes after hydration.

```javascript
// Example: Apply database assignments to diagram nodes
function hydrateNodeAssignments(diagram, assignmentsByRack) {
    diagram.nodes.forEach(node => {
        if (!node.data?.rackId) return;

        const rackAssignments = assignmentsByRack[node.data.rackId];
        if (rackAssignments) {
            // Replace any JSON-stored assignments with database truth
            node.data.assignments = rackAssignments;
        } else {
            // Rack exists but no assignments - initialize empty
            node.data.assignments = {};
        }
    });

    // Trigger UI refresh
    diagram.dataBind();
}
```

#### Example: Migration Job Logic

**Why this example**: Shows the one-time migration pattern to move existing diagram JSON assignments to database.

**Migration Scope:** The migration processes ALL layouts for each floor plan, including:
- Current layouts (active store state)
- Wanted layouts (draft, scheduled, archived)

Each layout's assignments are extracted from its associated diagram JSON and stored with the correct `layoutId`.

```php
// Example: Migration job to extract assignments from diagram JSON
// This runs once per store to populate fpSocketAssignments for ALL layouts

public function migrateStoreAssignments(string $typeNum): array
{
    $stats = ['processed' => 0, 'migrated' => 0, 'skipped' => 0, 'errors' => 0];

    // Get all floor plans with diagram data
    $floorPlans = $this->floorPlanService->getAll();

    foreach ($floorPlans as $floorPlan) {
        // Get ALL layouts for this floor plan (current + wanted)
        $layouts = $this->layoutService->getAllLayoutsForFloorPlan($floorPlan->id);

        foreach ($layouts as $layout) {
            // Each layout has its own diagram JSON
            $diagramData = $layout->diagramData ?? $floorPlan->diagramData;
            if (empty($diagramData)) {
                continue;
            }

            $diagramJson = json_decode($diagramData, true);
            if (!$diagramJson || empty($diagramJson['nodes'])) {
                continue;
            }

            foreach ($diagramJson['nodes'] as $node) {
                $stats['processed']++;

                // Skip non-rack nodes
                if (empty($node['data']['rackId'])) {
                    continue;
                }

                $rackId = (int) $node['data']['rackId'];
                $assignments = $node['data']['assignments'] ?? [];

                if (empty($assignments)) {
                    $stats['skipped']++;
                    continue;
                }

                try {
                    // Convert zone-based assignments to socket-based
                    $socketAssignments = $this->convertZoneToSocket($rackId, $assignments);

                    // Check for existing assignments (idempotency)
                    $existing = $this->getExistingAssignmentCount($layout->id, $rackId);
                    if ($existing > 0) {
                        // Already migrated - skip to preserve manual edits
                        $stats['skipped']++;
                        continue;
                    }

                    // Bulk insert with correct layoutId
                    $this->layoutService->bulkUpdateAssignments($layout->id, $socketAssignments);
                    $stats['migrated']++;

                } catch (\Exception $e) {
                    error_log("Migration error for layout {$layout->id}, rack $rackId: " . $e->getMessage());
                    $stats['errors']++;
                }
            }
        }
    }

    return $stats;
}
```

## Runtime View

### Primary Flow

#### Primary Flow: Loading Designer with Database Assignments
1. User navigates to Floor Plan Designer page
2. Page renders with SyncFusion diagram and existing racks
3. **NEW**: JavaScript calls `GET /assignments` API for current layout
4. API returns assignments grouped by socketId with rack mapping
5. Frontend maps assignments to rack nodes and populates zone panels
6. User sees existing assignments in zone dropdowns

```mermaid
sequenceDiagram
    actor User
    participant Browser
    participant Designer as designer.html
    participant API as FloorPlanApiController
    participant Service as LayoutService
    participant DB as fpSocketAssignments

    User->>Browser: Navigate to Designer
    Browser->>Designer: Load page
    Designer->>Designer: Initialize SyncFusion Diagram
    Designer->>Designer: Parse racks from CONFIG

    Note over Designer: NEW: Hydrate from Database
    Designer->>API: GET /layouts/:layoutId/assignments
    API->>Service: getAssignmentsGroupedBySocket()
    Service->>DB: SELECT with JOINs
    DB-->>Service: Assignment rows
    Service-->>API: Assignments by socketId
    API-->>Designer: {success, assignments, racks}

    Designer->>Designer: mapAssignmentsToRacks()
    Designer->>Designer: hydrateNodeAssignments()
    Designer-->>User: Display with assignments
```

#### Primary Flow: Saving Zone Assignment
1. User selects subcategory in zone dropdown
2. Frontend triggers debounced save (300ms)
3. Save calls `PUT /assignments/bulk` API
4. API translates zone numbers to socket IDs
5. Database updated with new assignments
6. Toast notification "Assignment saved"

```mermaid
sequenceDiagram
    actor User
    participant Designer as designer.html
    participant API as FloorPlanApiController
    participant Service as LayoutService
    participant DB as fpSocketAssignments

    User->>Designer: Change zone dropdown
    Designer->>Designer: onZoneAssignmentChange()
    Designer->>Designer: Update node.data.assignments
    Designer->>Designer: Debounce 300ms

    Designer->>API: PUT /assignments/bulk
    Note over API: {rackId, assignments: {zone: [codes]}}
    API->>API: Map zones to socketIds
    API->>Service: bulkUpdateAssignments()
    Service->>DB: DELETE old + INSERT new
    DB-->>Service: Success
    Service-->>API: {updated: count}
    API-->>Designer: {success: true}

    Designer->>Designer: showToast("Assignment saved")
    Designer-->>User: Visual confirmation
```

### Error Handling

| Error Type | User Message | Recovery Action |
|------------|--------------|-----------------|
| Network timeout on load | "Failed to load zone assignments. Click to retry." | Retry button triggers re-fetch |
| Network timeout on save | "Failed to save assignment. Retrying..." | Auto-retry with exponential backoff (3 attempts) |
| API validation error | "Invalid assignment data" | Revert UI selection, log details |
| 404 Layout not found | "Layout no longer exists. Please refresh." | Redirect to floor plan list |
| 403 Permission denied | "You don't have permission to edit assignments" | Disable edit controls, show read-only mode |
| 500 Server error | "Something went wrong. Please try again." | Log error, show retry option |

#### Save Failure Revert Behavior

When auto-save fails after 3 retry attempts:
1. Show error toast: "Failed to save assignment. Please try again."
2. REVERT dropdown selection to previous value (stored in `AssignmentState.previousValue`)
3. Dirty indicator remains visible (prompting user awareness)
4. User can manually retry by changing the selection again

**Implementation:**
```javascript
// Before attempting save, store current value
state.previousValue = getCurrentAssignment(socketId);

// After 3 failed retries
function handleSaveFailure(socketId, error) {
    showToast('Failed to save assignment. Please try again.', 'error');
    revertDropdownTo(socketId, state.previousValue);
    emitAnalytics('floor_plan_assignment_error', {
        error_type: error.type,
        error_message: error.message,
        layout_id: currentLayoutId
    });
}
```

### Complex Logic

```
ALGORITHM: Map Zone Numbers to Socket IDs
INPUT: rackId, zoneAssignments (keyed by 1-based zone number)
OUTPUT: socketAssignments (keyed by socketId)

1. QUERY sockets for rackId ordered by sortOrder ASC, id ASC
2. BUILD position map:
   FOR i = 0 to sockets.length:
     zoneNumber = i + 1  // 1-based
     positionMap[zoneNumber] = sockets[i].socketId
3. TRANSFORM assignments:
   FOR each (zoneNumber, categories) in zoneAssignments:
     socketId = positionMap[zoneNumber]
     IF socketId is NULL:
       LOG warning "Zone {zoneNumber} has no socket"
       CONTINUE
     EXTRACT codes from categories
     socketAssignments[socketId] = codes
4. RETURN socketAssignments
```

## Deployment View

### Single Application Deployment

- **Environment**: PHP 8.x application on Apache/nginx with MySQL
- **Configuration**: No new environment variables required
- **Dependencies**: No new external dependencies
- **Performance**:
  - Assignment load target: < 500ms (single DB query with JOINs)
  - Assignment save target: < 300ms (DELETE + INSERT in transaction)
  - Caching: Not needed for typical usage (< 200 assignments per store)

### Deployment Steps

1. **Deploy backend changes** (API response enhancement)
2. **Deploy frontend changes** (assignment hydration)
3. **Run migration job** per store via CLI command
4. **Verify** reports show correct data

### Rollback Strategy

- If issues discovered: revert frontend changes
- Database assignments remain valid (no schema changes)
- Diagram JSON still contains assignments (not deleted)
- Can revert to old behavior within minutes

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used
- pattern: Single Source of Truth
  relevance: CRITICAL
  why: "Database is the only authoritative source for assignments"

- pattern: Auto-save with Debounce
  relevance: HIGH
  why: "Immediate persistence without API spam"

- pattern: Position-based Index Mapping
  relevance: HIGH
  why: "Maps 1-based zone numbers to socket IDs by sortOrder position"
  doc: docs/patterns/frontend-backend-index-mapping.md

# Patterns from existing codebase
- pattern: CSRF Token Refresh
  relevance: MEDIUM
  why: "API responses include fresh tokens for subsequent requests"
```

### Analytics Instrumentation

Events from PRD tracking requirements, with emission locations specified:

| Event Name | Location | Trigger | Properties |
|------------|----------|---------|------------|
| `floor_plan_designer_opened` | Frontend | Page load complete | `has_assignments`, `rack_count`, `zone_count` |
| `floor_plan_assignment_loaded` | Frontend | After hydration completes successfully | `layout_id`, `rack_count`, `assignment_count`, `load_duration_ms` |
| `floor_plan_assignment_saved` | Backend | After successful database save | `layout_id`, `socket_id`, `subcategory_codes`, `save_duration_ms` |
| `floor_plan_assignment_error` | Frontend | On save failure (after retries exhausted) | `error_type`, `error_message`, `layout_id` |

**Note:** `floor_plan_heatmap_viewed` is out of scope for this phase (emitted from reports page, not designer).

**Implementation Pattern:**
```javascript
// Frontend event emission (designer.html)
analytics.track('floor_plan_assignment_loaded', {
    layout_id: layoutId,
    rack_count: racks.length,
    assignment_count: Object.keys(assignments).length,
    load_duration_ms: Date.now() - loadStartTime
});
```

```php
// Backend event emission (FloorPlanApiController.php)
$this->analytics->track('floor_plan_assignment_saved', [
    'layout_id' => $layoutId,
    'socket_id' => $socketId,
    'subcategory_codes' => $codes,
    'save_duration_ms' => $duration
]);
```

### System-Wide Patterns

- **Security**: Uses existing session authentication + CSRF protection
- **Error Handling**: Try-catch with error logging, user-friendly messages
- **Performance**: Database indexes on layoutId, socketId already exist
- **Logging**: Uses existing `error_log()` pattern for PHP errors
- **Audit**: Existing `FloorPlanAuditService` captures assignment changes

### Implementation Patterns

#### Code Patterns and Conventions

- PHP: PSR-4 namespaces, type hints, camelCase methods
- JavaScript: ES6+ async/await, const/let, template literals
- API: JSON responses with `success` boolean, consistent error structure

#### State Management Patterns

- Frontend: Assignments stored in `node.data.assignments` per diagram node
- Backend: Stateless - all state in database
- CSRF: Token refreshed after each successful API call

#### Error Handling Pattern

```pseudocode
FUNCTION: handleApiError(error, context)
  CLASSIFY:
    IF error.status == 403: permission_error
    IF error.status == 404: not_found_error
    IF error.status >= 500: server_error
    IF error.timeout: network_error
    ELSE: validation_error

  LOG: error_details to console (dev) and server (prod)

  RESPOND:
    SHOW toast with user-friendly message
    IF retryable: show retry button
    IF permission: disable edit controls
```

#### Test Pattern

```pseudocode
TEST_SCENARIO: "Assignment loads from database on page init"
  SETUP:
    - Floor plan with layout in database
    - Assignments in fpSocketAssignments table
    - Mock API response
  EXECUTE:
    - Initialize designer page
    - Trigger assignment load
  VERIFY:
    - API called with correct layoutId
    - Node assignments populated
    - Zone dropdowns show correct selections
```

### Integration Points

- **Connection Points**: Frontend <-> FloorPlanApiController <-> LayoutService <-> Database
- **Data Flow**: Assignments flow from DB -> API -> Frontend on load; Frontend -> API -> DB on save
- **Events**: No event system changes; standard HTTP request/response

## Architecture Decisions

- [x] ADR-1 **Database as Single Source of Truth**: All zone assignments stored only in `fpSocketAssignments` table
  - Rationale: Eliminates dual-storage confusion; reports already query DB
  - Trade-offs: Requires API call on page load (adds ~200-500ms)
  - Alternatives considered: Keep diagram JSON as cache - rejected due to sync complexity
  - User confirmed: YES (autopilot mode)

- [x] ADR-2 **Preserve and Ignore Diagram JSON Assignments**: Existing assignment data in diagram JSON is preserved but completely ignored by frontend
  - Rationale: Non-destructive migration; safe rollback; aligns with PRD principle "migration must be non-destructive"
  - Trade-offs: Diagram JSON contains stale data (not used, but harmless)
  - Alternatives considered: Strip assignments from JSON on save - adds complexity without benefit and contradicts non-destructive principle
  - User confirmed: YES (autopilot mode)

- [x] ADR-3 **One-Time Migration Job**: CLI command migrates existing JSON assignments to database
  - Rationale: Handles existing data; idempotent; can be run per-store
  - Trade-offs: Manual step required after deployment
  - Alternatives considered: Auto-migrate on page load - rejected due to race conditions
  - User confirmed: YES (autopilot mode)

- [x] ADR-4 **Position-Based Zone-to-Socket Mapping**: Zone numbers (1-based) map to sockets by position in sortOrder, not by sortOrder value
  - Rationale: Handles inconsistent sortOrder values (gaps, 0-based vs 1-based)
  - Trade-offs: If sockets reordered, existing assignments may map to wrong zones
  - Alternatives considered: Map by sortOrder value - fails with legacy data
  - User confirmed: YES (autopilot mode)

- [x] ADR-5 **Auto-Save with 300ms Debounce**: Save triggered automatically after zone selection changes
  - Rationale: Immediate persistence; no "forgot to save" issues
  - Trade-offs: More API calls; requires good error handling
  - Alternatives considered: Explicit save button - rejected per PRD decision
  - User confirmed: YES (autopilot mode)

## Quality Requirements

| Requirement | Target | Measurement |
|-------------|--------|-------------|
| Assignment Load Time | < 500ms | Time from page init to assignments displayed |
| Assignment Save Time | < 300ms | Time from selection change to save confirmation |
| Data Consistency | 100% | Reports match designer within 5 seconds of save |
| Error Recovery | User can retry | All errors have retry path or clear next step |
| Migration Success | 100% of valid data | Migration job reports processed/migrated/errors |

## Risks and Technical Debt

### Known Technical Issues

- **Stale Diagram JSON**: After migration, diagram JSON will contain outdated assignment data
  - Impact: Minimal - data ignored by frontend
  - Mitigation: Consider future cleanup job (low priority)

- **Legacy sortOrder Values**: Some racks may have inconsistent sortOrder (gaps, 0-based)
  - Impact: Position-based mapping handles this correctly
  - Mitigation: Document pattern in codebase patterns

### Technical Debt

- **Dual Storage Period**: During rollout, both JSON and DB may have assignments
  - Plan: Migration job runs after deployment; short overlap period

- **No Real-Time Collaboration**: Last-write-wins for concurrent edits
  - Plan: Acceptable per PRD; audit log captures all changes

### Implementation Gotchas

1. **Zone Number vs Socket ID**: Frontend uses 1-based zone numbers; backend uses socketId. Translation happens in `bulkUpdateAssignments`. Do not confuse these.

2. **CSRF Token Refresh**: After each successful API call, extract new token from response and update `CONFIG.csrfToken`.

3. **Empty Assignments**: When a zone has no assignments, frontend should send empty array, not omit the key.

4. **Non-PC Stores**: `drsSubCategories` table may not exist. Existing code handles this with fallback query.

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Assignments Load on Page Init**
```gherkin
Given: A floor plan with layout ID 1 exists
And: Socket assignments exist in fpSocketAssignments for layout 1
When: User navigates to Floor Plan Designer
Then: GET /assignments/1 API is called
And: Zone dropdowns show correct subcategory selections
And: Loading spinner is hidden within 100ms of response
```

**Scenario 2: Assignment Save Success**
```gherkin
Given: User is on Floor Plan Designer with rack selected
And: Zone 1 currently has no assignments
When: User selects "Mens Shoes" from Zone 1 dropdown
And: 300ms debounce period elapses
Then: PUT /assignments/bulk API is called
And: Request body contains rackId and zone 1 assignment
And: Toast "Assignment saved" appears for 3 seconds
And: fpSocketAssignments table contains new record
```

**Scenario 3: Assignment Load Failure Recovery**
```gherkin
Given: User navigates to Floor Plan Designer
When: GET /assignments API returns 500 error
Then: Toast "Failed to load zone assignments. Click to retry." appears
And: Retry button is visible
When: User clicks retry button
Then: GET /assignments API is called again
```

**Scenario 4: Migration Job Idempotency**
```gherkin
Given: Migration job ran successfully yesterday
And: fpSocketAssignments has records for layout 1
When: Migration job runs again for same store
Then: Existing records are NOT duplicated
And: Job reports "skipped" count for already-migrated racks
And: No errors occur
```

**Scenario 5: Invalid Zone Mapping**
```gherkin
Given: Rack has 3 sockets (zones 1, 2, 3)
When: API receives assignment for zone 5
Then: Zone 5 assignment is skipped
And: Response includes warning about skipped zones
And: Valid zones 1-3 are still processed
```

### Test Coverage Requirements

- **Business Logic**: Zone-to-socket mapping, assignment save/load
- **User Interface**: Zone dropdown population, toast notifications, loading states
- **Integration Points**: API request/response format, CSRF token handling
- **Edge Cases**: Empty assignments, missing sockets, concurrent saves
- **Performance**: Load time under 500ms, save time under 300ms
- **Security**: Permission checks, CSRF validation

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Zone | A numbered position within a rack where merchandise categories are assigned | Zones are 1-based (Zone 1, Zone 2, etc.) in the UI |
| Socket | Database representation of a zone | Maps 1:1 to zones via sortOrder position |
| Assignment | A mapping of a subcategory to a zone/socket | Stored in fpSocketAssignments table |
| Layout | A version of floor plan assignments (current, wanted, draft) | Assignments are per-layout |
| Rack | A fixture on the floor plan containing one or more zones | SyncFusion diagram node with rack data |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| Hydration | Loading data from server to populate UI state | Assignment hydration on page load |
| Debounce | Delay execution until input stops for specified time | 300ms debounce on assignment save |
| sortOrder | Integer field defining position order in database | Sockets sorted by sortOrder determine zone mapping |
| syncfusionNodeId | Unique identifier for diagram nodes | Links SyncFusion nodes to database racks |

### API/Interface Terms

| Term | Definition | Context |
|------|------------|---------|
| bulkUpdateAssignments | API method to replace all assignments for specified sockets | PUT /assignments/bulk endpoint |
| getAssignmentsGroupedBySocket | Service method returning assignments keyed by socketId | Used by GET /assignments API |
| CSRF Token | Cross-Site Request Forgery protection token | Required header on all POST/PUT/DELETE requests |
