# Solution Design Document
# 045 - Workbook Backstock Management View

## 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 **Framework**: PHP 8.x + Slim 2.6.2 backend, Twig 1.44.8 templates, vanilla JS frontend with Syncfusion EJ2 components. No build system for JS (direct script includes).

CON-2 **API Reuse**: Must reuse existing `/api/:typeNum/backstock/*` endpoints. No new CRUD endpoint logic. Only new endpoints for Ably event publishing hooks if needed.

CON-3 **Feature Flag Convention**: TINYINT(1) column on `kiosk_buykiosk.stores` table, checked via `Store::getFeatureFlag()`. Migration system for DB changes.

CON-4 **SPA Architecture**: Workbook uses view-switching SPA pattern (CSS class toggle, History API, `viewChanged` custom event). No page reloads between views.

CON-5 **Real-Time**: Ably Realtime for client-side, Ably REST for PHP server-side. Existing throttle system (40 msg/sec per channel). Single channel per store (`typeNum`).

CON-6 **Syncfusion**: Must use Syncfusion EJ2 Grid, DropDownList, MultiSelect, DatePicker. Follow documented gotchas (destroy before recreate, visible container init, no itemTemplate with CheckBox mode).

CON-7 **No Build System**: JS files loaded via `<script>` tags in workspace-foot.html. No webpack/vite bundling. Cache-busting via `?v={{ css_version }}` query param.

## Implementation Context

### Required Context Sources

- ICO-1 General Application Context
 ```yaml
 - doc: CLAUDE.md
   relevance: HIGH
   why: "Stack details, conventions, migration system, Syncfusion gotchas"

 - doc: docs/specs/045-workbook-backstock-report/product-requirements.md
   relevance: CRITICAL
   why: "All feature requirements, acceptance criteria, edge cases"
 ```

- ICO-2 Workspace SPA Architecture
 ```yaml
 - file: userfrosting/templates/themes/default/workspace/workspace.html
   relevance: CRITICAL
   why: "View container structure, SPA view registration pattern"

 - file: public_html/js/workspace/modules/common/spa-navigation.js
   relevance: CRITICAL
   why: "View switching, History API, viewChanged event dispatch, onViewActivated()"

 - file: userfrosting/templates/themes/default/workspace/partials/sidebar-nav.html
   relevance: HIGH
   why: "Navigation item registration pattern with data-view/data-url"

 - file: userfrosting/templates/themes/default/workspace/layouts/workspace-foot.html
   relevance: HIGH
   why: "JS module loading pattern, conditional script includes"

 - file: userfrosting/src/BuyerKiosk/Workbook/Controllers/WorkbookPageController.php
   relevance: HIGH
   sections: [pageSpaView, pageWorkbook]
   why: "Deep-link routing, template data injection"

 - file: userfrosting/routes/workbook/pages.php
   relevance: HIGH
   why: "Workbook route definitions and patterns"
 ```

- ICO-3 Admin Backstock Implementation
 ```yaml
 - file: userfrosting/templates/themes/default/backstock/home.html
   relevance: HIGH
   why: "Admin UI structure to replicate"

 - file: userfrosting/templates/themes/default/backstock/js/main.js
   relevance: CRITICAL
   why: "Syncfusion Grid init, column definitions, search, selection patterns"

 - file: userfrosting/templates/themes/default/backstock/js/floatingBar.js
   relevance: HIGH
   why: "Floating action bar pattern"

 - file: userfrosting/templates/themes/default/backstock/js/massEditModals.js
   relevance: HIGH
   why: "Mass edit modal patterns, Syncfusion component lifecycle"

 - file: userfrosting/templates/themes/default/backstock/js/manageBin.js
   relevance: HIGH
   why: "Manage modal tabs, unified save pattern"

 - file: userfrosting/routes/groups/backstock.php
   relevance: HIGH
   why: "All API endpoint definitions being reused"

 - file: userfrosting/src/BuyerKiosk/Backstock/Controllers/BackstockController.php
   relevance: MEDIUM
   why: "Controller pattern for bin data preparation"

 - file: public_html/css/admin/modules/backstock.css
   relevance: MEDIUM
   why: "Existing backstock styles to adapt"
 ```

- ICO-4 Feature Flag & Store Config
 ```yaml
 - file: userfrosting/src/BuyerKiosk/Core/Store.php
   relevance: HIGH
   sections: [getFeatureFlag, createStoreFromRowArray, embeddedChatEnabled property]
   why: "Feature flag implementation pattern"

 - file: userfrosting/templates/themes/default/store/configuration.html
   relevance: MEDIUM
   why: "Admin settings toggle UI pattern"

 - file: userfrosting/src/BuyerKiosk/StoreConfig/Controllers/StoreConfigController.php
   relevance: MEDIUM
   sections: [updateMpcSettings]
   why: "Configuration save pattern, cache invalidation"
 ```

- ICO-5 Ably Real-Time Integration
 ```yaml
 - file: userfrosting/src/BuyerKiosk/Workbook/WorkbookAbly.php
   relevance: CRITICAL
   why: "Server-side Ably publishing pattern, payload structure, throttling"

 - file: public_html/js/workspace/modules/workbook/ably-sync.js
   relevance: CRITICAL
   why: "Client-side Ably subscription, message routing, deduplication, connection lifecycle"

 - file: userfrosting/src/BuyerKiosk/Core/AblyPublishThrottle.php
   relevance: MEDIUM
   why: "Rate limiting pattern for Ably publishes"
 ```

### Implementation Boundaries

- **Must Preserve**: All existing backstock API endpoints, admin panel backstock functionality, existing workbook views and navigation, Store class interface
- **Can Modify**: `WorkbookPageController.php` (add data/flags), `workspace.html` (add view), `sidebar-nav.html` (add nav item), `workspace-foot.html` (add script includes), `spa-navigation.js` (add onViewActivated handler), `Store.php` (add flag property/map entry), store config template (add toggle)
- **Must Not Touch**: Existing backstock API route handlers (`routes/groups/backstock.php`), `BackstockController.php`, `BackstockFactory.php`, `Bin.php`, `ReportService.php`, existing workbook panel (`BackstockPanelController`)

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    Employee[Store Employee] -->|SPA Navigation| WB[Workbook Backstock View]
    Owner[Store Owner] -->|Admin Settings| AC[Admin Config Page]

    WB -->|Fetch bins, actions| API[Existing Backstock API]
    WB -->|Subscribe| AblyRT[Ably Realtime Channel]

    API -->|Query| StoreDB[(Store Database)]
    API -->|Query| CentralDB[(Central Database)]
    API -->|Publish after mutation| AblyREST[Ably REST]

    AblyREST -->|Broadcast| AblyRT

    AC -->|Save flag| ConfigAPI[Store Config API]
    ConfigAPI -->|Update| CentralDB
```

#### Interface Specifications

```yaml
# Inbound Interfaces
inbound:
  - name: "Workbook SPA Navigation"
    type: HTTP/HTTPS
    format: HTML (initial load) + SPA view switch
    authentication: Session (UserFrosting auth)
    data_flow: "User navigates to backstock view"

  - name: "Ably Realtime Subscription"
    type: WebSocket
    format: JSON messages
    authentication: Ably API key
    data_flow: "Real-time bin updates from other users"

# Outbound Interfaces (reused, not new)
outbound:
  - name: "Backstock CRUD API"
    type: HTTPS
    format: REST (JSON)
    authentication: Session + checkStoreGroup
    data_flow: "Bin CRUD, mass edits, categories, locations"
    criticality: HIGH

  - name: "Ably REST Publishing"
    type: HTTPS
    format: JSON
    authentication: Ably API key
    data_flow: "Publish bin mutation events"
    criticality: MEDIUM

# Data Interfaces
data:
  - name: "Store Database"
    type: MySQL/MariaDB
    connection: PDO via dbConnectByName()
    data_flow: "Bins, locations, categories, actions (per-store)"

  - name: "Central Database (kiosk_buykiosk)"
    type: MySQL/MariaDB
    connection: PDO
    data_flow: "Store config, feature flags"

  - name: "Redis"
    type: Redis
    connection: Predis client
    data_flow: "Store cache, Ably throttle counters"
```

### Project Commands

```bash
# Testing
./test.sh --testsuite unit                    # Run all unit tests
cd userfrosting && ./vendor/bin/phpunit --filter "Backstock"  # Backstock-specific tests

# Database Migrations
php userfrosting/conductor run                # Run pending migrations

# CSS Build
php userfrosting/conductor build-css --minify # Production CSS build

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

# Local Dev
# Changes are live immediately via ngrok → dev2.buyerkiosk.com
```

## Solution Strategy

- **Architecture Pattern**: SPA View Extension — adds a new view to the existing workspace SPA container, following the established view-switching pattern (CSS class toggle + History API + viewChanged event).

- **Integration Approach**: API-first lazy loading. The view shell is rendered server-side (empty container), and bin data is fetched via existing API endpoints when the view activates. This avoids loading backstock data for users who never visit the view.

- **JS Module Pattern**: External JS files in `public_html/js/workspace/modules/backstock/`, following the workbook convention (chat-panel.js, schedule-ably-sync.js). Not inline Twig includes like the admin version.

- **Real-Time Pattern**: Dedicated `BackstockAblySync` class (following `schedule-ably-sync.js` pattern) that subscribes to `backstock:*` events on the store's Ably channel. Server-side publishing via new `BackstockAbly` PHP class extending the `WorkbookAbly` pattern.

- **Feature Flag**: Standard TINYINT column on `stores` table + `getFeatureFlag()` map entry. Admin toggle on store configuration page.

- **Key Decision**: Reuse existing API endpoints rather than creating new ones. The backstock API routes already use `checkStoreGroup()` for auth (verified), which covers workbook user access.

## Building Block View

### Components

```mermaid
graph LR
    subgraph "Workspace SPA"
        SB[Sidebar Nav] -->|click| SPA[SPA Navigation]
        SPA -->|activates| BV[Backstock View]
    end

    subgraph "Backstock JS Modules"
        BV --> BM[BackstockManager]
        BM --> BG[BackstockGrid]
        BM --> FP[BackstockFloatingBar]
        BM --> ME[MassEditModals]
        BM --> MB[ManageBinModal]
        BM --> BC[BinCreation]
        BM --> BA[BackstockAblySync]
    end

    subgraph "Existing Backend"
        BG -->|GET /bins| API[Backstock API]
        FP -->|POST /mass/*| API
        MB -->|POST /bin/:id/save-all| API
        BC -->|POST /bin/| API
        API -->|publish| APHY[BackstockAbly PHP]
        APHY -->|broadcast| ABLY[Ably Channel]
        ABLY -->|subscribe| BA
        BA -->|update| BG
    end

    subgraph "Feature Flag"
        FF[Store Flag] -->|gate| SB
        FF -->|gate| BV
        AT[Admin Toggle] -->|save| FF
    end
```

### Directory Map

**Component**: Workbook Backstock View (Frontend)
```
public_html/
├── js/workspace/modules/backstock/
│   ├── BackstockManager.js          # NEW: Orchestrator - init, data loading, view lifecycle
│   ├── BackstockGrid.js             # NEW: Syncfusion Grid setup, columns, search, filtering
│   ├── BackstockFloatingBar.js      # NEW: Mass selection floating action bar
│   ├── BackstockMassEdit.js         # NEW: Mass edit modal dialogs (9 actions)
│   ├── BackstockManageBin.js        # NEW: Manage bin modal (5 tabs)
│   ├── BackstockBinCreation.js      # NEW: Add bin + bulk create modals
│   └── BackstockAblySync.js         # NEW: Ably real-time sync for backstock events
├── css/admin/modules/
│   └── backstock.css                # MODIFY: Add workbook-specific overrides (minimal)
```

**Component**: Workspace Templates
```
userfrosting/templates/themes/default/
├── workspace/
│   ├── workspace.html               # MODIFY: Add backstockView <main> container
│   ├── partials/
│   │   ├── sidebar-nav.html         # MODIFY: Add Backstock nav item (feature-flagged)
│   │   └── backstock/
│   │       ├── backstock-view.html  # NEW: Backstock view content (grid, toolbar, stats)
│   │       ├── manage-bin-modal.html # NEW: Manage bin modal template
│   │       ├── mass-edit-modals.html # NEW: Mass edit confirmation modals
│   │       ├── add-bin-modal.html   # NEW: Add bin modal template
│   │       ├── bulk-create-modal.html # NEW: Bulk create modal template
│   │       ├── locations-modal.html # NEW: Locations management modal
│   │       └── categories-modal.html # NEW: Categories management modal
│   └── layouts/
│       └── workspace-foot.html      # MODIFY: Add backstock JS includes (feature-flagged)
```

**Component**: Backend (PHP)
```
userfrosting/
├── src/BuyerKiosk/
│   ├── Workbook/
│   │   ├── Controllers/
│   │   │   └── WorkbookPageController.php  # MODIFY: Pass feature flag to template
│   │   └── BackstockAbly.php               # NEW: Server-side Ably publishing for backstock
│   ├── Core/
│   │   └── Store.php                       # MODIFY: Add flag property + flagMap entry
│   └── StoreConfig/Controllers/
│       └── StoreConfigController.php       # MODIFY: Add flag to updateMpcSettings()
├── routes/
│   └── workbook/
│       └── pages.php                       # MODIFY: Add /:typeNum/workbook/backstock route
├── migrations/input/
│   └── YYYYMMDD_045_001_workbook_backstock_flag.json  # NEW: Add column to stores table
└── templates/themes/default/
    └── store/configuration.html            # MODIFY: Add toggle in Workbook Features section
```

**Component**: SPA Navigation
```
public_html/js/workspace/modules/common/
└── spa-navigation.js                       # MODIFY: Add 'backstock' to onViewActivated()
```

### Interface Specifications

#### Data Storage Changes

```yaml
# Central database: kiosk_buykiosk
Table: stores
  ADD COLUMN: workbookBackstockEnabled TINYINT(1) DEFAULT 0
    COMMENT 'Feature flag: enable backstock management in workbook'
    AFTER embeddedChatEnabled

# Migration file: userfrosting/migrations/input/YYYYMMDD_045_001_workbook_backstock_flag.json
# check_query: SELECT COLUMN_NAME FROM information_schema.COLUMNS
#   WHERE TABLE_SCHEMA = 'kiosk_buykiosk' AND TABLE_NAME = 'stores'
#   AND COLUMN_NAME = 'workbookBackstockEnabled'
# If exists → skip (idempotent)
```

No store-database changes required. All backstock tables (`bsBins`, `bsLocations`, `bsCategories`, `bsActions`) are unchanged.

#### Internal API Changes

No new API endpoints. All bin CRUD, mass edit, location, category, and action endpoints are reused from the existing backstock API.

**Existing endpoints consumed by the workbook view:**

```yaml
# Bin Operations
GET  /api/:typeNum/backstock/bins/           # List all active bins
GET  /api/:typeNum/backstock/bins/hidden/    # List hidden bins
GET  /api/:typeNum/backstock/bin/:id/        # Get single bin
POST /api/:typeNum/backstock/bin/            # Create bin
POST /api/:typeNum/backstock/bin/:id/save-all/ # Unified save (edit + actions)
DELETE /api/:typeNum/backstock/bin/:id/       # Delete bin

# Mass Edit Operations
POST /api/:typeNum/backstock/mass/change-location/
POST /api/:typeNum/backstock/mass/empty/
POST /api/:typeNum/backstock/mass/change-category/
POST /api/:typeNum/backstock/mass/change-tags/
POST /api/:typeNum/backstock/mass/hide/
POST /api/:typeNum/backstock/mass/unhide/
POST /api/:typeNum/backstock/mass/print-labels/
POST /api/:typeNum/backstock/mass/update-age/
POST /api/:typeNum/backstock/mass/add-notes/

# Supporting Data
GET  /api/:typeNum/backstock/locations/
POST /api/:typeNum/backstock/locations/
POST /api/:typeNum/backstock/locations/:id/edit/
POST /api/:typeNum/backstock/locations/:id/delete/
GET  /api/:typeNum/backstock/categories/
POST /api/:typeNum/backstock/categories/
POST /api/:typeNum/backstock/categories/:id/edit/
POST /api/:typeNum/backstock/categories/:id/delete/

# Actions & History
GET  /api/:typeNum/backstock/bin/:id/actions/
POST /api/:typeNum/backstock/actions/

# Audit & Enhancement
GET  /api/:typeNum/backstock/bins/:id/details/
PUT  /api/:typeNum/backstock/bins/:id/notes/
PUT  /api/:typeNum/backstock/bins/:id/item-count/
PUT  /api/:typeNum/backstock/bins/:id/estimated-value/
POST /api/:typeNum/backstock/bins/:id/audit/

# Label Printing
POST /api/:typeNum/backstock/reprint/
POST /api/:typeNum/backstock/bulkCreate/

# CSV Export (Feature 10)
# No new endpoint needed. Export is generated client-side:
# - Read current Syncfusion Grid dataSource (respects active filters/search)
# - Generate CSV with columns: Bin Name, Location, On-Site, Main Category, Other Categories, Age, Last Action
# - Trigger browser download via Blob URL
# - Alternative: If server-side export needed for large datasets, reuse GET /bins/ with
#   query params for current filters and a ?format=csv parameter (future enhancement)

# Store Config (for flag toggle)
PUT  /api/:typeNum/store/mpc-settings   # Add workbookBackstockEnabled to payload
```

**One modification to existing endpoints:** After successful mutations (create, save-all, delete, mass-*), the existing API controllers need to publish Ably events. This is done by adding a `BackstockAbly` publish call in a non-blocking `try/catch` at the end of each controller action, following the pattern from `TasksApiController`.

#### Application Data Models

```pseudocode
ENTITY: Store (MODIFIED)
  FIELDS:
    + workbookBackstockEnabled: TINYINT(1) DEFAULT 0 (NEW)

  BEHAVIORS:
    ~ getFeatureFlag($flagName): Add 'workbook_backstock' => 'workbookBackstockEnabled' to $flagMap

ENTITY: BackstockAbly (NEW - PHP class)
  FIELDS:
    typeNum: string
    ably: AblyRest
    enabled: bool
    throttle: AblyPublishThrottle

  BEHAVIORS:
    + binCreated(int $binId, array $binData): void
    + binUpdated(int $binId, array $binData): void
    + binDeleted(int $binId): void
    + binHidden(int $binId): void
    + binUnhidden(int $binId): void
    + binsUpdated(array $binIds, string $actionType, ?array $binsData): void
    + publish(string $action, array $data): void

ENTITY: BackstockAblySync (NEW - JS class)
  FIELDS:
    typeNum: string
    channel: Ably.RealtimeChannel
    instanceId: string
    connected: bool
    callbacks: {onBinUpdate, onBinCreate, onBinDelete, onMassUpdate, onConnectionChange}

  BEHAVIORS:
    + connect(): void
    + disconnect(): void
    + handleMessage(message): void
    + isConnected(): bool

ENTITY: BackstockManager (NEW - JS class)
  FIELDS:
    typeNum: string
    grid: BackstockGrid
    floatingBar: BackstockFloatingBar
    massEdit: BackstockMassEdit
    manageBin: BackstockManageBin
    ablySync: BackstockAblySync
    initialized: bool

  BEHAVIORS:
    + init(): void
    + loadBins(): Promise<void>
    + destroy(): void
    + onViewActivated(): void
    + onViewDeactivated(): void
    + exportCsv(): void

  CSV_EXPORT_BEHAVIOR:
    1. Read current grid dataSource (filtered if filters active, full if not)
    2. Map rows to CSV columns: Bin Name, Location, On-Site, Main Category, Other Categories, Age, Last Action
    3. Create Blob with UTF-8 BOM prefix (\uFEFF) for Excel compatibility
    4. Generate filename: "backstock-{typeNum}-{YYYY-MM-DD}.csv"
    5. Trigger browser download via temporary anchor + Blob URL
    6. Track analytics event: workbook_backstock_export
```

#### Integration Points

```yaml
# Ably Real-Time (NEW integration for backstock)
# NOTE: Uses the EXISTING store channel (e.g., "pc00"), NOT a separate "backstock:pc00" channel.
# Events are namespaced with "backstock:" prefix on the EVENT NAME, not the channel name.
# This aligns with the existing workbook Ably pattern where workbook events also use
# the same store channel with different event name prefixes.
- from: BackstockAbly (PHP)
  to: BackstockAblySync (JS)
  protocol: WebSocket (via Ably)
  channel: "{typeNum}" (e.g., "pc00")  # Same channel as workbook/schedule events
  event_names:                          # Event names (not channel names) are prefixed
    - "backstock:bin:created"           # Single bin created
    - "backstock:bin:updated"           # Single bin updated
    - "backstock:bin:deleted"           # Single bin deleted
    - "backstock:bin:hidden"            # Single bin hidden
    - "backstock:bin:unhidden"          # Single bin unhidden
    - "backstock:bins:mass-updated"     # Mass update (includes action type + affected IDs)
  payload_pattern:
    action: "backstock:{entity}:{verb}"
    binId: int (single) or binIds: int[] (mass)
    binData: object (full bin for <=50, null for >50 - client refetches)
    actionType: string (for mass: "change-location", "empty", etc.)
    instanceId: string (sender's instance ID for dedup)
    timestamp: int (Unix)
    source: "workbook"

# Feature Flag Check (existing pattern, new flag)
- from: WorkbookPageController (PHP)
  to: Store::getFeatureFlag()
  data_flow: "Check workbook_backstock flag on page render"

- from: sidebar-nav.html (Twig)
  to: workbook_backstock_enabled template variable
  data_flow: "Conditionally show/disable nav item"
```

### Implementation Examples

#### Example: BackstockManager Initialization

**Why this example**: Clarifies the SPA lifecycle — when to init, when to destroy, how to handle re-entry.

```javascript
// public_html/js/workspace/modules/backstock/BackstockManager.js
var BackstockManager = {
    typeNum: null,
    initialized: false,
    grid: null,
    ablySync: null,

    init: function(typeNum) {
        if (this.initialized) return;
        this.typeNum = typeNum;
        this.initialized = true;

        // Load data via API (not server-rendered)
        this.loadBins().then(function(binsData) {
            // Initialize Syncfusion Grid
            BackstockManager.grid = BackstockGrid.init(
                '#workbook-backstock-grid',
                binsData,
                typeNum
            );

            // Initialize floating bar
            BackstockFloatingBar.init(BackstockManager.grid);

            // Initialize Ably sync
            BackstockManager.ablySync = new BackstockAblySync(typeNum, {
                onBinUpdate: function(data) {
                    BackstockGrid.updateRow(data.binId, data.binData);
                },
                onBinCreate: function(data) {
                    BackstockGrid.addRow(data.binData);
                },
                onBinDelete: function(data) {
                    BackstockGrid.removeRow(data.binId);
                },
                onMassUpdate: function(data) {
                    // Refetch affected bins via API
                    BackstockManager.refreshBins(data.binIds);
                },
                onConnectionChange: function(state) {
                    BackstockManager.updateConnectionIndicator(state);
                }
            });
            BackstockManager.ablySync.connect();
        });
    },

    loadBins: function() {
        return fetch('/api/' + this.typeNum + '/backstock/bins/')
            .then(function(r) { return r.json(); })
            .then(function(data) { return data.bins || data; });
    },

    destroy: function() {
        if (this.ablySync) this.ablySync.disconnect();
        if (this.grid) this.grid.destroy();
        BackstockMassEdit.destroyAllComponents();
        this.initialized = false;
    },

    // Called when user navigates TO backstock view
    onViewActivated: function() {
        if (!this.initialized) {
            this.init(this.typeNum);
        } else {
            // Re-entering view: reload fresh data
            this.loadBins().then(function(binsData) {
                BackstockGrid.refreshDataSource(binsData);
            });
        }
    },

    // Called when user navigates AWAY from backstock view
    onViewDeactivated: function() {
        // Keep initialized but stop Ably to save resources
        // Re-connect on next activation
        if (this.ablySync) this.ablySync.disconnect();
    }
};
```

#### Example: Ably Publish After Mutation (PHP)

**Why this example**: Shows the non-blocking Ably publish pattern integrated into existing API controllers.

```php
// Added at end of existing mass edit controller actions
// e.g., in routes/groups/backstock.php after mass change-location success

try {
    $backstockAbly = new \BuyerKiosk\Workbook\BackstockAbly($typeNum);
    $backstockAbly->binsUpdated(
        $binIds,           // array of affected bin IDs
        'change-location', // action type
        $updatedBins       // array of updated bin data (or null for mass)
    );
} catch (\Exception $e) {
    error_log("BackstockAbly publish error: " . $e->getMessage());
    // Non-blocking: API response already sent
}
```

#### Example: Ably Mass Update Payload Optimization

**Why this example**: Documents the threshold-based payload strategy for mass updates.

```javascript
// BackstockAblySync.js - handling mass updates
handleMassUpdate: function(data) {
    if (data.binIds && data.binIds.length <= 50) {
        // Small batch: server sends full bin data, update in-place
        data.binsData.forEach(function(bin) {
            BackstockGrid.updateRow(bin.id, bin);
        });
    } else {
        // Large batch: server sends only IDs + action type
        // Client refetches affected bins via API
        BackstockManager.refreshBins(data.binIds);
    }
}
```

## Runtime View

### Primary Flow: Employee Views and Manages Bins

1. Employee clicks "Backstock" in workbook sidebar
2. SPA navigation switches to backstockView (CSS class toggle), updates URL
3. `viewChanged` event fires, `BackstockManager.onViewActivated()` called
4. BackstockManager fetches bins via `GET /api/:typeNum/backstock/bins/`
5. Syncfusion Grid renders with bin data
6. Employee interacts: search, filter, select, open manage modal
7. Changes saved via existing API endpoints
8. After save, API controller publishes Ably event
9. Other users' grids update via BackstockAblySync

```mermaid
sequenceDiagram
    actor Employee
    participant Sidebar
    participant SPA as SPA Navigation
    participant BM as BackstockManager
    participant API as Backstock API
    participant DB as Store Database
    participant Ably as Ably Channel
    participant Other as Other Users

    Employee->>Sidebar: Click "Backstock"
    Sidebar->>SPA: switchView('backstock')
    SPA->>SPA: Toggle CSS class, push history
    SPA->>BM: viewChanged event → onViewActivated()
    BM->>API: GET /api/:typeNum/backstock/bins/
    API->>DB: Query bsBins
    DB-->>API: Bin data
    API-->>BM: JSON response
    BM->>BM: Initialize Syncfusion Grid
    BM->>Ably: Subscribe to backstock:* events

    Employee->>BM: Select bins → Mass Edit → Change Location
    BM->>API: POST /api/:typeNum/backstock/mass/change-location/
    API->>DB: UPDATE bsBins SET location
    API->>Ably: Publish backstock:bins:mass-updated
    API-->>BM: Success response
    BM->>BM: Update grid rows, clear selection, toast

    Ably-->>Other: backstock:bins:mass-updated event
    Other->>Other: Update grid rows in-place
```

### Error Handling

| Error Type | Trigger | User Feedback | Recovery |
|-----------|---------|---------------|----------|
| API fetch failure (bins) | Network error, 500 | "Unable to load backstock data. Please try again." + retry button | Retry button reloads bins |
| API 403 | User lacks store access | "You don't have access to this store's backstock." | Redirect to workbook dashboard |
| Feature flag OFF on direct URL | Navigate to /workbook/backstock when flag OFF | Redirect to workbook dashboard | N/A |
| Mass edit partial failure | Some bins fail, some succeed | Warning toast: "X of Y bins updated. Z failed." Failed bins stay selected | User retries with remaining selection |
| Mass edit full failure | All bins fail (e.g., server error) | Error toast with message | All bins stay selected for retry |
| Manage bin save conflict | Another user modified bin since modal opened | Warning banner: "This bin was modified by another user. Save anyway?" | User can save (overwrite) or reload |
| Ably connection lost | Network issues | Subtle offline indicator dot in view header | Auto-reconnect; full refresh on reconnect |
| Ably connection restored | Network restored | Indicator clears; grid auto-refreshes | Automatic |
| Print job failure | Printer offline or service error | Error toast: "Print job failed. Please try again." | Retry button in toast |
| Search no results | No bins match query | Grid shows "No bins match your search" empty state | Clear search button |
| Bin creation validation error | Missing required fields | Inline field validation errors in modal | User corrects and retries |
| Delete confirmation | User clicks delete icon | "Are you sure? This cannot be undone." confirmation modal | Cancel or confirm |

### Detailed UI Behaviors

#### Hide Empty / Show Hidden Toggle Logic

The grid toolbar has two independent toggle switches:
- **"Hide Empty Bins"** (default: OFF) — When ON, filters out bins where `itemCount === 0`
- **"Show Hidden Bins"** (default: OFF) — When ON, fetches and includes hidden bins via `GET /api/:typeNum/backstock/bins/hidden/`

Toggle state machine:
```
Default: Hide Empty = OFF, Show Hidden = OFF → Shows all active bins (including empties, excluding hidden)
Hide Empty ON:  Filter out rows where bin.itemCount === 0 (client-side filter on grid)
Show Hidden ON: Merge hidden bins into grid datasource (separate API call, merged client-side)
Both ON:        Show hidden bins BUT filter empties (even hidden empties are filtered out)
```

Implementation: Both toggles use Syncfusion Grid's client-side filtering capability (`grid.filterByColumn()` for hide-empty, datasource merge + `dataBind()` for show-hidden). Toggle state persists for the session only (resets on page reload).

#### Comma-Separated Search UX

Search bar supports comma-separated terms (matching admin behavior):
- Input: `"jewelry, watches, rings"` → searches for bins matching ANY of the three terms
- Terms are split by comma, trimmed of whitespace
- Each term searches across: bin name, location name, category name, tags
- Results are the UNION of all matching terms (inclusive OR)
- A subtle help text below the search bar reads: "Tip: Use commas to search multiple terms"
- Search debounce: 300ms from last keystroke before executing

#### Mid-Session Feature Flag Toggle

If an admin disables the feature flag while a user has the backstock view open:
1. Existing open sessions continue working until next navigation (no real-time kill)
2. On next `onViewActivated()` call (re-entering the view), BackstockManager checks the flag
3. If flag is OFF: redirect to workbook dashboard, show info toast "Backstock has been disabled for this store"
4. Sidebar nav item reverts to disabled state on next full page load
5. No data loss — any in-progress edits should be saved before the view was left

Implementation: The flag check uses a lightweight `GET /api/:typeNum/store/feature-flags` call (or reads from the already-loaded store config object injected by WorkbookPageController). This avoids a hard check on every API call.

#### Zero Bins Empty State

When the store has zero backstock bins:
- Grid area displays a centered empty state illustration (Bootstrap 5 `.text-center` with icon)
- Heading: "No Backstock Bins Yet"
- Subtext: "Create your first bin to start tracking backstock inventory."
- Primary CTA button: "Create Bin" (opens add-bin modal)
- Secondary CTA: "Bulk Create" (opens bulk-create modal)
- Summary stats bar shows all zeros: Total: 0, On-Site: 0, Off-Site: 0

When all bins are filtered out by search/toggles but bins exist:
- Grid shows standard Syncfusion "No records to display" row
- Summary stats still reflect the TOTAL bin counts (not filtered counts)
- Clear filters button appears above grid: "Clear all filters"

#### Category Badge Click-to-Filter

Category badges (colored pills) displayed in the grid's "Categories" column are clickable:
- Click a category badge → applies a filter for that category
- Grid filters to show only bins with that category
- Active filter shown as a chip in the filter bar: "Category: [name] ✕"
- Click the ✕ to remove the filter
- Multiple category filters stack (AND logic: bin must have ALL selected categories)
- Implementation: `grid.filterByColumn('categories', 'contains', categoryName)`

#### Double-Submit Prevention

All action buttons (mass edit confirm, manage bin save, create bin, bulk create) implement double-submit prevention:
1. On click: button text changes to spinner + "Saving..." / "Processing..."
2. Button becomes `disabled` immediately (prevents rapid double-clicks)
3. On API success: button re-enables, modal closes, success toast
4. On API error: button re-enables with original text, error toast, modal stays open
5. Additional guard: each action handler checks a `_isSubmitting` flag before making API call

Pattern:
```javascript
if (this._isSubmitting) return;
this._isSubmitting = true;
$btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm"></span> Saving...');
fetch(url, opts)
    .then(handleSuccess)
    .catch(handleError)
    .finally(function() {
        this._isSubmitting = false;
        $btn.prop('disabled', false).html(originalText);
    }.bind(this));
```

#### Audit Tab Disabled Behavior

In the manage-bin modal, the "Audit" tab shows audit history. When audit tracking is inactive for the store (no audit data exists or store doesn't use auditing):
- Audit tab header shows with a subtle "(Inactive)" label
- Tab content displays: "Audit tracking is not active for this store."
- No API call is made for audit data
- The tab is still clickable (not disabled) to avoid confusion — user sees the explanatory message
- If the store later enables auditing, the tab automatically shows data on next modal open

#### CSV Export Flow (Feature 10)

Export button in the grid toolbar triggers a client-side CSV download:
1. User clicks "Export CSV" button (icon: download icon)
2. If filters/search are active, a confirmation dialog appears: "Export [X] filtered bins or all [Y] bins?"
   - "Export Filtered" → exports current view
   - "Export All" → exports full dataset
3. CSV is generated client-side from the grid's current dataSource
4. Columns: Bin Name, Location, On-Site (Yes/No), Main Category, Other Categories (semicolon-separated), Age (days), Last Action
5. File downloads as `backstock-{typeNum}-{YYYY-MM-DD}.csv`
6. UTF-8 BOM prefix for Excel compatibility
7. Analytics event tracked: `workbook_backstock_export`

#### Feature 11 (Ably Real-Time Sync) Scope

Feature 11 covers real-time sync of bin mutations ONLY — not locations, categories, or store config changes. Specifically:
- **In scope**: bin CRUD, mass edits, hide/unhide — events listed in Integration Points
- **Out of scope for Ably**: Location create/edit/delete, category create/edit/delete, store config changes
- Rationale: Location and category changes are infrequent admin operations. When they occur, the next `onViewActivated()` reload will pick them up.

## Deployment View

### Single Application Deployment

- **Environment**: PHP 8.x on Apache/Nginx, served via ngrok to dev2.buyerkiosk.com
- **Configuration**: No new environment variables. Uses existing `ABLY_KEY`, database connections.
- **Dependencies**:
  - Ably REST PHP library (already loaded)
  - Syncfusion EJ2 (already loaded via CDN in workspace-head.html)
  - Redis (already available for throttling)
- **Performance**:
  - Grid load target: <2 seconds for 500 bins
  - Search debounce: 300ms
  - Ably sync latency: <2 seconds (95th percentile)
- **Feature Flag Rollout**:
  - Migration adds column with DEFAULT 0 (all stores OFF)
  - Store owners enable via admin settings toggle
  - No batch enablement needed

### Deployment Sequence
1. Run migration (`php userfrosting/conductor run`) — adds column
2. Deploy PHP changes (Store.php, controllers, BackstockAbly.php, routes)
3. Deploy template changes (workspace.html, sidebar-nav.html, modals)
4. Deploy JS modules (backstock/ directory)
5. Build CSS (`php userfrosting/conductor build-css --minify`)
6. Feature is gated by flag — no immediate user impact until flag enabled

**Rollback**: Set `workbookBackstockEnabled = 0` for affected stores. JS modules loaded conditionally, so disabling flag hides everything.

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used
- pattern: SPA View Pattern (workspace.html + spa-navigation.js)
  relevance: CRITICAL
  why: "Must follow exact view registration and switching pattern"

- pattern: Feature Flag Pattern (Store::getFeatureFlag)
  relevance: HIGH
  why: "Must follow TINYINT column + flagMap convention"

- pattern: Ably Publish Pattern (WorkbookAbly.php)
  relevance: HIGH
  why: "Server-side event publishing with throttle and error isolation"

- pattern: Ably Subscribe Pattern (ably-sync.js)
  relevance: HIGH
  why: "Client-side event consumption with dedup and connection management"

- pattern: Syncfusion Grid Pattern (backstock/js/main.js)
  relevance: HIGH
  why: "Column definitions, selection persistence, custom templates"

- pattern: Mass Edit Pattern (backstock/js/massEditModals.js)
  relevance: HIGH
  why: "Modal lifecycle, component destruction, partial failure handling"

# New patterns (should be documented)
- pattern: API-Loaded SPA View
  relevance: MEDIUM
  why: "First workbook view to use API-loaded data instead of server-rendered"
```

### System-Wide Patterns

- **Security**: Session-based auth + `checkStoreGroup()` on every API call. Feature flag gates UI visibility. No new permissions needed.
- **Error Handling**: Non-blocking Ably publishes (try/catch, log errors). API errors return JSON with `success: false` and `message`. JS shows toast notifications for all outcomes.
- **Performance**: Syncfusion Grid handles 500+ rows with pagination (50/page). API responses should be <500ms. Ably events throttled at 40/sec per channel.
- **Logging**: PHP `error_log()` for Ably failures. JS `console.log/error` for debug. Analytics events for adoption tracking (see PRD tracking table).

### Implementation Patterns

#### Code Patterns and Conventions

- **PHP**: PSR-4 autoloaded classes under `BuyerKiosk\Workbook\` namespace
- **JS**: Namespace objects (not ES modules, no import/export) — `BackstockManager`, `BackstockGrid`, etc.
- **CSS**: Bootstrap 5.3.3 utilities + `tokens.css` custom properties
- **HTML**: Twig templates with `{% include %}` for partials, `{% if feature_flag %}` for gating
- **AJAX**: `fetch()` API for new code (admin uses `$.ajax()` but workbook prefers fetch)
- **Slim 2 POST data**: Send as `{ json: JSON.stringify(payload) }` form data (Slim 2 convention)

#### State Management Patterns

- **Grid state**: Syncfusion Grid manages its own data source internally
- **Selection state**: Grid `persistSelection: true` maintains selections across pages
- **View state**: `BackstockManager.initialized` boolean prevents double-init
- **Ably state**: `BackstockAblySync.connected` boolean tracks connection
- **Modal state**: Store bin IDs on modal DOM via `$modal.data('binIds', ids)`
- **Syncfusion component refs**: Track in object map (`_sfComponents`) for proper destroy

#### Performance Characteristics

- **Lazy loading**: Bins fetched only when backstock view activates (not on workspace load)
- **Pagination**: 50 rows/page default, configurable 25-200
- **Search debounce**: 300ms delay before executing search
- **Ably throttle**: 40 messages/sec per channel (server-side)
- **Ably payload threshold**: Full bin data for <=50 bins, IDs-only for >50 (client refetches, aligns with PRD chunking limit)
- **Grid refresh on re-entry**: Full API reload when navigating back to view (Business Rule 7)

#### Component Structure Pattern

```pseudocode
COMPONENT: BackstockManager
  INITIALIZE:
    Check typeNum from meta tag
    Check if already initialized
    Fetch bins via API

  ON_VIEW_ACTIVATED:
    IF not initialized: full init with API load
    ELSE: reload bins via API, reconnect Ably

  ON_VIEW_DEACTIVATED:
    Disconnect Ably (save resources)
    Keep grid initialized (avoid re-render cost)

  ON_ABLY_EVENT:
    Route to appropriate handler (create, update, delete, mass)
    Update grid in-place or refetch
    Show stale-data warning if manage modal is open for affected bin
```

#### Error Handling Pattern

```pseudocode
FUNCTION: handleApiError(response, context)
  IF response.status === 403:
    Show "Access denied" toast
    IF context === 'initial_load': redirect to workbook dashboard
  ELSE IF response.status === 404:
    Show "Not found" toast
    IF context === 'manage_bin': close modal
  ELSE IF response.status >= 500:
    Show "Server error. Please try again." toast
    Log to console with context
  ELSE:
    Parse response JSON for error message
    Show message in toast

FUNCTION: handleAblyError(error)
  Log error to console
  Update connection indicator to "offline"
  Do NOT block user from continuing to use the view
  Attempt reconnect automatically (Ably client handles this)
```

#### Test Pattern

```pseudocode
TEST_SCENARIO: "Bins load when backstock view activates"
  SETUP: Feature flag ON, user has store group access, bins exist
  EXECUTE: Navigate to /:typeNum/workbook/backstock
  VERIFY:
    - API call made to GET /api/:typeNum/backstock/bins/
    - Syncfusion Grid renders with correct row count
    - Summary stats display correct totals
    - Sidebar nav shows "Backstock" as active

TEST_SCENARIO: "Mass edit updates grid and publishes Ably event"
  SETUP: 3 bins selected, Ably connected
  EXECUTE: Click "Change Location" → select location → confirm
  VERIFY:
    - POST to /api/:typeNum/backstock/mass/change-location/
    - Grid rows updated with new location
    - Selection cleared
    - Success toast shown
    - Ably event published on channel

TEST_SCENARIO: "Feature flag OFF hides view"
  SETUP: Feature flag OFF for store
  EXECUTE: Load workbook
  VERIFY:
    - Sidebar shows disabled "Backstock" nav item
    - Direct URL redirects to workbook dashboard
    - No backstock JS modules initialize
```

### Integration Points

- **Connection Points**:
  - Sidebar nav (conditional include)
  - SPA navigation (`onViewActivated` handler)
  - Ably channel (subscribe to `backstock:*` events)
  - Existing API endpoints (all `/api/:typeNum/backstock/*`)
  - Store config page (toggle UI)

- **Data Flow**:
  - IN: Bins data via API, Ably events from other users, feature flag from Store object
  - OUT: API mutations (create, update, delete, mass edit), Ably publish after mutations

- **Events**:
  - `viewChanged` (consumed from SPA navigation)
  - `backstock:bin:*` (Ably events published and consumed)

## Architecture Decisions

- [x] ADR-1 **API-Loaded Data (Not Server-Rendered)**: Fetch bins via API when view activates
  - Rationale: SPA pattern means workspace loads data for all views upfront. Loading 500+ bins on every workspace page load (even when user never visits backstock) wastes resources.
  - Trade-offs: Slightly slower first render (API call vs pre-loaded data). Acceptable because grid renders in <2s.
  - User confirmed: Yes

- [x] ADR-2 **External JS Modules (Not Inline Twig)**: JS files in `/js/workspace/modules/backstock/`
  - Rationale: Follows workbook convention (chat-panel.js, schedule-ably-sync.js). Better browser caching. Cleaner separation of concerns.
  - Trade-offs: Can't use Twig variables directly in JS (use meta tags or data attributes instead). Minor inconvenience.
  - User confirmed: Yes

- [x] ADR-3 **Separate BackstockAblySync Class**: Dedicated class rather than extending WorkbookAblySync
  - Rationale: WorkbookAblySync already handles 6+ event types. Backstock events are distinct and self-contained. Follows pattern of schedule-ably-sync.js and chat-ably-sync.js.
  - Trade-offs: Separate Ably subscription on same channel (but Ably handles multiple subscriptions efficiently). Slightly more code.
  - User confirmed: Yes

- [x] ADR-4 **Reuse Existing API Endpoints**: No new CRUD endpoints
  - Rationale: Existing endpoints work with workbook auth (verified: `checkStoreGroup()` only, no `uri_backstock` check). Less code, less maintenance, single source of truth.
  - Trade-offs: Ably publishing must be added to existing controllers (light modification). API payloads/responses designed for admin may have unnecessary fields.
  - User confirmed: Yes (PRD decision)

- [x] ADR-5 **Feature Flag Default OFF**: New stores and existing stores start with flag disabled
  - Rationale: Opt-in model lets store owners choose. No surprise changes for existing users.
  - Trade-offs: Requires manual enablement. Acceptable since admin toggle provides easy self-service.
  - User confirmed: Yes (PRD decision)

## Quality Requirements

| Requirement | Target | Test Approach |
|------------|--------|---------------|
| Grid initial load | <2 seconds for 500 bins | Load test with seeded bins; measure API + render time |
| Search responsiveness | <300ms after debounce | Manual testing with various query lengths |
| Ably sync latency | <2 seconds (95th percentile) | Measure time from publish to grid update across 2 browser windows |
| Mass edit success rate | >99% for valid operations | Unit test mass edit endpoints with edge cases |
| Feature flag isolation | Zero backstock resources when OFF | Verify no API calls, no JS init when flag disabled |
| Grid pagination | Smooth with 500+ rows | Test with pc00 store (479 bins) |
| Cross-browser support | Chrome, Safari (desktop + tablet) | Manual testing on supported browsers |
| Error feedback | Toast within 500ms of error | Test all error paths (403, 500, network) |
| Selection persistence | Maintain across page changes | Select bins, change page, verify selection retained |
| Responsive layout | Usable on 768px+ width | Test on tablet viewport |

## Risks and Technical Debt

### Known Technical Issues

- Admin backstock API routes have **inconsistent auth** — some check `checkStoreGroup()`, some check API key, some have no checks. This is a pre-existing issue. This spec does not fix it but the SDD notes it as future hardening.
- The admin backstock JS uses Twig inline includes, making it impossible to directly reuse the code. The workbook version must be a clean rewrite of the JS modules.
- `bsBins.mainCategory` is INT(11) storing POS subcategory code IDs. After `makeBinReadable()`, it becomes an array. The API response already handles this transformation, so the workbook JS receives clean data.

### Technical Debt

- **JS code duplication**: The workbook backstock JS will mirror much of the admin backstock JS logic. Long-term, these should be unified into shared modules. For now, duplication is acceptable because the workbook version uses a different loading pattern (API-loaded vs server-rendered) and different module structure (external files vs inline Twig).
- **Ably publish injection**: Adding Ably publishes to existing API controllers modifies files in the "Must Not Touch" boundary. However, the modification is minimal (try/catch block at end of actions) and follows the established pattern from `TasksApiController`. This is the pragmatic choice vs. creating duplicate endpoint routes.

### Implementation Gotchas

- **Syncfusion Grid destroy/recreate**: If user navigates away and back, the grid must be properly destroyed before re-initialization. Use `BackstockManager.initialized` flag to prevent double-init.
- **Syncfusion components in modals**: MUST initialize components ONLY when modal is visible (`shown.bs.modal` event). Hidden containers cause `null` errors. Always call `.destroy()` on `hidden.bs.modal`.
- **PDO named parameter reuse**: Cannot use `:param` twice in a query. Use `:param1`, `:param2` with same value bound to both.
- **Slim 2 POST body**: Send data as form field `json=JSON.stringify(payload)`, NOT as raw JSON body. Slim 2 convention.
- **catID vs id field**: API returns `catID` for sub-categories in some responses, `id` in others. JS must handle both: `cat.catID || cat.id`.
- **Ably self-echo**: Must include `instanceId` in every Ably payload and filter out own messages in the subscriber to prevent feedback loops.
- **makeBinReadable transform**: The API response for `GET /bins/` returns bins already processed by `makeBinReadable()`. The `mainCategory` field is an object `{id, name, color}`, not a raw int. JS column templates must handle this object structure.

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Feature Flag Gate**
```gherkin
Given: Store has workbookBackstockEnabled = 0
And: User loads the workbook
When: User views the sidebar
Then: "Backstock" nav item appears disabled with tooltip "Contact your admin to enable"
And: No backstock API calls are made
And: No backstock JS modules initialize
```

**Scenario 2: Initial Bin Load**
```gherkin
Given: Store has workbookBackstockEnabled = 1
And: Store has 100 active bins
When: User clicks "Backstock" in sidebar
Then: URL updates to /:typeNum/workbook/backstock
And: API call to GET /api/:typeNum/backstock/bins/ completes in <2s
And: Syncfusion Grid displays 50 bins (page 1 of 2)
And: Summary stats show Total: 100
```

**Scenario 3: Mass Edit with Partial Failure**
```gherkin
Given: User has 5 bins selected
And: 2 of those bins have been deleted by another user
When: User clicks "Empty Bins" and confirms
Then: API returns partial success (3 updated, 2 failed)
And: Warning toast shows "3 of 5 bins emptied. 2 failed."
And: 2 failed bins remain selected
And: 3 successful bins are deselected
```

**Scenario 4: Real-Time Sync**
```gherkin
Given: Employee A and Employee B both viewing backstock for store pc00
When: Employee A empties Bin #5
Then: Employee A sees Bin #5 updated in grid immediately
And: Ably event published to backstock:bin:updated on pc00 channel
And: Employee B receives event within 2 seconds
And: Employee B's grid updates Bin #5 row in-place without refresh
```

**Scenario 5: Manage Bin Stale Data Warning**
```gherkin
Given: Employee A has Bin #5 manage modal open
When: Employee B modifies Bin #5 (Ably event arrives)
Then: Employee A sees warning banner in manage modal: "This bin was modified by another user"
And: Employee A can still save (last write wins) or close and reopen
```

**Scenario 6: Direct URL with Flag OFF**
```gherkin
Given: Store has workbookBackstockEnabled = 0
When: User navigates directly to /:typeNum/workbook/backstock
Then: User is redirected to /:typeNum/workbook/
And: No error is shown (graceful redirect)
```

**Scenario 7: Network Loss During Use**
```gherkin
Given: User is actively using backstock view
When: Network connection is lost
Then: Ably connection indicator shows "offline" (subtle dot)
And: User can still browse cached grid data
And: Mutation attempts show "Network error. Please check your connection." toast
When: Network is restored
Then: Ably reconnects automatically
And: Grid reloads fresh data
And: Connection indicator clears
```

**Scenario 8: CSV Export Respects Filters**
```gherkin
Given: User has 100 bins, filtered to 25 by category "Jewelry"
When: User clicks "Export CSV"
Then: Confirmation dialog shows "Export 25 filtered bins or all 100 bins?"
When: User clicks "Export Filtered"
Then: CSV file downloads with 25 rows
And: Filename is "backstock-{typeNum}-{date}.csv"
And: Analytics event workbook_backstock_export tracked with rowCount: 25
```

**Scenario 9: Zero Bins Empty State**
```gherkin
Given: Store has workbookBackstockEnabled = 1
And: Store has 0 backstock bins
When: User navigates to backstock view
Then: Empty state shows "No Backstock Bins Yet"
And: "Create Bin" and "Bulk Create" buttons are visible
And: Summary stats show Total: 0, On-Site: 0, Off-Site: 0
When: User clicks "Create Bin"
Then: Add bin modal opens
```

**Scenario 10: Hide Empty Toggle**
```gherkin
Given: Store has 50 bins, 10 of which have itemCount = 0
When: User toggles "Hide Empty Bins" ON
Then: Grid filters to show 40 bins
And: Summary stats still show Total: 50
When: User toggles "Hide Empty Bins" OFF
Then: Grid shows all 50 bins again
```

**Scenario 11: Mid-Session Flag Disable**
```gherkin
Given: User is viewing backstock in workbook
When: Admin disables workbookBackstockEnabled for the store
And: User navigates away then back to backstock
Then: User is redirected to workbook dashboard
And: Toast shows "Backstock has been disabled for this store"
And: Sidebar nav item shows disabled state
```

**Scenario 12: Double-Submit Prevention**
```gherkin
Given: User has 3 bins selected
When: User clicks "Empty Bins" confirm button
Then: Button shows spinner and becomes disabled
When: User rapidly clicks the button again
Then: Only one API request is made
And: Button re-enables after response
```

**Scenario 13: Audit Tab Inactive**
```gherkin
Given: Store does not use audit tracking
When: User opens manage-bin modal and clicks "Audit" tab
Then: Tab content shows "Audit tracking is not active for this store."
And: No API call for audit data is made
```

### Test Coverage Requirements

- **Business Logic**: Feature flag gating (ON/OFF/direct URL/mid-session toggle), bin CRUD flows, mass edit all 9 action types, search (single + comma-separated + empty), filter toggle states (Hide Empty ON/OFF, Show Hidden ON/OFF, combined), CSV export (filtered vs all), category badge click-to-filter
- **User Interface**: View switching (sidebar click + direct URL), grid rendering, modal open/close, toast notifications, responsive layout (tablet), disabled nav item state, zero bins empty state, double-submit prevention, audit tab inactive state, Ably connection indicator
- **Integration Points**: API fetch + error handling, Ably publish + subscribe + reconnect, feature flag save + cache invalidation, CSV export generation + download
- **Edge Cases**: 0 bins, 500+ bins, concurrent edits, partial mass edit failure, Ably disconnect, flag toggled mid-session, audit tracking inactive, all bins filtered out (vs truly empty), category filter stacking
- **Performance**: Grid load <2s, search <300ms, Ably latency <2s, CSV export for 500+ bins
- **Security**: Store group validation, feature flag enforcement, no cross-store data leakage

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Bin | A physical container in backstock storage that holds inventory items | Primary entity managed in the backstock grid |
| Backstock | Inventory stored in back-of-house locations, not on the sales floor | The feature domain this spec covers |
| typeNum | Store identifier pattern (e.g., "pc00", "ou00") | Used for store scoping, channel naming, API routing |
| Feature Flag | Per-store boolean toggle controlling feature visibility | `workbookBackstockEnabled` column on stores table |
| Mass Edit | Bulk operation applied to multiple selected bins simultaneously | 9 supported actions via floating action bar |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| SPA View | A `<main>` element in workspace.html that is shown/hidden via CSS class toggle | How the backstock view is rendered without page reload |
| viewChanged | Custom DOM event dispatched by spa-navigation.js when views switch | Trigger for BackstockManager initialization |
| persistSelection | Syncfusion Grid feature that maintains checkbox selections across pagination | Enables mass edit across pages |
| BackstockAbly | PHP class that publishes real-time events after bin mutations | Server-side Ably integration |
| BackstockAblySync | JS class that subscribes to and processes real-time backstock events | Client-side Ably integration |
| instanceId | Unique identifier per browser tab, included in Ably payloads | Prevents processing own messages (self-echo dedup) |

### API/Interface Terms

| Term | Definition | Context |
|------|------------|---------|
| save-all | Unified endpoint that saves all manage-modal changes in one call | `POST /api/:typeNum/backstock/bin/:id/save-all/` |
| makeBinReadable | PHP transform that enriches raw bin data with category names and computed fields | API responses include this transformation |
| checkStoreGroup | Auth function verifying user has access to a specific store | Called on every API request |
