# Solution Design Document

## Validation Checklist

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

---

## Constraints

CON-1 **Technology Stack**: PHP 8.x backend (Slim 2.6.2), Twig 1.44.8 templates, vanilla JavaScript with Bootstrap 5.3.3 CSS framework. No React/Vue/SPA frameworks.

CON-2 **Existing Infrastructure**: Must leverage existing `TimePunchController`, `WhenIWorkSchedule`, `WorkbookAbly` classes. Cannot modify WhenIWork API integration patterns.

CON-3 **Multi-Store Architecture**: All data access through store-specific databases via `dbConnectByName($store->getDbName())`. Panel must respect `typeNum` isolation.

CON-4 **PIN Security**: PIN stored as plain text in `clockPin` column (existing pattern). Manager override requires permission check against user role.

CON-5 **Real-Time**: Must use existing Ably infrastructure with channel naming pattern `{typeNum}` and event prefix `workbook:timepunch:*`.

CON-6 **Layout**: Right sidebar must not overlap KPI bar (fixed 50px bottom). Must respect existing CSS variables (`--header-height`, `--kpi-bar-height`, `--sidebar-width`).

CON-7 **Browser Support**: Modern browsers (Chrome, Safari, Firefox, Edge). Tablet-sized screens minimum (1024px+).

CON-8 **Kiosk Authentication Model**: Workbook is a shared kiosk - all employees can see the panel. PINs are the ONLY per-person authentication for clock actions. Session user only controls store access, not individual permissions.

## Implementation Context

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

### Required Context Sources

- ICO-1 **Architecture & Patterns**
```yaml
# Internal documentation and patterns
- doc: docs/patterns/architecture-overview.md
  relevance: HIGH
  why: "Defines multi-store architecture, namespace structure, entry points"

- doc: docs/patterns/controller-patterns.md
  relevance: HIGH
  why: "Controller patterns for API endpoints, permission checks"

- doc: docs/patterns/namespace-structure.md
  relevance: MEDIUM
  why: "BuyerKiosk namespace hierarchy for new classes"
```

- ICO-2 **WhenIWork Integration**
```yaml
# Source code files that must be understood
- file: userfrosting/src/BuyerKiosk/Workbook/WhenIWorkSchedule.php
  relevance: CRITICAL
  why: "Schedule provider - fetches shifts, maps WiW users to local employees"

- file: userfrosting/src/BuyerKiosk/Workbook/Controllers/TimePunchController.php
  relevance: CRITICAL
  why: "Existing clock in/out/break APIs with PIN verification"

- file: userfrosting/routes/workbook/timepunch.php
  relevance: HIGH
  why: "Route patterns for time punch endpoints"

- file: userfrosting/lib/wheniwork-api/src/Wheniwork.php
  relevance: MEDIUM
  why: "WhenIWork API client patterns (REST, W-Token auth)"
```

- ICO-3 **Real-Time Infrastructure**
```yaml
- file: userfrosting/src/BuyerKiosk/Workbook/WorkbookAbly.php
  relevance: CRITICAL
  why: "Ably publisher with existing timepunch events"

- file: public_html/js/workspace/modules/workbook/ably-sync.js
  relevance: HIGH
  why: "JS subscriber pattern for workbook events"
```

- ICO-4 **Workspace Layout**
```yaml
- file: userfrosting/templates/themes/default/workspace/workspace.html
  relevance: HIGH
  why: "Main workspace container structure"

- file: userfrosting/templates/themes/default/workspace/layouts/workspace-foot.html
  relevance: HIGH
  why: "Footer with modals, KPI bar, initialization scripts"

- file: public_html/css/workspace/workspace.css
  relevance: HIGH
  why: "CSS variables, sidebar patterns, flex layout"

- file: public_html/js/workspace/modules/chat/chat-overlay.js
  relevance: MEDIUM
  why: "Reference for floating panel patterns"
```

- ICO-5 **Employee & PIN System**
```yaml
- file: userfrosting/src/BuyerKiosk/Employee/Employee.php
  relevance: HIGH
  why: "Employee model with clockPin field"

- file: userfrosting/src/BuyerKiosk/Employee/EmployeeManager.php
  relevance: MEDIUM
  why: "Employee CRUD operations"

- file: public_html/js/workspace/modules/workbook/time-punch.js
  relevance: HIGH
  why: "Existing PIN entry UI patterns"
```

### Implementation Boundaries

- **Must Preserve**:
  - Existing `TimePunchController` API contracts (endpoints, request/response formats)
  - `WorkbookAbly` event naming convention (`workbook:timepunch:*`)
  - PIN verification logic in time punch endpoints
  - WhenIWork API client patterns (`\Wheniwork` class)
  - Store-specific database isolation via `dbConnectByName()`

- **Can Modify**:
  - Add new methods to `TimePunchController` for schedule panel data
  - Extend `WorkbookAbly` with new event types if needed
  - Add new CSS variables to `workspace.css` for right sidebar
  - Add new partials to workspace templates

- **Must Not Touch**:
  - `\Wheniwork` API client library (external dependency)
  - Employee sync/provider system (`WhenIWorkProvider`)
  - Core user authentication system (`AccountController`)
  - Left sidebar navigation structure
  - KPI bar component

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    subgraph BuyerKiosk
        ESP[Employee Schedule Panel]
        TPC[TimePunchController]
        WAS[WhenIWorkSchedule]
        WBA[WorkbookAbly]
    end

    Manager[Store Manager] --> ESP
    ShiftLead[Shift Lead] --> ESP
    Employee[Floor Employee] --> ESP

    ESP --> TPC
    TPC --> WIW[WhenIWork API]
    TPC --> WBA
    WAS --> WIW
    WBA --> Ably[Ably Realtime]
    Ably --> ESP

    TPC --> StoreDB[(Store DB)]
    WAS --> Redis[(Redis Cache)]
```

#### Interface Specifications

```yaml
# Inbound Interfaces (what calls this system)
inbound:
  - name: "Workspace Web Interface"
    type: HTTPS
    format: REST JSON
    authentication: Session-based (UserFrosting)
    data_flow: "Panel renders, clock actions, schedule views"

  - name: "Ably WebSocket"
    type: WebSocket
    format: JSON events
    authentication: API Key (window.ABLY_KEY)
    data_flow: "Real-time clock status updates"

# Outbound Interfaces (what this system calls)
outbound:
  - name: "WhenIWork API"
    type: HTTPS
    format: REST JSON
    authentication: W-Token header (OAuth token)
    base_url: "https://api.wheniwork.com/2"
    endpoints_used:
      - "GET /shifts" (schedule retrieval)
      - "GET /punch/state" (clock status)
      - "POST /times/clockin" (clock in)
      - "POST /times/clockout" (clock out)
      - "POST /v3/shift-breaks" (start break)
      - "PUT /v3/shift-breaks/{id}" (end break)
    criticality: HIGH

  - name: "Ably Publisher"
    type: HTTPS REST
    format: JSON
    authentication: ABLY_KEY from $_ENV
    channel: "{typeNum}"
    events_published:
      - "workbook:timepunch:clockin"
      - "workbook:timepunch:clockout"
      - "workbook:timepunch:breakstart"
      - "workbook:timepunch:breakend"
    criticality: MEDIUM

# Data Interfaces
data:
  - name: "Store Database"
    type: MySQL
    connection: "dbConnectByName($store->getDbName())"
    tables_used:
      - "employees" (employeeID, clockPin, externalId, photoUrl, position)
      - "workbook_schedule_cache" (cached schedule data)
      - "workbook_punch_log" (audit trail)
    data_flow: "Employee data, PIN verification, schedule cache"

  - name: "Redis Cache"
    type: Redis
    connection: "RedisManager"
    keys_used:
      - "{typeNum}_schedule_{Y-m-d}" (5-min TTL)
    data_flow: "Schedule caching for performance"
```

### Cross-Component Boundaries

- **API Contracts**:
  - Existing `TimePunchController` endpoints are stable contracts (mobile app may use them)
  - New panel-specific endpoints can be added without breaking existing consumers
  - Ably event payloads must include `action`, `category`, `timestamp`, `source` fields

- **Shared Resources**:
  - `employees` table (shared with Employee Management, Mobile App)
  - Redis cache keys (namespaced by typeNum to prevent collision)
  - Ably channel (shared with Queue, Workbook, Chat modules)

### Project Commands

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

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

# CSS Build Commands
php userfrosting/conductor build-css           # Development build
php userfrosting/conductor build-css --minify  # Production build (generates version hash)
php userfrosting/conductor build-css --watch   # Watch mode for development

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

# Dependencies
cd userfrosting && composer install            # Install PHP dependencies

# Deployment
./deploy.sh                                     # Test + deploy

# Development Server
# Access via https://dev2.buyerkiosk.com (Chrome DevTools MCP available)
```

## Solution Strategy

- **Architecture Pattern**: Layered MVC extension - Add new UI component (right sidebar) that consumes existing backend services through a new facade endpoint that aggregates schedule + clock status data.

- **Integration Approach**:
  - **Backend**: Extend `TimePunchController` with a new `getSchedulePanelData()` method that combines `WhenIWorkSchedule::getScheduleForDate()` with batch clock status fetching
  - **Frontend**: New `SchedulePanel` JavaScript class that renders the sidebar and subscribes to existing Ably events
  - **Real-Time**: Leverage existing `WorkbookAbly` events - no new event types needed

- **Justification**:
  - Reuses 90%+ of existing infrastructure (WhenIWork client, Ably, PIN verification)
  - Follows established patterns (workspace sidebar CSS, Ably sync JS modules)
  - Minimal backend changes - mostly new UI components
  - Backward compatible - doesn't break existing time punch endpoints

- **Key Decisions**:
  1. **Single API call for panel data** - Combines schedule + status to reduce round-trips
  2. **Client-side state management** - Panel maintains employee states in JS, updated via Ably
  3. **Extend existing controller** - Add to `TimePunchController` rather than new controller (cohesion)
  4. **CSS variables for layout** - Use existing workspace CSS pattern for consistency

## Building Block View

### Components

```mermaid
graph TB
    subgraph Frontend["Frontend (Browser)"]
        SP[SchedulePanel.js]
        QAM[QuickActionModal.js]
        SM[ScheduleModal.js]
        AS[schedule-ably-sync.js]
    end

    subgraph Templates["Twig Templates"]
        SPT[schedule-panel.html]
        QAMT[quick-action-modal.html]
        SMT[schedule-modal.html]
    end

    subgraph Backend["Backend (PHP)"]
        TPC[TimePunchController]
        WSS[WhenIWorkSchedule]
        WBA[WorkbookAbly]
    end

    subgraph External["External Services"]
        WIW[WhenIWork API]
        ABLY[Ably Realtime]
    end

    SP --> TPC
    SP --> AS
    QAM --> TPC
    AS --> ABLY
    TPC --> WSS
    TPC --> WBA
    WSS --> WIW
    WBA --> ABLY
    SPT --> SP
    QAMT --> QAM
    SMT --> SM
```

### Directory Map

**Backend (PHP)**
```
userfrosting/
├── src/BuyerKiosk/Workbook/
│   └── Controllers/
│       └── TimePunchController.php         # MODIFY: Add getSchedulePanelData()
├── routes/workbook/
│   └── timepunch.php                       # MODIFY: Add route for panel data
└── templates/themes/default/workspace/
    ├── layouts/
    │   └── workspace-foot.html             # MODIFY: Include panel template
    └── partials/
        └── schedule-panel/                 # NEW: Panel templates directory
            ├── schedule-panel.html         # NEW: Main panel sidebar
            ├── employee-avatar.html        # NEW: Avatar component partial
            ├── quick-action-modal.html     # NEW: Clock action modal
            └── schedule-modal.html         # NEW: Full schedule view modal
```

**Frontend (JavaScript)**
```
public_html/js/workspace/modules/
└── schedule-panel/                         # NEW: Module directory
    ├── SchedulePanel.js                    # NEW: Main panel controller
    ├── QuickActionModal.js                 # NEW: Clock action modal controller
    ├── ScheduleModal.js                    # NEW: Schedule view modal controller
    └── schedule-ably-sync.js               # NEW: Ably event handler for panel
```

**Styles (CSS)**
```
public_html/css/workspace/
└── schedule-panel.css                      # NEW: Panel-specific styles
```

### Interface Specifications

#### Existing Interfaces Used

```yaml
interfaces:
  - name: "TimePunch API"
    file: userfrosting/routes/workbook/timepunch.php
    relevance: CRITICAL
    endpoints:
      - "GET /api/:typeNum/workbook/timepunch/employees/"
      - "POST /api/:typeNum/workbook/timepunch/state/"
      - "POST /api/:typeNum/workbook/timepunch/clockin/"
      - "POST /api/:typeNum/workbook/timepunch/clockout/"
      - "POST /api/:typeNum/workbook/timepunch/break/start/"
      - "POST /api/:typeNum/workbook/timepunch/break/end/"
    why: "Reuse these endpoints for clock actions from panel"

  - name: "WorkbookAbly Events"
    file: userfrosting/src/BuyerKiosk/Workbook/WorkbookAbly.php
    relevance: CRITICAL
    events:
      - "workbook:timepunch:clockin"
      - "workbook:timepunch:clockout"
      - "workbook:timepunch:breakstart"
      - "workbook:timepunch:breakend"
    why: "Subscribe to these for real-time panel updates"
```

#### Data Storage Changes

**Minor schema addition** to support manager override audit trail:

```yaml
# Existing tables used
Table: employees
  Columns used:
    - employeeID (int, PK)
    - employeeFirstName, employeeLastName (varchar)
    - photoUrl (varchar, nullable)
    - position (varchar, nullable)
    - clockPin (varchar(10), nullable) - PIN verification
    - externalId (varchar) - WhenIWork user ID mapping
    - source (varchar) - 'wheniwork' filter
    - role (int) - Employee role/permission level (for manager check)
    - active (tinyint)

Table: workbook_schedule_cache
  Purpose: Schedule data caching (already used by WhenIWorkSchedule)

Table: workbook_punch_log (MODIFY)
  Purpose: Audit trail for clock actions
  ADD COLUMN: overrideByEmployeeId INT NULL
    - Stores the manager's employeeID when override is used
    - NULL for normal PIN-authenticated actions
  ADD COLUMN: overrideReason VARCHAR(255) NULL
    - Optional reason text for override
```

**Migration required**:
```sql
ALTER TABLE workbook_punch_log
  ADD COLUMN overrideByEmployeeId INT(10) UNSIGNED NULL
    COMMENT 'Manager employeeID if override was used',
  ADD COLUMN overrideReason VARCHAR(255) NULL
    COMMENT 'Optional reason for manager override';
```

#### Internal API Changes

```yaml
# NEW ENDPOINT: Get Schedule Panel Data
Endpoint: Schedule Panel Data
  Method: GET
  Path: /api/:typeNum/workbook/schedule-panel/
  Authentication: Session (checkStoreGroup only - no per-user permission)
  Description: "Returns today's scheduled employees with current clock status"
  Note: "Panel visible to ALL workbook users. This is a shared kiosk - PINs provide per-person auth for actions."

  Response (Success 200):
    success: true
    data:
      enabled: boolean           # WhenIWork integration enabled
      employees: array
        - id: int                # employeeID
          firstName: string
          lastName: string
          fullName: string
          photoUrl: string|null
          position: string|null
          hasPin: boolean
          shiftStart: string|null    # "9:00am" display format
          shiftEnd: string|null      # "5:00pm" display format
          shiftNotes: string|null
          status: enum               # 'scheduled'|'clocked_in'|'on_break'|'clocked_out'
          clockedInAt: string|null   # ISO datetime if clocked in
          breakStartedAt: string|null # ISO datetime if on break
      lastUpdated: string        # ISO datetime

  Response (Error 403):
    success: false
    error: "Access denied to this store"

  Response (Error 500):
    success: false
    error: "Failed to fetch schedule data: {message}"

# NEW ENDPOINT: Manager Override Clock Action
Endpoint: Manager Override
  Method: POST
  Path: /api/:typeNum/workbook/timepunch/override/
  Authentication: Session (checkStoreGroup) + Manager PIN carries permission
  Description: "Clock action using manager's PIN - manager role verified via PIN lookup"

  Request:
    employeeId: int (required)       # Target employee to clock
    action: enum (required)          # 'clockin'|'clockout'|'breakstart'|'breakend'
    managerPin: string (required)    # Manager's own employee PIN
    reason: string (optional)        # Override reason for audit log

  Flow:
    1. Verify session has store access (checkStoreGroup)
    2. Look up manager by PIN: findByPin(managerPin, typeNum)
    3. Verify manager has 'manager' role flag
    4. Execute clock action for target employee
    5. Log with overrideByEmployeeId = manager's employeeID

  Response (Success 200):
    success: true
    message: "Successfully clocked {action}"
    data:
      time: object                   # WhenIWork time record
      overrideByEmployeeId: int      # Manager's employeeID
      overrideByName: string         # Manager's name for UI feedback

  Response (Error 401):
    success: false
    error: "Invalid manager PIN"

  Response (Error 403):
    success: false
    error: "This PIN does not belong to a manager"
```

#### Application Data Models

```pseudocode
# JavaScript Models (Client-Side State)

MODEL: SchedulePanelState
  FIELDS:
    typeNum: string                    # Store identifier
    enabled: boolean                   # WhenIWork integration status
    employees: Map<int, EmployeeState> # Keyed by employeeId
    isExpanded: boolean                # Panel expanded/collapsed
    lastUpdated: Date                  # Last API refresh

  BEHAVIORS:
    loadFromAPI(): Promise<void>       # Initial data fetch
    updateEmployeeStatus(employeeId, status): void
    getEmployeesByStatus(status): EmployeeState[]
    persistPanelState(): void          # Save to localStorage

MODEL: EmployeeState
  FIELDS:
    id: int
    firstName: string
    lastName: string
    fullName: string
    photoUrl: string|null
    position: string|null
    hasPin: boolean
    shiftStart: string|null            # Display format "9:00am"
    shiftEnd: string|null
    shiftNotes: string|null
    status: 'scheduled'|'clocked_in'|'on_break'|'clocked_out'
    clockedInAt: Date|null
    breakStartedAt: Date|null

  COMPUTED:
    initials: string                   # First letters of first/last name
    sortOrder: int                     # For display ordering

# PHP Data Transfer Objects

DTO: SchedulePanelEmployee
  FIELDS:
    id: int
    firstName: string
    lastName: string
    fullName: string
    photoUrl: ?string
    position: ?string
    hasPin: bool
    shiftStart: ?string
    shiftEnd: ?string
    shiftNotes: ?string
    status: string
    clockedInAt: ?string
    breakStartedAt: ?string

  FACTORY:
    fromScheduleAndPunchState(schedule: array, punchState: object): self
```

#### Integration Points

```yaml
# Inter-Component Communication
- from: SchedulePanel.js
  to: TimePunchController (PHP)
  protocol: REST/HTTPS
  endpoints:
    - GET /api/:typeNum/workbook/schedule-panel/
    - POST /api/:typeNum/workbook/timepunch/state/
    - POST /api/:typeNum/workbook/timepunch/clockin/
    - POST /api/:typeNum/workbook/timepunch/clockout/
    - POST /api/:typeNum/workbook/timepunch/break/start/
    - POST /api/:typeNum/workbook/timepunch/break/end/
    - POST /api/:typeNum/workbook/timepunch/override/
  data_flow: "Panel requests data, sends clock actions"

- from: schedule-ably-sync.js
  to: Ably WebSocket
  protocol: WebSocket
  channel: "{typeNum}"
  events:
    - "workbook:timepunch:clockin"
    - "workbook:timepunch:clockout"
    - "workbook:timepunch:breakstart"
    - "workbook:timepunch:breakend"
  data_flow: "Real-time status updates to panel"

# External System Integration
WhenIWork API:
  protocol: HTTPS REST
  authentication: W-Token header
  base_url: https://api.wheniwork.com/2
  integration: "Fetch schedules, execute clock actions"
  critical_data:
    - shifts (schedule data)
    - punch/state (clock status)
    - times/clockin, times/clockout (clock actions)
    - v3/shift-breaks (break management)
```

### Implementation Examples

#### Example: Employee Status Sorting Logic

**Why this example**: The panel requires employees sorted by status (clocked in → scheduled → clocked out). This logic is critical for UX.

```javascript
// Employee sorting for panel display
function sortEmployeesByStatus(employees) {
    const statusOrder = {
        'clocked_in': 0,
        'on_break': 1,      // On break shown with clocked in
        'scheduled': 2,
        'clocked_out': 3
    };

    return employees.sort((a, b) => {
        // First by status
        const statusDiff = statusOrder[a.status] - statusOrder[b.status];
        if (statusDiff !== 0) return statusDiff;

        // Then by shift start time
        if (a.shiftStart && b.shiftStart) {
            return a.shiftStart.localeCompare(b.shiftStart);
        }

        // Finally by name
        return a.fullName.localeCompare(b.fullName);
    });
}
```

#### Example: Ably Event Handler for Panel Updates

**Why this example**: Demonstrates how the panel updates in real-time when clock events occur.

```javascript
// Handle Ably timepunch events
handleTimePunchMessage(action, data) {
    const { employeeId, employeeName, isClockedIn, onBreak } = data;

    // Find employee in panel state
    const employee = this.state.employees.get(employeeId);
    if (!employee) return;

    // Determine new status
    let newStatus;
    switch (action) {
        case 'workbook:timepunch:clockin':
            newStatus = 'clocked_in';
            employee.clockedInAt = new Date();
            break;
        case 'workbook:timepunch:clockout':
            newStatus = 'clocked_out';
            break;
        case 'workbook:timepunch:breakstart':
            newStatus = 'on_break';
            employee.breakStartedAt = new Date();
            break;
        case 'workbook:timepunch:breakend':
            newStatus = 'clocked_in';
            employee.breakStartedAt = null;
            break;
    }

    // Update state and re-render
    employee.status = newStatus;
    this.renderEmployeeAvatar(employee);
    this.reorderEmployees();
}
```

#### Example: Manager Override Permission Check (PIN-Based)

**Why this example**: Security-critical logic - manager permission is carried by the PIN, not the session user.

```php
/**
 * Check if PIN belongs to a manager who can perform override
 *
 * NOTE: This is PIN-based auth, not session-user auth.
 * The workbook is a shared kiosk - any employee can use it.
 * The PIN identifies WHO is performing the action.
 */
private function validateManagerOverride(string $managerPin, string $typeNum): ?array
{
    // Look up employee by PIN
    $stmt = $this->db->prepare("
        SELECT employeeID, employeeFirstName, employeeLastName, role
        FROM employees
        WHERE clockPin = :pin
        AND active = 1
        LIMIT 1
    ");
    $stmt->execute([':pin' => $managerPin]);
    $manager = $stmt->fetch(\PDO::FETCH_ASSOC);

    if (!$manager) {
        return null; // Invalid PIN
    }

    // Check if employee has manager role
    // Role levels: 1=floor, 2=shift_lead, 3=manager, 4=admin
    if ($manager['role'] < 3) {
        return null; // Not a manager
    }

    return $manager; // Return manager data for audit logging
}
```

## Runtime View

### Primary Flow: Clock In from Schedule Panel

1. Manager opens workbook → Panel loads with today's scheduled employees
2. Manager taps grey avatar (scheduled employee) → Quick action modal opens
3. Manager clicks "Clock In" → PIN entry appears
4. Manager enters employee PIN (or chooses override) → PIN validated
5. System calls WhenIWork API → Clock in recorded
6. Ably broadcasts event → All panels update in real-time
7. Avatar changes to full-color with green dot

```mermaid
sequenceDiagram
    actor Manager
    participant Panel as SchedulePanel.js
    participant Modal as QuickActionModal.js
    participant API as TimePunchController
    participant WIW as WhenIWork API
    participant Ably as WorkbookAbly

    Manager->>Panel: Tap employee avatar
    Panel->>Modal: Open with employee data
    Manager->>Modal: Click "Clock In"
    Modal->>Modal: Show PIN entry
    Manager->>Modal: Enter PIN
    Modal->>API: POST /timepunch/clockin/
    API->>API: Verify PIN
    API->>WIW: POST /times/clockin
    WIW-->>API: Success + time record
    API->>Ably: employeeClockedIn()
    Ably-->>Panel: workbook:timepunch:clockin
    API-->>Modal: Success response
    Modal->>Modal: Close
    Panel->>Panel: Update avatar status
```

### Error Handling

- **Invalid PIN** (401): Show "Invalid PIN. Please try again." Allow 3 retries, then prompt for manager override.
- **Employee not found** (404): Show "Employee not found" and close modal.
- **WhenIWork API timeout** (500): Show "Unable to connect to WhenIWork. Please try again." with retry button.
- **Employee already clocked in**: API returns available actions only, so "Clock In" button won't be shown.
- **No PIN configured**: Show "No PIN configured" message with link to employee settings.

### Panel Disabled / No Integration UX

When WhenIWork is not enabled for a store, the panel handles this gracefully:

**Decision**: Hide panel entirely (not empty state)

**Rationale**:
- Showing an empty "not configured" state wastes screen space on stores that may never use WhenIWork
- Panel is part of workspace layout - hiding it returns that space to the main content area
- Consistent with how other integrations work (features hidden when not configured)

**Implementation**:
```twig
{# In workspace-foot.html #}
{% if store.wiwEnable %}
    {% include 'workspace/partials/schedule-panel/schedule-panel.html' %}
{% endif %}
```

**API Behavior**:
- `GET /schedule-panel/` returns `{ success: true, data: { enabled: false, message: "WhenIWork not configured" } }`
- Frontend checks `enabled` flag before rendering (defense in depth, though template already handles)

**CSS**:
- When panel is hidden, main content area automatically expands (no right margin)
- CSS variable `--right-sidebar-width` only applied when panel exists

### Real-Time Update Flow

```
FLOW: Real-Time Status Update
TRIGGER: Clock action completed anywhere (panel, mobile app, WhenIWork direct)

1. TimePunchController → calls WorkbookAbly::employeeClockedIn/Out/Break()
2. WorkbookAbly → publishes to Ably channel "{typeNum}"
3. schedule-ably-sync.js → receives event via WebSocket subscription
4. SchedulePanel.handleTimePunchMessage() → updates employee status in state
5. SchedulePanel.renderEmployeeAvatar() → updates DOM (color, status dot)
6. SchedulePanel.reorderEmployees() → moves avatar to correct position
```

### Real-Time Fallback Behavior

When Ably connection is unavailable or unstable, the panel implements graceful degradation:

```
FLOW: Ably Disconnect Fallback
TRIGGER: Ably connection state changes to 'disconnected' or 'suspended'

1. schedule-ably-sync.js detects disconnect via connection.on('disconnected')
2. Panel shows visual indicator: "⚠️ Live updates paused" (subtle warning badge)
3. Panel starts polling fallback:
   - Fetch GET /schedule-panel/ every 60 seconds
   - Compare returned data with current state
   - Update any changed employee statuses
4. When Ably reconnects:
   - Stop polling interval
   - Remove warning indicator
   - Show brief "✓ Live updates restored" toast (auto-dismiss 3s)
```

**Implementation Notes**:
- Polling interval: 60 seconds (balance between freshness and API load)
- Show "Last updated: X minutes ago" timestamp in panel header
- Manual refresh button always available (calls GET /schedule-panel/)
- Polling uses same endpoint as initial load (no new endpoint needed)

## Deployment View

**No deployment changes required.** This feature uses existing infrastructure:

- **Environment**: Standard BuyerKiosk web deployment (PHP on shared hosting)
- **Configuration**: No new environment variables (uses existing `ABLY_KEY`)
- **Dependencies**: WhenIWork integration must be enabled per-store (existing config)
- **Performance**:
  - Schedule data cached in Redis (5-min TTL, existing pattern)
  - Panel makes single API call on load, updates via Ably
  - Expected response time < 500ms for schedule panel endpoint

**Rollout Strategy**:
- Panel visibility controlled by existing WhenIWork enable flag (`$store->getWiwEnable()`)
- No feature flag needed - panel only shows for stores with WhenIWork integration
- Backward compatible - no breaking changes to existing endpoints

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used in this feature
- pattern: docs/patterns/controller-patterns.md
  relevance: CRITICAL
  why: "API controller structure, permission checks, JSON response format"

- pattern: docs/patterns/architecture-overview.md
  relevance: HIGH
  why: "Multi-store database access, typeNum isolation"

# Patterns to follow from existing code
- file: userfrosting/src/BuyerKiosk/Workbook/WorkbookAbly.php
  relevance: CRITICAL
  why: "Ably event publishing pattern"

- file: public_html/js/workspace/modules/workbook/ably-sync.js
  relevance: CRITICAL
  why: "Ably subscription and event handling pattern"

- file: public_html/js/workspace/modules/chat/chat-overlay.js
  relevance: HIGH
  why: "Floating panel UI pattern"
```

### System-Wide Patterns Applied

- **Security**: PIN verification via existing `clockPin` field; manager override requires permission check
- **Error Handling**: Try-catch with JSON error responses; graceful degradation when WhenIWork unavailable
- **Performance**: Redis caching for schedule data (5-min TTL); single API call on panel load
- **Logging/Auditing**: All clock actions logged to `workbook_punch_log` table (existing pattern)

### Implementation Patterns

#### Code Patterns and Conventions

**PHP Backend Patterns:**

| Pattern | Convention | Example |
|---------|------------|---------|
| Method naming | camelCase, verb-first | `getSchedulePanelData()`, `clockIn()`, `validateManagerOverride()` |
| Class properties | Private with type hints | `private \PDO $db;`, `private \Store $store;` |
| Response format | `['success' => bool, 'data' => ...]` | `echo json_encode(['success' => true, 'data' => $employees])` |
| Error response | `['error' => 'message']` with HTTP status | `$app->halt(401, json_encode(['error' => 'Invalid PIN']))` |
| DB queries | PDO prepared statements, named params | `$stmt->execute([':employeeId' => $id])` |
| Permission checks | Early return pattern | `if (!$store) { $app->halt(403, ...); return; }` |

**JavaScript Frontend Patterns:**

| Pattern | Convention | Example |
|---------|------------|---------|
| Module structure | ES6 classes | `class SchedulePanel { constructor(options) {...} }` |
| State storage | Instance properties | `this.state = { employees: [], isExpanded: true }` |
| Event binding | addEventListener + custom events | `document.dispatchEvent(new CustomEvent('schedule:panel:updated'))` |
| DOM caching | Cache in constructor | `this.container = document.getElementById('schedule-panel')` |
| Naming | camelCase (JS), kebab-case (CSS) | `selectedEmployee`, `.schedule-panel-header` |

**Docblock Convention:**
```php
/**
 * GET /api/:typeNum/workbook/schedule-panel/
 * Returns today's scheduled employees with clock status
 *
 * @param string $typeNum Store identifier
 * @return void Outputs JSON response
 */
public function getSchedulePanelData(string $typeNum): void
```

#### State Management Patterns

**Three-Tier State Architecture:**

1. **Instance State** (in-memory, session-scoped):
   ```javascript
   this.state = {
       isExpanded: true,
       selectedEmployee: null,
       employees: new Map(),
       lastUpdated: null
   };
   ```

2. **localStorage** (user-scoped, persistent):
   ```javascript
   // Storage key pattern: {feature}_state_{typeNum}
   const storageKey = 'schedule_panel_state_' + this.typeNum;

   saveState() {
       localStorage.setItem(this.storageKey, JSON.stringify({
           version: 1,
           isExpanded: this.state.isExpanded
       }));
   }
   ```

3. **Server State** (authoritative, API-fetched):
   - Schedule data from WhenIWork via backend API
   - Clock status from real-time Ably events
   - Employee data from store database

**State Update Flow:**
```
User Action → Local State Update → API Call → Server Response → Ably Broadcast → All Clients Update
```

#### Performance Characteristics

**Caching Strategy:**

| Layer | TTL | Purpose |
|-------|-----|---------|
| Redis | 5 min | Hot path for schedule data |
| Database | Permanent | Fallback when Redis unavailable |
| Browser | Session | DOM element caching |

**API Optimization:**
- Single batch endpoint for panel data (reduces round-trips)
- Fetch `limit + 1` pattern to detect pagination without COUNT query
- Selective JOINs only when needed

**Frontend Optimization:**
- Element caching in constructor (avoid repeated queries)
- UUID cache-busting for fresh data on page reload
- Lazy loading for audio/notification elements

**Target Metrics:**
- Schedule panel API: < 500ms response
- Real-time updates: < 5s latency via Ably
- Panel render with 20 employees: < 100ms

#### Integration Patterns

**External API Communication (WhenIWork):**
```php
// Pattern: Service class wraps external API
$schedule = $this->whenIWorkSchedule->getScheduleForDate($date);

// Error handling: Log and return empty, don't throw
try {
    $shifts = $this->wiwClient->get('/shifts', $params);
} catch (Exception $e) {
    error_log("WhenIWork API error: " . $e->getMessage());
    return []; // Graceful degradation
}
```

**Real-Time Events (Ably):**
```javascript
// Event naming: {module}:{submodule}:{action}
const events = [
    'workbook:timepunch:clockin',
    'workbook:timepunch:clockout',
    'workbook:timepunch:breakstart',
    'workbook:timepunch:breakend'
];

// Subscribe pattern
this.channel.subscribe((message) => {
    if (this.messageIDs.includes(message.id)) return; // Dedup
    if (message.data.instanceId === this.instanceId) return; // Skip own
    this.handleMessage(message.name, message.data);
});
```

**Custom Event Dispatch:**
```javascript
// Inter-component communication
dispatchEvent(eventName, detail) {
    const event = new CustomEvent('schedule:panel:' + eventName, {
        detail: { ...detail, typeNum: this.typeNum },
        bubbles: true
    });
    document.dispatchEvent(event);
}
```

#### Component Structure Pattern

```javascript
// ES6 Class with multi-phase initialization
class SchedulePanel {
    constructor(options = {}) {
        // Phase 1: Store config
        this.typeNum = options.typeNum;
        this.csrfToken = options.csrfToken;

        // Phase 2: Initialize state
        this.state = {
            isExpanded: true,
            employees: new Map(),
            selectedEmployee: null
        };

        // Phase 3: Bind event handlers
        this.boundOnEmployeeClick = this.onEmployeeClick.bind(this);

        // Phase 4: Call init
        this.init();
    }

    init() {
        this.cacheElements();      // Cache DOM references
        this.bindEvents();         // Attach event listeners
        this.loadState();          // Restore from localStorage
        this.loadEmployees();      // Fetch initial data
    }

    // State container pattern
    hideAllStates() {
        ['loading', 'error', 'empty', 'content'].forEach(state => {
            this.containers[state].style.display = 'none';
        });
    }

    showState(stateName) {
        this.hideAllStates();
        this.containers[stateName].style.display = 'block';
    }
}

// Auto-initialize if element exists
document.addEventListener('DOMContentLoaded', () => {
    if (document.getElementById('schedule-panel')) {
        window.schedulePanel = new SchedulePanel({
            typeNum: document.querySelector('meta[name="typeNum"]').content
        });
    }
});
```

#### Data Processing Pattern

```php
// Controller method pattern
public function getSchedulePanelData(string $typeNum): void
{
    try {
        // 1. VALIDATE: Check store access
        $store = checkAccessAndReturnStoreObject($this->app, $typeNum, 'schedule-panel');
        if (!$store) {
            $this->app->halt(403, json_encode(['error' => 'Access denied']));
            return;
        }

        // 2. CHECK: Integration enabled
        if (!$store->getWiwEnable()) {
            echo json_encode(['success' => true, 'data' => ['enabled' => false]]);
            return;
        }

        // 3. FETCH: Get schedule data (service delegation)
        $schedule = $this->scheduleService->getTodaySchedule();

        // 4. TRANSFORM: Map to response format
        $employees = array_map(fn($e) => $this->formatEmployee($e), $schedule);

        // 5. RESPOND: Return JSON
        $this->app->response->headers->set('Content-Type', 'application/json');
        echo json_encode([
            'success' => true,
            'data' => [
                'enabled' => true,
                'employees' => $employees,
                'lastUpdated' => date('c')
            ]
        ]);

    } catch (Exception $e) {
        error_log("SchedulePanelController::getData error: " . $e->getMessage());
        $this->app->halt(500, json_encode(['error' => 'Failed to load schedule']));
    }
}
```

#### Error Handling Pattern

**HTTP Status Code Usage:**

| Code | Meaning | When to Use |
|------|---------|-------------|
| 200 | Success | Successful operations |
| 201 | Created | New resource created (rare for this feature) |
| 400 | Bad Request | Missing/invalid input |
| 401 | Unauthorized | Invalid PIN |
| 403 | Forbidden | Permission denied, not a manager |
| 404 | Not Found | Employee not found |
| 500 | Server Error | WhenIWork API failure, unexpected exceptions |

**Backend Error Pattern:**
```php
try {
    // Primary operation
    $result = $this->clockIn($employeeId);

    // Non-critical operations (don't fail request if these fail)
    try {
        $this->ably->employeeClockedIn($employeeId, $result);
    } catch (Exception $e) {
        error_log("Ably publish failed: " . $e->getMessage());
        // Don't propagate - clock action still succeeded
    }

    echo json_encode(['success' => true, 'data' => $result]);

} catch (InvalidPinException $e) {
    $this->app->halt(401, json_encode(['error' => 'Invalid PIN']));
} catch (NotFoundException $e) {
    $this->app->halt(404, json_encode(['error' => $e->getMessage()]));
} catch (Exception $e) {
    error_log("ClockIn error: " . $e->getMessage());
    $this->app->halt(500, json_encode(['error' => 'Failed to clock in']));
}
```

**Frontend Error Display:**
```javascript
// Toast notifications (primary)
if (typeof toastr !== 'undefined') {
    toastr.error('Failed to clock in. Please try again.');
}

// Inline validation (PIN entry)
showPinError(message) {
    this.pinError.textContent = message;
    this.pinError.style.display = 'block';
    this.pinInput.value = '';
    this.pinInput.focus();
}

// Logging pattern
console.error('[SchedulePanel] API error:', error);
```

#### Test Pattern

**File Naming:** `{ClassName}Test.php` in mirrored directory structure
```
src/BuyerKiosk/Workbook/Controllers/TimePunchController.php
→ tests/Unit/Workbook/Controllers/TimePunchControllerTest.php
```

**Method Naming:** `test{Action}{Condition}{Result}`
```php
public function testGetSchedulePanelDataReturnsEmployees(): void
public function testClockInWithInvalidPinReturns401(): void
public function testManagerOverrideRequiresManagerRole(): void
```

**Test Structure (AAA Pattern):**
```php
public function testClockInWithValidPinSucceeds(): void
{
    // Arrange
    $employee = EmployeeFixtures::createWithPin('1234');
    $this->insertEmployee($employee);

    // Act
    $response = $this->controller->clockIn($employee['id'], '1234');

    // Assert
    $this->assertTrue($response['success']);
    $this->assertArrayHasKey('clockedInAt', $response['data']);
}
```

**Mock Patterns:**
```php
// PDO mocking with fluent builder
$pdoMock = PdoMockBuilder::create($this)
    ->expectQuery('SELECT * FROM employees WHERE clockPin = ?')
    ->withParams(['1234'])
    ->willReturn([['employeeID' => 1, 'role' => 3]])
    ->build();

// Store fixture factory
$store = StoreMock::forStore('ou00')
    ->withIntegrations(['wheniwork'])
    ->create();
```

**Fixture Pattern:**
```php
class EmployeeScheduleFixtures
{
    public static function createScheduledEmployee(array $overrides = []): array
    {
        return array_merge([
            'employeeID' => 1,
            'employeeFirstName' => 'John',
            'employeeLastName' => 'Doe',
            'clockPin' => '1234',
            'status' => 'scheduled',
            'shiftStart' => '09:00',
            'shiftEnd' => '17:00'
        ], $overrides);
    }
}
```

#### Static Analysis (PHPStan)

**Configuration:** Level 2 analysis on `src/BuyerKiosk/`

```bash
# Run PHPStan analysis
cd userfrosting && ./vendor/bin/phpstan analyse

# Analyze specific path
cd userfrosting && ./vendor/bin/phpstan analyse src/BuyerKiosk/Workbook/

# With increased memory for large analysis
cd userfrosting && ./vendor/bin/phpstan analyse --memory-limit=2G
```

**Baseline:** Existing `phpstan-baseline.neon` contains ~3200 lines of ignored legacy errors. New code MUST NOT add to the baseline.

**Requirements for New Code:**
- Must pass PHPStan level 2 without errors
- Type hints required on all method parameters and return types
- No `@phpstan-ignore` annotations without documented justification
- Run `./test.sh --stan` before committing to verify compliance

**Common Issues to Avoid:**
```php
// ❌ Bad: Missing type hints
public function getEmployee($id)

// ✅ Good: Full type hints
public function getEmployee(int $id): ?array

// ❌ Bad: Unknown class references
$logger = new KLogger(...);

// ✅ Good: Use typed properties with known classes
private \Psr\Log\LoggerInterface $logger;
```

### Integration Points

**Connection Points:**
- **TimePunchController**: Extend with `getSchedulePanelData()` method
- **workspace-foot.html**: Include panel template when WhenIWork enabled
- **WorkbookAbly**: Subscribe to existing timepunch events

**Data Flow:**
```
[Browser] ←→ [TimePunchController] ←→ [WhenIWorkSchedule] ←→ [WhenIWork API]
    ↑                                        ↓
    └──────────── [Ably WebSocket] ←─────────┘
```

**Events Consumed:**
- `workbook:timepunch:clockin` - Update employee to clocked-in state
- `workbook:timepunch:clockout` - Update employee to clocked-out state
- `workbook:timepunch:breakstart` - Update employee to on-break state
- `workbook:timepunch:breakend` - Update employee to clocked-in state

**Events Triggered:**
- `schedule:panel:expanded` - Panel expanded (for layout manager)
- `schedule:panel:collapsed` - Panel collapsed
- `schedule:panel:employee-selected` - Employee clicked (for modal)

## Architecture Decisions

- [x] **ADR-1 Panel Position**: Fixed right sidebar vs floating overlay
  - **Choice**: Fixed right sidebar (like left navigation)
  - **Rationale**: Consistent with workspace layout; always visible at a glance
  - **Alternatives considered**: Floating overlay (like chat) - more flexible but obscures content
  - **Trade-offs**: Takes permanent screen real estate; requires responsive handling
  - **User confirmed**: ✅ 2025-12-09

- [x] **ADR-2 Data Loading Strategy**: Batch fetch vs individual API calls
  - **Choice**: Single batch API call (`GET /schedule-panel/`) combining schedule + status
  - **Rationale**: Reduces network round-trips; better performance with 10+ employees
  - **Alternatives considered**: Separate calls for schedule and each employee status
  - **Trade-offs**: Requires new endpoint; more complex backend aggregation
  - **User confirmed**: ✅ 2025-12-09

- [x] **ADR-3 Real-Time Updates**: Poll vs WebSocket
  - **Choice**: Ably WebSocket subscription to existing events
  - **Rationale**: Instant updates; leverages existing infrastructure; no additional load
  - **Alternatives considered**: Polling every 60s as fallback
  - **Trade-offs**: Depends on Ably availability; fallback polling not implemented
  - **User confirmed**: ✅ (follows existing Workbook pattern)

- [x] **ADR-4 PIN Entry Flow**: Per-action vs session-based
  - **Choice**: PIN required for each clock action
  - **Rationale**: Matches PRD requirements; more secure for shared kiosk
  - **Alternatives considered**: Store verified PIN for session duration
  - **Trade-offs**: Slightly more friction for multiple actions on same employee
  - **User confirmed**: ✅ 2025-12-09

- [x] **ADR-5 Manager Override**: User PIN vs linked employee PIN
  - **Choice**: Manager enters their own user account PIN
  - **Rationale**: Managers have user accounts with PINs; clearer audit trail
  - **Alternatives considered**: Manager enters linked employee PIN, store override code
  - **Trade-offs**: Managers must have user accounts (not just employee records)
  - **User confirmed**: ✅ 2025-12-09

## Quality Requirements

- **Performance**:
  - Schedule panel API response < 500ms (cached schedule + batch status)
  - Real-time updates via Ably < 5 seconds latency
  - Panel render with 20 employees < 100ms
  - Avatar images lazy-loaded with placeholder

- **Usability**:
  - Panel visible without scrolling on 1024px+ screens
  - Touch targets minimum 44px for tablet use
  - Status indicators color-blind accessible (use shapes + colors)
  - Panel collapse state persists across sessions

- **Security**:
  - PIN required for all clock actions (no exceptions)
  - Manager override requires `uri_manager_actions` permission
  - All clock actions logged with user/employee IDs
  - PIN masked in UI (dots, not numbers)

- **Reliability**:
  - Graceful degradation if Ably disconnects (manual refresh button)
  - Error recovery with retry button for WhenIWork API failures
  - Panel shows stale data with "last updated" timestamp if refresh fails

## Risks and Technical Debt

### Known Technical Issues

- **PIN stored in plain text**: `clockPin` column is varchar, not hashed (existing pattern)
- **WhenIWork API rate limits**: Unknown limits; may need request queuing for large stores
- **Legacy employee ID mapping**: Some stores use `employeeID = wiwUserId` instead of `externalId`

### Technical Debt

- **Plain text PIN**: Should be hashed but changing would break existing PINs
- **No polling fallback**: If Ably disconnects, panel doesn't auto-refresh (should add)
- **Hardcoded status colors**: Should move to CSS variables or store config

### Implementation Gotchas

- **WhenIWork v2 vs v3 API**: Break endpoints use v3 (`/v3/shift-breaks`), others use v2
- **Timezone handling**: "Today" must use store timezone, not browser/server timezone
- **Employee sync timing**: If employee added in WhenIWork but not synced, won't appear in panel
- **Ably message deduplication**: Must track message IDs to prevent duplicate status updates
- **PIN verification is direct comparison**: No hashing - `$employee['clockPin'] !== $pin`

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Clock In with Valid PIN**
```gherkin
Given: Panel shows scheduled employee with grey avatar
And: Employee has PIN set
When: Manager taps avatar and clicks "Clock In"
And: Enters correct employee PIN
Then: WhenIWork API called successfully
And: Avatar changes to full-color with green dot
And: Employee moves to "clocked in" section
And: Ably event broadcast to other clients
```

**Scenario 2: Invalid PIN Handling**
```gherkin
Given: Quick action modal open for employee
When: User enters incorrect PIN
Then: "Invalid PIN" error displayed
And: PIN field cleared and focused
And: User can retry (up to 3 times)
And: After 3 failures, manager override option shown (if permitted)
```

**Scenario 3: Manager Override**
```gherkin
Given: User has manager permissions
And: Employee PIN entry failed 3 times
When: Manager clicks "Use Manager PIN"
And: Enters their own PIN
Then: Manager PIN verified against user account
And: Clock action executed with override flag
And: Audit log records manager override
```

**Scenario 4: Real-Time Update from External Source**
```gherkin
Given: Panel showing employee as "scheduled"
When: Employee clocks in via WhenIWork mobile app
Then: Ably event received by panel
And: Avatar updates to clocked-in state without page refresh
And: Employee reorders in list
```

### Test Coverage Requirements

- **API Tests**: `getSchedulePanelData()`, `override/` endpoint, PIN verification
- **Unit Tests**: Employee sorting logic, status derivation, Ably event handling
- **Integration Tests**: WhenIWork API mocking, Ably event flow
- **UI Tests**: Panel render, modal states, collapse/expand, localStorage persistence
- **Edge Cases**: No employees scheduled, employee without PIN, WhenIWork disabled

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| typeNum | Store identifier pattern `[a-z][a-z]\d+` (e.g., `ou00`) | Used for database names, Ably channels, routes |
| Clock In/Out | Recording employee work time start/end | Via WhenIWork API |
| Break | Paid or unpaid time away from work | Uses WhenIWork shift-breaks API |
| Manager Override | Clock action using manager's PIN instead of employee's | For when employee forgets PIN |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| WIW / WhenIWork | Third-party scheduling and time tracking service | External API integration |
| Ably | Real-time messaging platform | WebSocket events for live updates |
| externalId | WhenIWork user ID stored in local employees table | Maps local employees to WhenIWork accounts |
| clockPin | 4-6 digit PIN for time clock verification | Stored in employees.clockPin column |

### API/Interface Terms

| Term | Definition | Context |
|------|------------|---------|
| W-Token | WhenIWork OAuth token | HTTP header for API authentication |
| punch/state | WhenIWork endpoint returning clock capabilities | Determines available actions |
| times/clockin | WhenIWork endpoint to record clock in | POST request with user ID |
| shift-breaks | WhenIWork v3 endpoint for break management | Start/end break operations |
