# Daybook Dashboard - Technical Specification

## 1. Overview

The Daybook Dashboard is a new employee-facing landing page that consolidates scheduling, tasks, KPIs, notes/comments, and backstock activity into a single collaborative workspace. It will replace `queue.html` as the default landing page for non-admin employees, with seamless tab navigation between the Daybook and the existing Queue view.

### 1.1 Goals
- Display employee schedules from WhenIWork (existing) and Homebase (new integration)
- Provide a collaborative whiteboard/notes system with time-based expiration
- Show configurable KPIs at the bottom of the dashboard
- Enhance the existing task list system with assignments, due dates, priorities, and scheduled visibility
- Display today's backstock activity (items added/pulled)
- Enable real-time collaboration using Ably (existing)

### 1.2 Reference Implementation
Based on competitor analysis (ResaleAI), the dashboard will feature:
- Left panel: Task lists with completion percentages
- Right panel: Notes/comments feed with employee schedule
- Bottom bar: KPI metrics (Sales, Avg Trans, Trade %, etc.)

---

## 2. Existing Systems Analysis

### 2.1 Task System (Needs Enhancement)
**Current Schema:**
```sql
-- taskGroups table
CREATE TABLE `taskGroups` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `groupName` varchar(60) DEFAULT NULL,
  PRIMARY KEY (`id`)
);

-- tasks table
CREATE TABLE `tasks` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `taskName` varchar(60) DEFAULT NULL,
  `comment` varchar(500) DEFAULT NULL,
  `taskGroup` int(10) unsigned DEFAULT NULL,
  `recurOn` varchar(60) DEFAULT 'SUN MON TUE WED THU FRI SAT',
  PRIMARY KEY (`id`)
);

-- taskData table (completion tracking)
CREATE TABLE `taskData` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `taskGroup` int(11) DEFAULT NULL,
  `timeCompleted` timestamp NOT NULL DEFAULT current_timestamp(),
  `comments` text DEFAULT NULL,
  `employeeName` varchar(60) DEFAULT NULL,
  `date` date DEFAULT NULL,
  PRIMARY KEY (`id`)
);
```

**Current PHP Classes:**
- `Task.php` - Basic CRUD for tasks
- `TaskGroup.php` - Group management
- `TaskList.php` - Aggregates tasks by group, filters by day (recurOn)

**Limitations to Address:**
- No employee assignment
- No due dates/times
- No priority levels
- No status tracking (not started/in progress/completed)
- No scheduled visibility times
- No task-level comments

### 2.2 WhenIWork Integration (Needs Extension)
**Current Implementation:**
- `EmployeesController.php` - Pulls employees, syncs to local DB, gets clocked-in employees
- `Employee.php` - Simple model with wiwID, firstName, lastName, payRate
- Uses Redis cache (`{typeNum}_employees` key, 5-min TTL)
- Already fetches shifts for today to determine clocked-in status

**Needs:**
- Extend shift data to show full schedule (start/end times)
- Cache schedule data separately
- Abstract scheduling provider for Homebase support

### 2.3 Backstock System (Ready to Use)
**Current Implementation:**
- `BackstockFactory.php` - Creates bins, actions, categories
- `Action.php` - Tracks add/remove/empty actions with timestamps
- Existing endpoint: `GET /api/:typeNum/backstock/actions/` returns recent actions

**Available for Daybook:**
- Filter actions by today's date
- Group by action type (added vs pulled)
- Show category breakdown

### 2.4 Shift Notes (Legacy - Avoid)
```sql
CREATE TABLE `shiftNotes` (
  `internalID` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `id` varchar(10) DEFAULT NULL,
  `timeStamp` timestamp NOT NULL DEFAULT current_timestamp(),
  `itemType` tinyint(1) unsigned DEFAULT NULL,
  `itemPriority` tinyint(1) unsigned DEFAULT NULL,
  `itemSubmitter` varchar(32) DEFAULT NULL,
  `itemTitle` varchar(255) DEFAULT NULL,
  `itemComment` text DEFAULT NULL,
  `itemCloser` varchar(32) DEFAULT NULL,
  `itemStatus` tinyint(2) unsigned DEFAULT NULL,
  `timeClosed` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`internalID`)
);
```
*Recommendation: Build new notes/comments system from scratch as this schema is inadequate.*

---

## 3. Database Schema Changes

### 3.1 Enhanced Task System

```sql
-- Modify existing tasks table (add columns)
ALTER TABLE `tasks` ADD COLUMN `priority` tinyint(1) unsigned DEFAULT 2 COMMENT '1=High, 2=Normal, 3=Low';
ALTER TABLE `tasks` ADD COLUMN `sortOrder` int(10) unsigned DEFAULT 0;

-- New: Task List Configuration (replaces simple taskGroups for Daybook)
CREATE TABLE `daybook_task_lists` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `groupId` int(10) unsigned NOT NULL COMMENT 'FK to taskGroups.id',
  `displayName` varchar(100) DEFAULT NULL,
  `scheduledDays` varchar(60) DEFAULT 'SUN MON TUE WED THU FRI SAT',
  `startTime` time DEFAULT NULL COMMENT 'When this list becomes active on daybook',
  `sortOrder` int(10) unsigned DEFAULT 0,
  `isActive` tinyint(1) unsigned DEFAULT 1,
  PRIMARY KEY (`id`),
  KEY `groupId` (`groupId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- New: Task Assignments
CREATE TABLE `daybook_task_assignments` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `taskId` int(10) unsigned NOT NULL,
  `employeeId` int(10) unsigned DEFAULT NULL COMMENT 'NULL = assigned to all',
  `dueDate` date DEFAULT NULL,
  `dueTime` time DEFAULT NULL,
  `createdAt` timestamp DEFAULT current_timestamp(),
  `createdBy` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `taskId` (`taskId`),
  KEY `employeeId` (`employeeId`),
  KEY `dueDate` (`dueDate`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- New: Task Completions (replaces taskData for daily tracking)
CREATE TABLE `daybook_task_completions` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `taskId` int(10) unsigned NOT NULL,
  `date` date NOT NULL,
  `status` tinyint(1) unsigned DEFAULT 0 COMMENT '0=Not Started, 1=In Progress, 2=Completed',
  `completedBy` int(10) unsigned DEFAULT NULL,
  `completedAt` timestamp NULL DEFAULT NULL,
  `notes` text DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `task_date` (`taskId`, `date`),
  KEY `date` (`date`),
  KEY `status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- New: Task Comments
CREATE TABLE `daybook_task_comments` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `taskId` int(10) unsigned NOT NULL,
  `date` date NOT NULL COMMENT 'The day this comment relates to',
  `employeeId` int(10) unsigned NOT NULL,
  `comment` text NOT NULL,
  `createdAt` timestamp DEFAULT current_timestamp(),
  PRIMARY KEY (`id`),
  KEY `taskId_date` (`taskId`, `date`),
  KEY `employeeId` (`employeeId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

### 3.2 Notes/Comments System (New)

```sql
-- Daybook Notes (Manager/Owner posts)
CREATE TABLE `daybook_notes` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `authorEmployeeId` int(10) unsigned NOT NULL,
  `title` varchar(255) DEFAULT NULL,
  `content` text NOT NULL,
  `contentHtml` text DEFAULT NULL COMMENT 'Rich text HTML content',
  `startDate` date NOT NULL COMMENT 'First day note is visible',
  `endDate` date DEFAULT NULL COMMENT 'Last day note is visible (NULL = indefinite)',
  `isManagerOnly` tinyint(1) unsigned DEFAULT 0,
  `isPinned` tinyint(1) unsigned DEFAULT 0,
  `createdAt` timestamp DEFAULT current_timestamp(),
  `updatedAt` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(),
  PRIMARY KEY (`id`),
  KEY `visibility` (`startDate`, `endDate`, `isManagerOnly`),
  KEY `authorEmployeeId` (`authorEmployeeId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Note Reactions (likes)
CREATE TABLE `daybook_note_reactions` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `noteId` int(10) unsigned NOT NULL,
  `employeeId` int(10) unsigned NOT NULL,
  `reactionType` varchar(20) DEFAULT 'like',
  `createdAt` timestamp DEFAULT current_timestamp(),
  PRIMARY KEY (`id`),
  UNIQUE KEY `note_employee` (`noteId`, `employeeId`),
  KEY `noteId` (`noteId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Note Comments (sub-comments on notes)
CREATE TABLE `daybook_note_comments` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `noteId` int(10) unsigned NOT NULL,
  `employeeId` int(10) unsigned NOT NULL,
  `comment` text NOT NULL,
  `createdAt` timestamp DEFAULT current_timestamp(),
  PRIMARY KEY (`id`),
  KEY `noteId` (`noteId`),
  KEY `employeeId` (`employeeId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Whiteboard Canvas State (the main drawable canvas)
CREATE TABLE `daybook_whiteboard_canvas` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `date` date NOT NULL COMMENT 'Canvas is per-day',
  `canvasData` longtext NOT NULL COMMENT 'JSON serialized canvas state (Fabric.js format)',
  `thumbnail` mediumtext DEFAULT NULL COMMENT 'Base64 PNG thumbnail for quick preview',
  `lastModifiedBy` int(10) unsigned DEFAULT NULL,
  `lastModifiedAt` timestamp DEFAULT current_timestamp() ON UPDATE current_timestamp(),
  `createdAt` timestamp DEFAULT current_timestamp(),
  PRIMARY KEY (`id`),
  UNIQUE KEY `date` (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Whiteboard Overlay Items (sticky notes, text boxes positioned on canvas)
CREATE TABLE `daybook_whiteboard_items` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `employeeId` int(10) unsigned NOT NULL,
  `type` enum('sticky', 'text', 'image') DEFAULT 'sticky',
  `content` text NOT NULL COMMENT 'Text content or image URL',
  `positionX` int(10) unsigned DEFAULT 0 COMMENT 'X position on canvas (pixels)',
  `positionY` int(10) unsigned DEFAULT 0 COMMENT 'Y position on canvas (pixels)',
  `width` int(10) unsigned DEFAULT 200,
  `height` int(10) unsigned DEFAULT 150,
  `rotation` smallint(6) DEFAULT 0 COMMENT 'Rotation in degrees',
  `zIndex` int(10) unsigned DEFAULT 0,
  `backgroundColor` varchar(9) DEFAULT '#FFFF00' COMMENT 'Hex color with optional alpha',
  `textColor` varchar(7) DEFAULT '#000000',
  `fontSize` tinyint(3) unsigned DEFAULT 14,
  `expiresAt` timestamp NULL DEFAULT NULL,
  `createdAt` timestamp DEFAULT current_timestamp(),
  PRIMARY KEY (`id`),
  KEY `expiresAt` (`expiresAt`),
  KEY `employeeId` (`employeeId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Whiteboard Drawing History (for undo/redo and audit)
CREATE TABLE `daybook_whiteboard_history` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `date` date NOT NULL,
  `employeeId` int(10) unsigned NOT NULL,
  `action` enum('draw', 'erase', 'clear', 'add_item', 'move_item', 'delete_item') NOT NULL,
  `actionData` text DEFAULT NULL COMMENT 'JSON with action details',
  `timestamp` timestamp DEFAULT current_timestamp(),
  PRIMARY KEY (`id`),
  KEY `date` (`date`),
  KEY `employeeId` (`employeeId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

### 3.3 KPI Configuration

```sql
-- KPI visibility settings per store
CREATE TABLE `daybook_kpi_config` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `kpiKey` varchar(50) NOT NULL COMMENT 'sales, avgTrans, tradePercent, etc.',
  `displayName` varchar(100) NOT NULL,
  `isVisible` tinyint(1) unsigned DEFAULT 1,
  `sortOrder` int(10) unsigned DEFAULT 0,
  `showGoal` tinyint(1) unsigned DEFAULT 1,
  `showComps` tinyint(1) unsigned DEFAULT 1,
  PRIMARY KEY (`id`),
  UNIQUE KEY `kpiKey` (`kpiKey`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Default KPIs to insert
INSERT INTO `daybook_kpi_config` (`kpiKey`, `displayName`, `sortOrder`) VALUES
('sales', 'Sales', 1),
('avgTrans', 'Avg Trans', 2),
('tradePercent', 'Trade %', 3),
('transactions', 'Transactions', 4),
('laborHours', 'Total Labor Hours', 5),
('salesPerLaborHour', 'Sales / Labor Hour', 6),
('totalWages', 'Total Wages', 7),
('buys', 'Buys', 8),
('buysComp', 'Buys Comp', 9),
('trade', 'Trade', 10),
('percentTraded', '% Traded', 11),
('tradeComp', 'Trade Comp', 12),
('percentTradedComp', '% Traded Comp', 13);
```

### 3.4 Schedule Cache

```sql
-- Cached schedule data from WhenIWork/Homebase
CREATE TABLE `daybook_schedule_cache` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `date` date NOT NULL,
  `employeeId` int(10) unsigned NOT NULL,
  `providerEmployeeId` varchar(50) DEFAULT NULL COMMENT 'External ID from WiW/Homebase',
  `provider` enum('wheniwork', 'homebase', 'manual') DEFAULT 'wheniwork',
  `shiftStart` datetime NOT NULL,
  `shiftEnd` datetime NOT NULL,
  `position` varchar(100) DEFAULT NULL,
  `notes` text DEFAULT NULL,
  `cachedAt` timestamp DEFAULT current_timestamp(),
  PRIMARY KEY (`id`),
  KEY `date_employee` (`date`, `employeeId`),
  KEY `date` (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

---

## 4. PHP Class Structure

### 4.1 New Classes

```
userfrosting/models/Class/Daybook/
├── DaybookController.php      # Main controller for daybook routes
├── TaskListManager.php        # Enhanced task list with scheduling
├── TaskCompletion.php         # Track task completion status
├── TaskAssignment.php         # Task assignment management
├── NoteManager.php            # Notes/comments CRUD
├── WhiteboardManager.php      # Whiteboard items
├── KPIConfig.php              # KPI visibility settings
├── KPIProvider.php            # Aggregates KPI data from various sources
├── ScheduleProvider.php       # Abstract schedule provider
├── WhenIWorkSchedule.php      # WhenIWork schedule implementation
└── HomebaseSchedule.php       # Homebase schedule implementation (future)

userfrosting/controllers/Daybook/
├── DaybookPageController.php  # Renders daybook page
├── TasksApiController.php     # Task-related API endpoints
├── NotesApiController.php     # Notes/comments API endpoints
├── WhiteboardApiController.php # Whiteboard API endpoints
├── ScheduleApiController.php  # Schedule API endpoints
└── KPIApiController.php       # KPI data API endpoints
```

### 4.2 Key Class: ScheduleProvider (Abstract)

```php
<?php
namespace BuyerKiosk\Daybook;

abstract class ScheduleProvider
{
    protected $store;
    protected $predis;
    protected $cacheKey;
    protected $cacheTTL = 300; // 5 minutes

    abstract public function getScheduleForDate(\DateTime $date): array;
    abstract public function getEmployeeShift(int $employeeId, \DateTime $date): ?array;
    abstract public function syncScheduleToCache(\DateTime $date): bool;

    protected function getCacheKey(\DateTime $date): string
    {
        return $this->store->getTypeNum() . '_schedule_' . $date->format('Y-m-d');
    }
}
```

### 4.3 Key Class: KPIProvider

```php
<?php
namespace BuyerKiosk\Daybook;

class KPIProvider
{
    private $store;
    private $db;

    // Available KPI calculations
    public function getSales(\DateTime $date): array;
    public function getAvgTransaction(\DateTime $date): array;
    public function getTradePercent(\DateTime $date): array;
    public function getTransactionCount(\DateTime $date): array;
    public function getLaborHours(\DateTime $date): array;
    public function getSalesPerLaborHour(\DateTime $date): array;
    public function getBuysCount(\DateTime $date): array;
    public function getBackstockSummary(\DateTime $date): array;

    // Comparison data
    public function getCompsForDate(\DateTime $date): array; // Same day last year
    public function getGoalsForDate(\DateTime $date): array; // From LiveFinancials

    // Aggregator
    public function getAllKPIs(\DateTime $date, array $visibleKpis): array;
}
```

---

## 5. API Endpoints

### 5.1 Daybook Page Routes

```php
// Main daybook page (new landing page)
$app->get('/:typeNum/daybook/?', 'DaybookPageController:pageDaybook');

// Combined Queue + Daybook view (tab switching)
$app->get('/:typeNum/workspace/?', 'DaybookPageController:pageWorkspace');
```

### 5.2 Task API Routes

```php
$app->group('/api/:typeNum/daybook/tasks', function() use ($app) {
    // Get all task lists for today
    $app->get('/lists/?', 'TasksApiController:getTodayLists');

    // Get specific task list with tasks
    $app->get('/lists/:listId/?', 'TasksApiController:getTaskList');

    // Get active task list (based on current time)
    $app->get('/lists/active/?', 'TasksApiController:getActiveList');

    // Update task completion status
    $app->post('/tasks/:taskId/status/?', 'TasksApiController:updateTaskStatus');

    // Add comment to task
    $app->post('/tasks/:taskId/comment/?', 'TasksApiController:addTaskComment');

    // Get task comments
    $app->get('/tasks/:taskId/comments/?', 'TasksApiController:getTaskComments');

    // Admin: Create/update task list schedule
    $app->post('/lists/?', 'TasksApiController:createTaskList');
    $app->put('/lists/:listId/?', 'TasksApiController:updateTaskList');

    // Admin: Assign task to employee
    $app->post('/tasks/:taskId/assign/?', 'TasksApiController:assignTask');
});
```

### 5.3 Notes API Routes

```php
$app->group('/api/:typeNum/daybook/notes', function() use ($app) {
    // Get all visible notes for today
    $app->get('/?', 'NotesApiController:getTodayNotes');

    // Create note (manager/owner only)
    $app->post('/?', 'NotesApiController:createNote');

    // Update note
    $app->put('/:noteId/?', 'NotesApiController:updateNote');

    // Delete note
    $app->delete('/:noteId/?', 'NotesApiController:deleteNote');

    // React to note (like)
    $app->post('/:noteId/react/?', 'NotesApiController:addReaction');
    $app->delete('/:noteId/react/?', 'NotesApiController:removeReaction');

    // Add comment to note
    $app->post('/:noteId/comment/?', 'NotesApiController:addComment');

    // Get note comments
    $app->get('/:noteId/comments/?', 'NotesApiController:getComments');
});
```

### 5.4 Whiteboard API Routes

```php
$app->group('/api/:typeNum/daybook/whiteboard', function() use ($app) {
    // === Canvas Drawing APIs ===

    // Get today's canvas state (full drawing data)
    $app->get('/canvas/?', 'WhiteboardApiController:getCanvas');

    // Get canvas for specific date
    $app->get('/canvas/:date/?', 'WhiteboardApiController:getCanvasByDate');

    // Save canvas state (debounced from frontend)
    $app->post('/canvas/?', 'WhiteboardApiController:saveCanvas');

    // Clear entire canvas
    $app->delete('/canvas/?', 'WhiteboardApiController:clearCanvas');

    // Get canvas thumbnail (for preview)
    $app->get('/canvas/thumbnail/?', 'WhiteboardApiController:getCanvasThumbnail');

    // === Overlay Items APIs (sticky notes, text boxes) ===

    // Get all active overlay items
    $app->get('/items/?', 'WhiteboardApiController:getItems');

    // Add overlay item (sticky note, text box)
    $app->post('/items/?', 'WhiteboardApiController:addItem');

    // Update item position/content/style
    $app->put('/items/:itemId/?', 'WhiteboardApiController:updateItem');

    // Delete overlay item
    $app->delete('/items/:itemId/?', 'WhiteboardApiController:deleteItem');

    // === Real-time Sync ===

    // Get incremental drawing updates since timestamp
    $app->get('/updates/:since/?', 'WhiteboardApiController:getUpdatesSince');

    // Post incremental drawing action (for real-time sync)
    $app->post('/action/?', 'WhiteboardApiController:postDrawAction');

    // === Maintenance ===

    // Clean up expired items (called by cron)
    $app->post('/cleanup/?', 'WhiteboardApiController:cleanupExpired');

    // Archive canvas to history (end of day)
    $app->post('/archive/?', 'WhiteboardApiController:archiveCanvas');
});
```

### 5.5 Schedule API Routes

```php
$app->group('/api/:typeNum/daybook/schedule', function() use ($app) {
    // Get today's schedule
    $app->get('/?', 'ScheduleApiController:getTodaySchedule');

    // Get schedule for specific date
    $app->get('/:date/?', 'ScheduleApiController:getScheduleForDate');

    // Force refresh schedule cache
    $app->post('/refresh/?', 'ScheduleApiController:refreshSchedule');
});
```

### 5.6 KPI API Routes

```php
$app->group('/api/:typeNum/daybook/kpi', function() use ($app) {
    // Get today's KPIs
    $app->get('/?', 'KPIApiController:getTodayKPIs');

    // Get KPIs for specific date
    $app->get('/:date/?', 'KPIApiController:getKPIsForDate');

    // Admin: Get KPI config
    $app->get('/config/?', 'KPIApiController:getConfig');

    // Admin: Update KPI visibility
    $app->put('/config/?', 'KPIApiController:updateConfig');
});
```

### 5.7 Backstock Summary API

```php
// Extend existing backstock routes
$app->get('/api/:typeNum/backstock/summary/today/?', 'ActionsController:getTodaySummary');
```

---

## 6. Frontend Architecture

### 6.1 Template Structure

```
userfrosting/templates/themes/default/
├── daybook/
│   ├── daybook.html           # Main daybook template
│   ├── partials/
│   │   ├── task-list.html     # Task list component
│   │   ├── task-item.html     # Single task row
│   │   ├── notes-feed.html    # Notes/comments feed
│   │   ├── note-card.html     # Single note card
│   │   ├── schedule-panel.html # Employee schedule
│   │   ├── kpi-bar.html       # Bottom KPI bar
│   │   ├── backstock-summary.html # Backstock activity
│   │   └── whiteboard.html    # Whiteboard canvas
│   └── modals/
│       ├── add-note.html      # Create/edit note modal
│       ├── task-detail.html   # Task details/comments
│       ├── employee-selector.html # Select employee for actions
│       └── whiteboard-tools.html  # Whiteboard drawing tools
└── workspace.html             # Combined Queue + Daybook view
```

### 6.2 JavaScript Structure

```
public_html/view/common/js/daybook/
├── daybook.js                 # Main daybook controller
├── task-manager.js            # Task list interactions
├── notes-manager.js           # Notes CRUD and interactions
├── whiteboard.js              # Canvas drawing/sticky notes
├── schedule-display.js        # Schedule rendering
├── kpi-display.js             # KPI bar updates
├── backstock-display.js       # Backstock summary
├── employee-selector.js       # Employee picker component
└── ably-sync.js               # Real-time sync via Ably
```

### 6.3 Real-time Updates (Ably Integration)

The Daybook will use the existing Ably infrastructure already in place for the buy queue. This ensures all browser instances stay synchronized in real-time.

#### 6.3.1 Existing Ably Infrastructure

**Backend (PHP) - `userfrosting/routes/groups/drs.php`:**
```php
// Existing function used throughout the system
function sendEncodedData($data) {
    $ably = new Ably\AblyRest($_ENV['ABLY_KEY']);
    $channel = $ably->channel($data['category']);
    $channel->publish($data['action'], $data);
}
```

**Frontend (JS) - Pattern from `buyQueue.js`:**
```javascript
// Existing pattern - connects to store-specific channel
var client = new Ably.Realtime({
    key: "YOUR_ABLY_KEY",
    idempotentRestPublishing: true
});
var channel = client.channels.get(typeNum);
channel.subscribe(function(message) {
    // Handle real-time updates
});
```

#### 6.3.2 Daybook Channel Strategy

We'll use the **existing store channel** (`typeNum`) with new action types for Daybook events. This keeps everything on one channel per store, matching the current queue pattern.

**Channel:** `{typeNum}` (e.g., `pa00`, `ou00`)

**New Daybook Action Types:**
```javascript
const DAYBOOK_ACTIONS = {
    // Task Actions
    'daybook:task:complete': 'Task marked complete',
    'daybook:task:uncomplete': 'Task marked incomplete',
    'daybook:task:progress': 'Task marked in progress',
    'daybook:task:comment': 'Comment added to task',
    'daybook:task:assign': 'Task assigned to employee',

    // Notes Actions
    'daybook:note:create': 'New note created',
    'daybook:note:update': 'Note updated',
    'daybook:note:delete': 'Note deleted',
    'daybook:note:react': 'Reaction added to note',
    'daybook:note:unreact': 'Reaction removed from note',
    'daybook:note:comment': 'Comment added to note',

    // Whiteboard Actions
    'daybook:whiteboard:draw': 'Drawing stroke added',
    'daybook:whiteboard:sticky:add': 'Sticky note added',
    'daybook:whiteboard:sticky:move': 'Sticky note moved',
    'daybook:whiteboard:sticky:edit': 'Sticky note edited',
    'daybook:whiteboard:sticky:delete': 'Sticky note deleted',
    'daybook:whiteboard:clear': 'Canvas cleared',
    'daybook:whiteboard:undo': 'Undo action',

    // Schedule Actions
    'daybook:schedule:refresh': 'Schedule data refreshed',

    // KPI Actions
    'daybook:kpi:refresh': 'KPI data refreshed',

    // Backstock Actions (extends existing)
    'daybook:backstock:update': 'Backstock activity updated'
};
```

#### 6.3.3 Backend Publishing Helper

**New file: `userfrosting/lib/DaybookAbly.php`:**
```php
<?php
namespace BuyerKiosk\Daybook;

class DaybookAbly
{
    private $ably;
    private $typeNum;

    public function __construct(string $typeNum)
    {
        $this->ably = new \Ably\AblyRest($_ENV['ABLY_KEY']);
        $this->typeNum = $typeNum;
    }

    /**
     * Publish a daybook event to the store channel
     */
    public function publish(string $action, array $data): void
    {
        $channel = $this->ably->channel($this->typeNum);
        $payload = array_merge($data, [
            'action' => $action,
            'category' => $this->typeNum,
            'timestamp' => time(),
            'source' => 'daybook'
        ]);
        $channel->publish($action, $payload);
    }

    // Convenience methods for common events
    public function taskCompleted(int $taskId, int $employeeId, string $date): void
    {
        $this->publish('daybook:task:complete', [
            'taskId' => $taskId,
            'employeeId' => $employeeId,
            'date' => $date
        ]);
    }

    public function taskComment(int $taskId, int $commentId, int $employeeId, string $comment): void
    {
        $this->publish('daybook:task:comment', [
            'taskId' => $taskId,
            'commentId' => $commentId,
            'employeeId' => $employeeId,
            'comment' => $comment
        ]);
    }

    public function noteCreated(int $noteId, array $noteData): void
    {
        $this->publish('daybook:note:create', [
            'noteId' => $noteId,
            'note' => $noteData
        ]);
    }

    public function noteReaction(int $noteId, int $employeeId, string $reaction, bool $added): void
    {
        $action = $added ? 'daybook:note:react' : 'daybook:note:unreact';
        $this->publish($action, [
            'noteId' => $noteId,
            'employeeId' => $employeeId,
            'reaction' => $reaction
        ]);
    }

    public function whiteboardDraw(array $pathData, int $employeeId): void
    {
        $this->publish('daybook:whiteboard:draw', [
            'path' => $pathData,
            'employeeId' => $employeeId
        ]);
    }

    public function whiteboardStickyAdd(array $stickyData): void
    {
        $this->publish('daybook:whiteboard:sticky:add', [
            'sticky' => $stickyData
        ]);
    }

    public function whiteboardClear(int $employeeId): void
    {
        $this->publish('daybook:whiteboard:clear', [
            'employeeId' => $employeeId
        ]);
    }

    public function kpiRefresh(): void
    {
        $this->publish('daybook:kpi:refresh', []);
    }

    public function scheduleRefresh(): void
    {
        $this->publish('daybook:schedule:refresh', []);
    }
}
```

#### 6.3.4 Frontend Ably Manager

**New file: `public_html/view/common/js/daybook/ably-sync.js`:**
```javascript
/**
 * DaybookAblySync - Real-time synchronization for Daybook
 * Uses existing Ably connection pattern from buyQueue.js
 */
class DaybookAblySync {
    constructor(typeNum, options = {}) {
        this.typeNum = typeNum;
        this.client = null;
        this.channel = null;
        this.messageIDs = []; // Deduplication like buyQueue.js
        this.handlers = {};
        this.instanceId = this.generateInstanceId();

        // Callbacks for each component
        this.onTaskUpdate = options.onTaskUpdate || (() => {});
        this.onNoteUpdate = options.onNoteUpdate || (() => {});
        this.onWhiteboardUpdate = options.onWhiteboardUpdate || (() => {});
        this.onKPIUpdate = options.onKPIUpdate || (() => {});
        this.onScheduleUpdate = options.onScheduleUpdate || (() => {});
        this.onBackstockUpdate = options.onBackstockUpdate || (() => {});

        this.connect();
    }

    generateInstanceId() {
        return 'daybook_' + Math.random().toString(36).substr(2, 9);
    }

    connect() {
        // Use same Ably key as buyQueue.js (injected via template)
        this.client = new Ably.Realtime({
            key: window.ABLY_KEY || "YOUR_ABLY_KEY",
            idempotentRestPublishing: true
        });

        this.client.connection.on('connected', () => {
            console.log('[DaybookAbly] Connected to Ably');
            this.subscribeToChannel();
        });

        this.client.connection.on('disconnected', () => {
            console.log('[DaybookAbly] Disconnected from Ably');
        });

        this.client.connection.on('failed', (err) => {
            console.error('[DaybookAbly] Connection failed:', err);
        });
    }

    subscribeToChannel() {
        // Subscribe to the store's main channel (same as queue)
        this.channel = this.client.channels.get(this.typeNum);

        this.channel.subscribe((message) => {
            // Deduplicate messages (same pattern as buyQueue.js)
            if (this.messageIDs.includes(message.id)) {
                return;
            }
            this.messageIDs.push(message.id);

            // Keep message ID array from growing indefinitely
            if (this.messageIDs.length > 100) {
                this.messageIDs.shift();
            }

            // Only process daybook-related actions
            if (message.name && message.name.startsWith('daybook:')) {
                this.handleMessage(message);
            }
        });
    }

    handleMessage(message) {
        const data = message.data;
        const action = message.name;

        // Skip messages from this instance to prevent echo
        if (data.instanceId === this.instanceId) {
            return;
        }

        console.log('[DaybookAbly] Received:', action, data);

        // Route to appropriate handler
        if (action.startsWith('daybook:task:')) {
            this.handleTaskMessage(action, data);
        } else if (action.startsWith('daybook:note:')) {
            this.handleNoteMessage(action, data);
        } else if (action.startsWith('daybook:whiteboard:')) {
            this.handleWhiteboardMessage(action, data);
        } else if (action === 'daybook:kpi:refresh') {
            this.onKPIUpdate(data);
        } else if (action === 'daybook:schedule:refresh') {
            this.onScheduleUpdate(data);
        } else if (action === 'daybook:backstock:update') {
            this.onBackstockUpdate(data);
        }
    }

    handleTaskMessage(action, data) {
        switch (action) {
            case 'daybook:task:complete':
                this.onTaskUpdate({
                    type: 'complete',
                    taskId: data.taskId,
                    employeeId: data.employeeId,
                    date: data.date
                });
                break;
            case 'daybook:task:uncomplete':
                this.onTaskUpdate({
                    type: 'uncomplete',
                    taskId: data.taskId,
                    date: data.date
                });
                break;
            case 'daybook:task:progress':
                this.onTaskUpdate({
                    type: 'progress',
                    taskId: data.taskId,
                    employeeId: data.employeeId,
                    date: data.date
                });
                break;
            case 'daybook:task:comment':
                this.onTaskUpdate({
                    type: 'comment',
                    taskId: data.taskId,
                    commentId: data.commentId,
                    employeeId: data.employeeId,
                    comment: data.comment
                });
                break;
        }
    }

    handleNoteMessage(action, data) {
        switch (action) {
            case 'daybook:note:create':
                this.onNoteUpdate({
                    type: 'create',
                    noteId: data.noteId,
                    note: data.note
                });
                break;
            case 'daybook:note:update':
                this.onNoteUpdate({
                    type: 'update',
                    noteId: data.noteId,
                    note: data.note
                });
                break;
            case 'daybook:note:delete':
                this.onNoteUpdate({
                    type: 'delete',
                    noteId: data.noteId
                });
                break;
            case 'daybook:note:react':
                this.onNoteUpdate({
                    type: 'react',
                    noteId: data.noteId,
                    employeeId: data.employeeId,
                    reaction: data.reaction
                });
                break;
            case 'daybook:note:unreact':
                this.onNoteUpdate({
                    type: 'unreact',
                    noteId: data.noteId,
                    employeeId: data.employeeId
                });
                break;
            case 'daybook:note:comment':
                this.onNoteUpdate({
                    type: 'comment',
                    noteId: data.noteId,
                    commentId: data.commentId,
                    employeeId: data.employeeId,
                    comment: data.comment
                });
                break;
        }
    }

    handleWhiteboardMessage(action, data) {
        switch (action) {
            case 'daybook:whiteboard:draw':
                this.onWhiteboardUpdate({
                    type: 'draw',
                    path: data.path,
                    employeeId: data.employeeId
                });
                break;
            case 'daybook:whiteboard:sticky:add':
                this.onWhiteboardUpdate({
                    type: 'sticky:add',
                    sticky: data.sticky
                });
                break;
            case 'daybook:whiteboard:sticky:move':
                this.onWhiteboardUpdate({
                    type: 'sticky:move',
                    stickyId: data.stickyId,
                    position: data.position
                });
                break;
            case 'daybook:whiteboard:sticky:edit':
                this.onWhiteboardUpdate({
                    type: 'sticky:edit',
                    stickyId: data.stickyId,
                    content: data.content
                });
                break;
            case 'daybook:whiteboard:sticky:delete':
                this.onWhiteboardUpdate({
                    type: 'sticky:delete',
                    stickyId: data.stickyId
                });
                break;
            case 'daybook:whiteboard:clear':
                this.onWhiteboardUpdate({
                    type: 'clear',
                    employeeId: data.employeeId
                });
                break;
        }
    }

    // Methods to publish events (called when local user makes changes)
    publishTaskComplete(taskId, employeeId, date) {
        this.publish('daybook:task:complete', { taskId, employeeId, date });
    }

    publishTaskUncomplete(taskId, date) {
        this.publish('daybook:task:uncomplete', { taskId, date });
    }

    publishTaskProgress(taskId, employeeId, date) {
        this.publish('daybook:task:progress', { taskId, employeeId, date });
    }

    publishTaskComment(taskId, commentId, employeeId, comment) {
        this.publish('daybook:task:comment', { taskId, commentId, employeeId, comment });
    }

    publishNoteCreate(noteId, note) {
        this.publish('daybook:note:create', { noteId, note });
    }

    publishNoteUpdate(noteId, note) {
        this.publish('daybook:note:update', { noteId, note });
    }

    publishNoteDelete(noteId) {
        this.publish('daybook:note:delete', { noteId });
    }

    publishNoteReact(noteId, employeeId, reaction) {
        this.publish('daybook:note:react', { noteId, employeeId, reaction });
    }

    publishNoteUnreact(noteId, employeeId) {
        this.publish('daybook:note:unreact', { noteId, employeeId });
    }

    publishNoteComment(noteId, commentId, employeeId, comment) {
        this.publish('daybook:note:comment', { noteId, commentId, employeeId, comment });
    }

    publishWhiteboardDraw(path, employeeId) {
        this.publish('daybook:whiteboard:draw', { path, employeeId });
    }

    publishWhiteboardStickyAdd(sticky) {
        this.publish('daybook:whiteboard:sticky:add', { sticky });
    }

    publishWhiteboardStickyMove(stickyId, position) {
        this.publish('daybook:whiteboard:sticky:move', { stickyId, position });
    }

    publishWhiteboardStickyEdit(stickyId, content) {
        this.publish('daybook:whiteboard:sticky:edit', { stickyId, content });
    }

    publishWhiteboardStickyDelete(stickyId) {
        this.publish('daybook:whiteboard:sticky:delete', { stickyId });
    }

    publishWhiteboardClear(employeeId) {
        this.publish('daybook:whiteboard:clear', { employeeId });
    }

    publish(action, data) {
        if (!this.channel) {
            console.error('[DaybookAbly] Cannot publish - not connected');
            return;
        }

        const payload = {
            ...data,
            action: action,
            category: this.typeNum,
            instanceId: this.instanceId,
            timestamp: Date.now()
        };

        this.channel.publish(action, payload);
    }

    disconnect() {
        if (this.client) {
            this.client.close();
        }
    }
}

// Export for use
window.DaybookAblySync = DaybookAblySync;
```

#### 6.3.5 Integration with Daybook Components

**Main Daybook Initialization (`daybook.js`):**
```javascript
// Initialize Ably sync when daybook loads
document.addEventListener('DOMContentLoaded', function() {
    var pathname = window.location.pathname;
    var typeNum = pathname.split("/")[1];

    // Create the Ably sync manager
    window.daybookAbly = new DaybookAblySync(typeNum, {
        onTaskUpdate: function(update) {
            // Update task UI based on update type
            if (window.daybookTasks) {
                window.daybookTasks.handleRemoteUpdate(update);
            }
        },
        onNoteUpdate: function(update) {
            // Update notes UI
            if (window.daybookNotes) {
                window.daybookNotes.handleRemoteUpdate(update);
            }
        },
        onWhiteboardUpdate: function(update) {
            // Update whiteboard canvas
            if (window.daybookWhiteboard) {
                window.daybookWhiteboard.handleRemoteUpdate(update);
            }
        },
        onKPIUpdate: function(update) {
            // Refresh KPI display
            if (window.daybookKPI) {
                window.daybookKPI.refresh();
            }
        },
        onScheduleUpdate: function(update) {
            // Refresh schedule display
            if (window.daybookSchedule) {
                window.daybookSchedule.refresh();
            }
        },
        onBackstockUpdate: function(update) {
            // Refresh backstock summary
            if (window.daybookBackstock) {
                window.daybookBackstock.refresh();
            }
        }
    });
});
```

#### 6.3.6 Example: Task Manager with Ably

```javascript
// task-manager.js - Example of component using Ably sync
class DaybookTaskManager {
    constructor(containerId, typeNum) {
        this.container = document.getElementById(containerId);
        this.typeNum = typeNum;
    }

    // Called when user completes a task locally
    async completeTask(taskId, employeeId) {
        const date = new Date().toISOString().split('T')[0];

        // 1. Update UI immediately (optimistic update)
        this.updateTaskUI(taskId, 'completed', employeeId);

        // 2. Send to server
        const response = await fetch(`/api/${this.typeNum}/daybook/tasks/${taskId}/status/`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ status: 2, employeeId, date })
        });

        if (response.ok) {
            // 3. Broadcast to other clients via Ably
            window.daybookAbly.publishTaskComplete(taskId, employeeId, date);
        } else {
            // Rollback on failure
            this.updateTaskUI(taskId, 'pending', null);
        }
    }

    // Called when receiving Ably message from another client
    handleRemoteUpdate(update) {
        switch (update.type) {
            case 'complete':
                this.updateTaskUI(update.taskId, 'completed', update.employeeId);
                this.showToast(`Task completed by ${this.getEmployeeName(update.employeeId)}`);
                break;
            case 'uncomplete':
                this.updateTaskUI(update.taskId, 'pending', null);
                break;
            case 'progress':
                this.updateTaskUI(update.taskId, 'in_progress', update.employeeId);
                break;
            case 'comment':
                this.addCommentToTask(update.taskId, update.comment, update.employeeId);
                break;
        }
    }

    updateTaskUI(taskId, status, employeeId) {
        const taskEl = this.container.querySelector(`[data-task-id="${taskId}"]`);
        if (!taskEl) return;

        // Update checkbox
        const checkbox = taskEl.querySelector('.task-checkbox');
        checkbox.checked = (status === 'completed');

        // Update styling
        taskEl.classList.remove('task-pending', 'task-progress', 'task-completed');
        taskEl.classList.add(`task-${status === 'completed' ? 'completed' : status === 'in_progress' ? 'progress' : 'pending'}`);

        // Add animation for remote updates
        taskEl.classList.add('animate__animated', 'animate__pulse');
        setTimeout(() => {
            taskEl.classList.remove('animate__animated', 'animate__pulse');
        }, 1000);
    }
}
```

#### 6.3.7 Backend API with Ably Publishing

**Example: Task Status Update Endpoint:**
```php
// userfrosting/controllers/Daybook/TasksApiController.php

public function updateTaskStatus($typeNum, $taskId)
{
    $store = $this->getStore($typeNum);
    $data = json_decode($this->app->request->getBody(), true);

    $completion = new TaskCompletion($store);
    $completion->taskId = $taskId;
    $completion->date = $data['date'];
    $completion->status = $data['status'];
    $completion->completedBy = $data['employeeId'];

    if ($completion->save()) {
        // Publish to Ably for real-time sync
        $ably = new \BuyerKiosk\Daybook\DaybookAbly($typeNum);

        if ($data['status'] == 2) {
            $ably->taskCompleted($taskId, $data['employeeId'], $data['date']);
        } elseif ($data['status'] == 1) {
            $ably->publish('daybook:task:progress', [
                'taskId' => $taskId,
                'employeeId' => $data['employeeId'],
                'date' => $data['date']
            ]);
        } else {
            $ably->publish('daybook:task:uncomplete', [
                'taskId' => $taskId,
                'date' => $data['date']
            ]);
        }

        echo json_encode(['success' => true]);
    } else {
        $this->app->halt(500, json_encode(['error' => 'Failed to update task']));
    }
}
```

---

## 7. UI/UX Specifications

### 7.1 Layout (Desktop - Primary)

```
+------------------------------------------------------------------+
|  [Logo]     < TODAY >     [Date Picker]        [Employee Name] v  |
+------------------------------------------------------------------+
|                          |                                        |
|  --- Today's Tasks ---   |  [Whitney Thompson]  [Calendar Icon]   |
|  33% complete           |   Jul 13 - Edited    Schedule Aug 8th  |
|                          |                                        |
|  [ ] Task 1 (Overdue)    |   **CLEARANCE BACK**  Olivia  8:53-4pm |
|  [ ] Task 2 (Due Today)  |   IS GOING ON!        Emily   8:53-12pm|
|                          |                       Breana  8:58-4pm |
|  Show 1 completed        |   Trendy Brands:      ...              |
|                          |   - Zara                               |
|  Add a task              |   - Free People                        |
|                          |   ...                                  |
|  --- Rack Maintenance -- |                                        |
|  0%                      |   [+ Add Note Button]                  |
|                          |                                        |
|  [ ] Men's Athletic      |   --- Backstock Today ---              |
|  [ ] M Denim/button      |   3 bins pulled | 28 bins added        |
|  [ ] M Short Sleeves     |   [Category breakdown...]              |
|                          |                                        |
+------------------------------------------------------------------+
|  Sales      |  Avg Trans  |  Trade %  |  Transactions  | Labor   |
|  $7,684.68  |   $35.74    |  15.19%   |      215       |  --     |
|  Goal: $7,815 | Goal: $36.86 | Goal: 15% |              |         |
|  Comp: $7,236 | Comp: $36.55 | Comp: 18.42%|            |         |
+------------------------------------------------------------------+
```

### 7.2 Task List Features
- Collapsible task groups with completion percentage
- Color-coded status: Red (overdue), Yellow (due today), Normal
- Checkbox to mark complete (opens employee selector if not logged in as specific employee)
- Click task name to view details/comments
- "Add a task" quick-add inline
- Drag-and-drop reordering (admin only)
- Time-based list switching (Opening Tasks -> Mid-Day Tasks at scheduled time)
- Dropdown to view all lists for the day

### 7.3 Notes Feed Features
- Chronological feed with most recent at top (pinned notes always on top)
- Rich text support (bold, italic, lists, images)
- Start/end date visibility
- "Manager only" checkbox for sensitive notes
- Like button with count
- Expandable comments section
- Author avatar and timestamp
- Edit/delete for author or admin

### 7.4 Schedule Panel Features
- Today's date with navigation arrows
- Employee list with shift times
- Visual indicator for currently clocked in
- Click employee to see full week schedule (modal)
- Color coding for different positions (if applicable)

### 7.5 KPI Bar Features
- Configurable visible metrics (admin settings)
- Real-time updates every 5 minutes
- Goal comparison (green if meeting/exceeding, red if behind)
- Comp comparison to same day last year
- Click metric for detailed breakdown (modal)

### 7.6 Backstock Summary
- Count of bins pulled today
- Count of bins added today
- Top categories breakdown
- Click for detailed log

---

## 8. Collaborative Whiteboard System (Detailed)

The whiteboard is a core feature allowing employees to draw, sketch, and leave visual notes that persist throughout the day and sync in real-time across all devices viewing the daybook.

### 8.1 Technology Stack

**Canvas Library: [Fabric.js](http://fabricjs.com/)**
- Mature, well-documented HTML5 canvas library
- Built-in support for drawing, shapes, text, images
- JSON serialization/deserialization of canvas state
- Touch/stylus support for tablets
- Object manipulation (move, scale, rotate)

**Alternative: [Konva.js](https://konvajs.org/)** if Fabric.js proves problematic.

### 8.2 Whiteboard Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│                    Whiteboard Component                          │
├─────────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                    Tool Palette                          │   │
│  │  [✏️ Pen] [🖌️ Brush] [📝 Text] [📌 Sticky] [🗑️ Erase] [🧹 Clear] │   │
│  │  Color: [■■■■■■] Size: [━━━●━━] │ Undo/Redo │           │   │
│  └─────────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                          │   │
│  │                   Canvas Layer                           │   │
│  │              (Fabric.js Canvas)                          │   │
│  │                                                          │   │
│  │    ┌──────────┐                                         │   │
│  │    │ Sticky   │     ~~~~~~ Drawings ~~~~~~              │   │
│  │    │ Note     │          /\    /\                       │   │
│  │    └──────────┘         /  \  /  \                      │   │
│  │                                                          │   │
│  └─────────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  👤 Last edited by: John D. at 2:34 PM  │ 📅 Today      │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
```

### 8.3 Drawing Tools

| Tool | Description | Implementation |
|------|-------------|----------------|
| **Pen** | Freehand drawing with thin stroke | `fabric.PencilBrush` with width 2-5px |
| **Brush** | Freehand with thicker stroke | `fabric.PencilBrush` with width 10-30px |
| **Marker** | Semi-transparent highlight | `fabric.PencilBrush` with opacity 0.4 |
| **Text** | Click to add text box | `fabric.IText` object |
| **Sticky Note** | Draggable colored note | Custom `fabric.Group` (rect + text) |
| **Shapes** | Rectangle, circle, arrow | `fabric.Rect`, `fabric.Circle`, `fabric.Line` |
| **Eraser** | Remove drawn strokes | Remove objects or use `destination-out` composite |
| **Select** | Move/resize/rotate objects | Default Fabric.js selection |
| **Pan/Zoom** | Navigate large canvas | Canvas viewport manipulation |

### 8.4 Color Palette

```javascript
const WHITEBOARD_COLORS = [
    '#000000', // Black
    '#FF0000', // Red
    '#FF6B00', // Orange
    '#FFD700', // Yellow
    '#00AA00', // Green
    '#0066FF', // Blue
    '#8B00FF', // Purple
    '#FF69B4', // Pink
    '#FFFFFF', // White (for eraser effect on dark bg)
];

const STICKY_COLORS = [
    '#FFFF88', // Yellow
    '#88FF88', // Green
    '#88FFFF', // Cyan
    '#FF88FF', // Pink
    '#FFB366', // Orange
    '#B3B3FF', // Lavender
];
```

### 8.5 Canvas State Management

**Saving Strategy:**
```javascript
// Debounced save - triggers 2 seconds after last change
const saveCanvas = debounce(async () => {
    const canvasJSON = canvas.toJSON(['id', 'employeeId', 'createdAt']);
    const thumbnail = canvas.toDataURL({
        format: 'png',
        quality: 0.3,
        multiplier: 0.25
    });

    await fetch(`/api/${typeNum}/daybook/whiteboard/canvas/`, {
        method: 'POST',
        body: JSON.stringify({ canvasData: canvasJSON, thumbnail }),
        headers: { 'Content-Type': 'application/json' }
    });
}, 2000);

// Listen for all canvas modifications
canvas.on('object:modified', saveCanvas);
canvas.on('object:added', saveCanvas);
canvas.on('object:removed', saveCanvas);
canvas.on('path:created', saveCanvas);
```

**Loading Canvas:**
```javascript
async function loadCanvas() {
    const response = await fetch(`/api/${typeNum}/daybook/whiteboard/canvas/`);
    const { canvasData } = await response.json();

    if (canvasData) {
        canvas.loadFromJSON(canvasData, () => {
            canvas.renderAll();
        });
    }
}
```

### 8.6 Real-time Collaboration via Ably

```javascript
// Ably channel for whiteboard
const whiteboardChannel = ably.channels.get(`${typeNum}-daybook-whiteboard`);

// Broadcast drawing actions
canvas.on('path:created', (e) => {
    whiteboardChannel.publish('draw', {
        employeeId: currentEmployeeId,
        object: e.path.toJSON(),
        timestamp: Date.now()
    });
});

// Receive and render others' drawings
whiteboardChannel.subscribe('draw', (message) => {
    if (message.data.employeeId !== currentEmployeeId) {
        fabric.util.enlivenObjects([message.data.object], (objects) => {
            objects.forEach(obj => {
                obj.set({ selectable: false }); // Can't edit others' drawings
                canvas.add(obj);
            });
            canvas.renderAll();
        });
    }
});

// Sync sticky notes
whiteboardChannel.subscribe('sticky:add', handleRemoteStickyAdd);
whiteboardChannel.subscribe('sticky:move', handleRemoteStickyMove);
whiteboardChannel.subscribe('sticky:delete', handleRemoteStickyDelete);

// Canvas cleared notification
whiteboardChannel.subscribe('canvas:clear', () => {
    canvas.clear();
    showToast('Canvas was cleared by another user');
});
```

### 8.7 Sticky Notes Implementation

```javascript
class StickyNote {
    constructor(options) {
        const rect = new fabric.Rect({
            width: options.width || 200,
            height: options.height || 150,
            fill: options.backgroundColor || '#FFFF88',
            rx: 5, ry: 5,
            shadow: new fabric.Shadow({
                color: 'rgba(0,0,0,0.3)',
                blur: 10,
                offsetX: 5,
                offsetY: 5
            })
        });

        const text = new fabric.IText(options.content || 'Click to edit...', {
            fontSize: options.fontSize || 14,
            fill: options.textColor || '#000000',
            width: options.width - 20,
            left: 10,
            top: 10,
            editable: true
        });

        this.group = new fabric.Group([rect, text], {
            left: options.positionX || 100,
            top: options.positionY || 100,
            id: options.id || generateUUID(),
            employeeId: options.employeeId,
            itemType: 'sticky',
            hasControls: true,
            hasBorders: true
        });
    }
}
```

### 8.8 Touch/Stylus Support

```javascript
// Optimize for tablet/touch input
canvas.allowTouchScrolling = false;
canvas.isDrawingMode = true;

// Pressure sensitivity (if supported)
canvas.freeDrawingBrush.width = 3;
if (window.PointerEvent) {
    canvas.on('mouse:move', (e) => {
        if (e.e.pressure && e.e.pressure > 0) {
            canvas.freeDrawingBrush.width = e.e.pressure * 10;
        }
    });
}

// Prevent accidental gestures
document.addEventListener('touchmove', (e) => {
    if (e.target === canvas.upperCanvasEl) {
        e.preventDefault();
    }
}, { passive: false });
```

### 8.9 Whiteboard UI Mockup

```
┌────────────────────────────────────────────────────────────────────────┐
│  WHITEBOARD                                              [Minimize ▼]  │
├────────────────────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────────────────────┐  │
│ │ ✏️  🖌️  📝  📌  ⬜  ⭕  ➡️  │  🎨[●]  📏[━━●━]  │  ↩️  ↪️  │  🗑️  │  │
│ └──────────────────────────────────────────────────────────────────┘  │
│ ┌──────────────────────────────────────────────────────────────────┐  │
│ │                                                                   │  │
│ │     ┌─────────────┐                                              │  │
│ │     │ REMEMBER!   │                                              │  │
│ │     │ Check floor │     ∿∿∿∿∿∿∿                                 │  │
│ │     │ every hour  │    /        \                                │  │
│ │     └─────────────┘   │  SALE!  │                                │  │
│ │                        \        /                                │  │
│ │   ╭──────────────╮      ∿∿∿∿∿∿∿       ★ Great job today! ★      │  │
│ │   │ Lulu Drop    │                                               │  │
│ │   │ starts Mon!  │         ────────→                             │  │
│ │   ╰──────────────╯        Check bin 47                           │  │
│ │                                                                   │  │
│ └──────────────────────────────────────────────────────────────────┘  │
│                        Last edited by Sarah M. at 3:45 PM              │
└────────────────────────────────────────────────────────────────────────┘
```

### 8.10 Whiteboard JavaScript Module

```javascript
// public_html/view/common/js/daybook/whiteboard.js

class DaybookWhiteboard {
    constructor(containerId, options = {}) {
        this.container = document.getElementById(containerId);
        this.typeNum = options.typeNum;
        this.employeeId = options.employeeId;
        this.ablyChannel = options.ablyChannel;

        this.canvas = null;
        this.currentTool = 'pen';
        this.currentColor = '#000000';
        this.brushSize = 3;
        this.isDrawing = false;

        this.init();
    }

    async init() {
        this.createCanvasElement();
        this.initFabricCanvas();
        this.createToolbar();
        this.bindEvents();
        this.setupAblySync();
        await this.loadExistingCanvas();
    }

    createCanvasElement() {
        this.container.innerHTML = `
            <div class="whiteboard-wrapper">
                <div class="whiteboard-toolbar" id="wb-toolbar"></div>
                <div class="whiteboard-canvas-container">
                    <canvas id="whiteboard-canvas"></canvas>
                </div>
                <div class="whiteboard-status">
                    <span id="wb-last-edit"></span>
                </div>
            </div>
        `;
    }

    initFabricCanvas() {
        this.canvas = new fabric.Canvas('whiteboard-canvas', {
            width: this.container.offsetWidth - 20,
            height: 400,
            backgroundColor: '#FFFFFF',
            isDrawingMode: true,
            selection: true
        });

        this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas);
        this.canvas.freeDrawingBrush.color = this.currentColor;
        this.canvas.freeDrawingBrush.width = this.brushSize;
    }

    setTool(tool) {
        this.currentTool = tool;

        switch(tool) {
            case 'pen':
            case 'brush':
            case 'marker':
                this.canvas.isDrawingMode = true;
                this.canvas.freeDrawingBrush.width =
                    tool === 'pen' ? 2 : tool === 'brush' ? 15 : 8;
                if (tool === 'marker') {
                    this.canvas.freeDrawingBrush.color =
                        this.hexToRgba(this.currentColor, 0.4);
                }
                break;
            case 'select':
                this.canvas.isDrawingMode = false;
                break;
            case 'text':
                this.canvas.isDrawingMode = false;
                this.addTextMode();
                break;
            case 'sticky':
                this.canvas.isDrawingMode = false;
                this.addStickyNote();
                break;
            case 'eraser':
                this.canvas.isDrawingMode = true;
                this.canvas.freeDrawingBrush.color = '#FFFFFF';
                this.canvas.freeDrawingBrush.width = 20;
                break;
        }
    }

    addStickyNote(options = {}) {
        const sticky = new StickyNote({
            positionX: options.x || 100,
            positionY: options.y || 100,
            employeeId: this.employeeId,
            backgroundColor: options.color || '#FFFF88',
            content: options.content || ''
        });

        this.canvas.add(sticky.group);
        this.canvas.setActiveObject(sticky.group);
        this.canvas.renderAll();

        // Broadcast to other clients
        this.ablyChannel.publish('sticky:add', {
            id: sticky.group.id,
            data: sticky.group.toJSON()
        });
    }

    clearCanvas() {
        if (confirm('Clear the entire whiteboard? This cannot be undone.')) {
            this.canvas.clear();
            this.canvas.backgroundColor = '#FFFFFF';
            this.canvas.renderAll();
            this.saveCanvas();
            this.ablyChannel.publish('canvas:clear', {
                employeeId: this.employeeId
            });
        }
    }

    async saveCanvas() {
        const canvasData = this.canvas.toJSON([
            'id', 'employeeId', 'itemType', 'createdAt'
        ]);
        const thumbnail = this.canvas.toDataURL({
            format: 'png',
            quality: 0.3,
            multiplier: 0.2
        });

        await fetch(`/api/${this.typeNum}/daybook/whiteboard/canvas/`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ canvasData, thumbnail })
        });
    }

    async loadExistingCanvas() {
        try {
            const response = await fetch(
                `/api/${this.typeNum}/daybook/whiteboard/canvas/`
            );
            const data = await response.json();

            if (data.canvasData) {
                this.canvas.loadFromJSON(data.canvasData, () => {
                    this.canvas.renderAll();
                });
            }

            if (data.lastModifiedBy && data.lastModifiedAt) {
                this.updateLastEditStatus(
                    data.lastModifiedBy,
                    data.lastModifiedAt
                );
            }
        } catch (err) {
            console.error('Failed to load whiteboard:', err);
        }
    }

    setupAblySync() {
        // Receive drawing from others
        this.ablyChannel.subscribe('draw', (msg) => {
            if (msg.data.employeeId !== this.employeeId) {
                fabric.util.enlivenObjects([msg.data.object], (objects) => {
                    objects.forEach(obj => {
                        obj.selectable = false;
                        this.canvas.add(obj);
                    });
                    this.canvas.renderAll();
                });
            }
        });

        // Receive sticky note updates
        this.ablyChannel.subscribe('sticky:add', (msg) => {
            if (msg.data.employeeId !== this.employeeId) {
                fabric.util.enlivenObjects([msg.data.data], (objects) => {
                    this.canvas.add(objects[0]);
                    this.canvas.renderAll();
                });
            }
        });

        // Canvas cleared by another user
        this.ablyChannel.subscribe('canvas:clear', (msg) => {
            if (msg.data.employeeId !== this.employeeId) {
                this.canvas.clear();
                this.canvas.backgroundColor = '#FFFFFF';
                this.canvas.renderAll();
                this.showNotification('Whiteboard cleared by another user');
            }
        });
    }
}

// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
    if (document.getElementById('whiteboard-container')) {
        window.daybookWhiteboard = new DaybookWhiteboard('whiteboard-container', {
            typeNum: STORE_TYPE_NUM,
            employeeId: CURRENT_EMPLOYEE_ID,
            ablyChannel: ablyClient.channels.get(`${STORE_TYPE_NUM}-daybook-whiteboard`)
        });
    }
});
```

### 8.11 Whiteboard CSS

```css
/* public_html/view/common/css/daybook/whiteboard.css */

.whiteboard-wrapper {
    border: 1px solid #ddd;
    border-radius: 8px;
    overflow: hidden;
    background: #f5f5f5;
}

.whiteboard-toolbar {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    background: #fff;
    border-bottom: 1px solid #ddd;
    flex-wrap: wrap;
}

.whiteboard-toolbar .tool-group {
    display: flex;
    gap: 4px;
    padding-right: 12px;
    border-right: 1px solid #eee;
}

.whiteboard-toolbar button {
    width: 36px;
    height: 36px;
    border: 1px solid #ddd;
    border-radius: 4px;
    background: #fff;
    cursor: pointer;
    font-size: 18px;
    transition: all 0.2s;
}

.whiteboard-toolbar button:hover {
    background: #f0f0f0;
}

.whiteboard-toolbar button.active {
    background: #e3f2fd;
    border-color: #2196f3;
}

.whiteboard-toolbar .color-picker {
    display: flex;
    gap: 4px;
}

.whiteboard-toolbar .color-swatch {
    width: 24px;
    height: 24px;
    border-radius: 50%;
    border: 2px solid transparent;
    cursor: pointer;
}

.whiteboard-toolbar .color-swatch.active {
    border-color: #333;
}

.whiteboard-toolbar .brush-size {
    width: 100px;
}

.whiteboard-canvas-container {
    position: relative;
    background: #fff;
    min-height: 400px;
}

.whiteboard-canvas-container canvas {
    display: block;
}

.whiteboard-status {
    padding: 8px 12px;
    background: #fafafa;
    border-top: 1px solid #ddd;
    font-size: 12px;
    color: #666;
}

/* Sticky note styles */
.sticky-note-popup {
    position: fixed;
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.15);
    padding: 16px;
    z-index: 1000;
}

.sticky-color-picker {
    display: flex;
    gap: 8px;
    margin-bottom: 12px;
}

.sticky-color-picker .color-option {
    width: 32px;
    height: 32px;
    border-radius: 4px;
    cursor: pointer;
    border: 2px solid transparent;
}

.sticky-color-picker .color-option.selected {
    border-color: #333;
}
```

---

## 9. Integration Points

### 9.1 WhenIWork Schedule Extension

Extend `EmployeesController.php`:
```php
public function getScheduleForDate(\DateTime $date): array
{
    $wiw = new \Wheniwork($this->store->getWiwToken());
    $start = clone $date;
    $start->setTime(0, 0, 0);
    $end = clone $date;
    $end->setTime(23, 59, 59);

    $result = $wiw->get("shifts", [
        "location_id" => $this->store->getWiwLocationID(),
        "start" => $start->format("Y-m-d H:i:s"),
        "end" => $end->format("Y-m-d H:i:s")
    ]);

    return $this->formatScheduleResponse($result->shifts ?? []);
}
```

### 9.2 Homebase Integration (Future)

```php
namespace BuyerKiosk\Daybook;

class HomebaseSchedule extends ScheduleProvider
{
    private $apiKey;
    private $locationId;

    public function __construct(\Store $store)
    {
        $this->store = $store;
        $this->apiKey = $store->getHomebaseApiKey();
        $this->locationId = $store->getHomebaseLocationId();
    }

    public function getScheduleForDate(\DateTime $date): array
    {
        // Homebase API implementation
        // Reference: https://developer.joinhomebase.com/
    }
}
```

### 9.3 KPI Data Sources

The Daybook leverages the **existing LiveFinancials system** which already provides real-time POS data integration. This is NOT a future enhancement - it's already implemented and in production.

#### 9.3.1 Existing LiveFinancials Integration

The POS system (DRS - Daily Report System) already sends live financial data to the web application via API endpoints in `userfrosting/routes/groups/drs.php`:

**API Endpoints:**
- `GET /:typeNum/live/financials/:api` - Retrieve current day financials
- `POST /:typeNum/live/financials` - Update live financials from POS

**LiveFinancials Table Schema (per-store database):**
```sql
CREATE TABLE `LiveFinancials` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `date` date NOT NULL,
  `buysGoal` decimal(10,2) DEFAULT 0.00,
  `buysCurrent` decimal(10,2) DEFAULT 0.00,
  `salesGoal` decimal(10,2) DEFAULT 0.00,
  `salesCurrent` decimal(10,2) DEFAULT 0.00,
  `buysOutstanding` decimal(10,2) DEFAULT 0.00,
  PRIMARY KEY (`id`),
  KEY `date` (`date`)
);
```

**Data Flow:**
1. POS system sends JSON payload with goals and current figures
2. `drs.php` route validates API key and updates/inserts `LiveFinancials` record
3. If WhenIWork is enabled, `FinancialsController` is called to get labor data
4. Combined response includes both sales data and labor metrics

**FinancialsController Integration** (`userfrosting/controllers/WhenIWork/FinancialsController.php`):
```php
// Already combines LiveFinancials with WhenIWork labor data
$fc = new \BuyerKiosk\WhenIWork\FinancialsController($app, $store);
$laborTotals = $fc->getLaborTotals();
// Returns: hr, dollars, salesCurrent, laborPercent, salesPerLaborHour, employeeData
```

**Existing Response Format:**
```json
{
  "bGoal": "$500.00",
  "bCurrent": "$250.00",
  "sGoal": "$2,000.00",
  "sCurrent": "$1,500.00",
  "bOutstanding": "$100.00",
  "laborHours": 24.5,
  "laborDollars": 280.75,
  "laborPercent": "18%",
  "isFromCache": 0
}
```

#### 9.3.2 Daily Report Data (closeSalesReport)

The `SalesReport.php` model (`userfrosting/models/Class/DailyReport/SalesReport.php`) captures comprehensive end-of-day data from the POS:

**Available Fields:**
- `grossSalesRetail`, `grossSalesCost`, `grossSalesNumber`, `grossSalesGM`
- `returnsRetail`, `returnsCost`, `returnsNumber`, `returnsGM`
- `netSalesRetail`, `netSalesCost`, `netSalesGM`
- `buysCost`, `buysRetail`, `buysGM`, `buysCount`
- `averageRetail`, `salesCount`
- `tradeCount`, `tradeAmount`
- `laborPercentage`
- `cashPaidIn`, `cashPaidOut`

#### 9.3.3 KPI Source Mapping

| KPI | Source | Table/API |
|-----|--------|-----------|
| Sales Current | LiveFinancials (real-time) | `LiveFinancials.salesCurrent` |
| Sales Goal | LiveFinancials | `LiveFinancials.salesGoal` |
| Buys Current | LiveFinancials (real-time) | `LiveFinancials.buysCurrent` |
| Buys Goal | LiveFinancials | `LiveFinancials.buysGoal` |
| Buys Outstanding | LiveFinancials | `LiveFinancials.buysOutstanding` |
| Avg Transaction | Calculated | `salesCurrent / transactionCount` |
| Trade % | Calculated | `buysCurrent / salesCurrent` |
| Labor Hours | WhenIWork + FinancialsController | Redis cache `{typeNum}_laborTotals` |
| Labor $ | WhenIWork + FinancialsController | Cached with 5-min TTL |
| Labor % | FinancialsController | `laborDollars / salesCurrent` |
| Sales/Labor Hour | FinancialsController | `salesCurrent / totalHours` |
| Buys Count | buyQueue table | `SELECT COUNT(*) FROM buyQueue WHERE isProcessed=1 AND DATE(timeCompleted)=TODAY` |
| Backstock | bsActions table | `SELECT COUNT(*) FROM bsActions WHERE DATE(timestamp)=TODAY` |
| Comps (YoY) | closeSalesReport | Same date previous year |

#### 9.3.4 Daybook KPI Service

For the Daybook, create a unified KPI service that leverages these existing integrations:

```php
<?php
namespace BuyerKiosk\Daybook;

class KPIService {
    private $store;
    private $storeDB;
    private $predis;

    public function __construct(\Store $store) {
        $this->store = $store;
        $this->storeDB = dbConnectByName($store->getDbName());
        $this->predis = new \Predis\Client($_ENV['REDIS_URL']);
    }

    /**
     * Get all KPIs for today, combining existing data sources
     */
    public function getTodayKPIs(): array {
        $today = new \DateTime('now', new \DateTimeZone($this->store->getTimeZone()));
        $cacheKey = $this->store->getTypeNum() . '_daybook_kpis_' . $today->format('Y-m-d');

        // Check cache (30 second TTL for real-time feel)
        if ($cached = $this->predis->get($cacheKey)) {
            return json_decode($cached, true);
        }

        $kpis = [
            'financials' => $this->getLiveFinancials($today),
            'labor' => $this->getLaborData(),
            'buys' => $this->getBuysData($today),
            'backstock' => $this->getBackstockData($today),
            'comparisons' => $this->getYearOverYearComps($today)
        ];

        $this->predis->setex($cacheKey, 30, json_encode($kpis));
        return $kpis;
    }

    private function getLiveFinancials(\DateTime $date): array {
        $stmt = $this->storeDB->prepare(
            "SELECT * FROM LiveFinancials WHERE date = :date"
        );
        $stmt->bindValue(':date', $date->format('Y-m-d'));
        $stmt->execute();

        if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
            return [
                'salesGoal' => (float)$row['salesGoal'],
                'salesCurrent' => (float)$row['salesCurrent'],
                'buysGoal' => (float)$row['buysGoal'],
                'buysCurrent' => (float)$row['buysCurrent'],
                'buysOutstanding' => (float)$row['buysOutstanding'],
                'salesProgress' => $row['salesGoal'] > 0
                    ? round(($row['salesCurrent'] / $row['salesGoal']) * 100, 1)
                    : 0,
                'buysProgress' => $row['buysGoal'] > 0
                    ? round(($row['buysCurrent'] / $row['buysGoal']) * 100, 1)
                    : 0
            ];
        }

        return $this->getDefaultFinancials();
    }

    private function getLaborData(): array {
        if ($this->store->getWiwEnable() > 0) {
            // Leverage existing FinancialsController
            $fc = new \BuyerKiosk\WhenIWork\FinancialsController(
                $GLOBALS['app'],
                $this->store
            );
            $laborTotals = $fc->getLaborTotals();

            return [
                'hours' => $laborTotals['hr'],
                'dollars' => $laborTotals['dollars'],
                'laborPercent' => $laborTotals['laborPercent'],
                'salesPerLaborHour' => $laborTotals['salesPerLaborHour'],
                'isFromCache' => $fc->isFromCache,
                'employees' => $fc->employeeDataArray ?? []
            ];
        }

        return ['enabled' => false];
    }

    private function getBuysData(\DateTime $date): array {
        // Count completed buys today
        $stmt = $this->storeDB->prepare(
            "SELECT COUNT(*) as count FROM buyQueue
             WHERE isProcessed = 1
             AND DATE(timeCompleted) = :date"
        );
        $stmt->bindValue(':date', $date->format('Y-m-d'));
        $stmt->execute();
        $row = $stmt->fetch(\PDO::FETCH_ASSOC);

        return [
            'completedCount' => (int)$row['count']
        ];
    }

    private function getBackstockData(\DateTime $date): array {
        $stmt = $this->storeDB->prepare(
            "SELECT actionType, COUNT(*) as count
             FROM bsActions
             WHERE DATE(timestamp) = :date
             GROUP BY actionType"
        );
        $stmt->bindValue(':date', $date->format('Y-m-d'));
        $stmt->execute();

        $data = ['added' => 0, 'pulled' => 0, 'emptied' => 0];
        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
            switch ($row['actionType']) {
                case 'add': $data['added'] = (int)$row['count']; break;
                case 'remove': $data['pulled'] = (int)$row['count']; break;
                case 'empty': $data['emptied'] = (int)$row['count']; break;
            }
        }

        return $data;
    }

    private function getYearOverYearComps(\DateTime $date): array {
        $lastYear = clone $date;
        $lastYear->modify('-1 year');

        $stmt = $this->storeDB->prepare(
            "SELECT netSalesRetail, buysCost FROM closeSalesReport
             WHERE date = :date"
        );
        $stmt->bindValue(':date', $lastYear->format('Y-m-d'));
        $stmt->execute();

        if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
            return [
                'lastYearSales' => (float)$row['netSalesRetail'],
                'lastYearBuys' => (float)$row['buysCost'],
                'available' => true
            ];
        }

        return ['available' => false];
    }

    private function getDefaultFinancials(): array {
        return [
            'salesGoal' => 0,
            'salesCurrent' => 0,
            'buysGoal' => 0,
            'buysCurrent' => 0,
            'buysOutstanding' => 0,
            'salesProgress' => 0,
            'buysProgress' => 0
        ];
    }
}
```

---

## 10. Permissions

### 10.1 New Permission Keys

```php
// Add to permission system
'daybook_view'           => 'Can view daybook dashboard',
'daybook_complete_tasks' => 'Can complete tasks',
'daybook_create_notes'   => 'Can create notes (manager+)',
'daybook_view_manager_notes' => 'Can view manager-only notes',
'daybook_manage_tasks'   => 'Can create/edit/delete tasks (admin)',
'daybook_manage_kpi'     => 'Can configure KPI visibility (owner)',
'daybook_whiteboard'     => 'Can use whiteboard features',
```

### 10.2 Role Defaults

| Permission | Employee | Shift Lead | Manager | Owner |
|------------|----------|------------|---------|-------|
| daybook_view | Yes | Yes | Yes | Yes |
| daybook_complete_tasks | Yes | Yes | Yes | Yes |
| daybook_create_notes | No | No | Yes | Yes |
| daybook_view_manager_notes | No | No | Yes | Yes |
| daybook_manage_tasks | No | No | Yes | Yes |
| daybook_manage_kpi | No | No | No | Yes |
| daybook_whiteboard | Yes | Yes | Yes | Yes |

---

## 11. Migration Plan

### 11.1 Phase 1: Database & Backend
1. Create new database tables
2. Build PHP model classes
3. Implement API endpoints
4. Extend WhenIWork integration for schedules

### 11.2 Phase 2: Frontend Core
1. Create daybook template
2. Implement task list UI
3. Implement notes feed
4. Implement schedule panel
5. Implement KPI bar

### 11.3 Phase 3: Real-time & Polish
1. Integrate Ably for real-time updates
2. Implement whiteboard features
3. Employee selector component
4. Mobile responsiveness
5. Testing and bug fixes

### 11.4 Phase 4: Admin Features
1. Task list scheduling admin
2. KPI configuration admin
3. Homebase integration groundwork

### 11.5 Phase 5: Rollout
1. Beta testing with select stores
2. Gradual rollout
3. Make daybook default landing page

---

## 12. Cron Jobs

```bash
# Clean up expired whiteboard items (every hour)
0 * * * * php /path/to/userfrosting/scripts/daybook-cleanup.php

# Refresh schedule cache (every 15 minutes during business hours)
*/15 6-22 * * * php /path/to/userfrosting/scripts/daybook-schedule-sync.php

# Pre-calculate KPIs (every 5 minutes during business hours)
*/5 6-22 * * * php /path/to/userfrosting/scripts/daybook-kpi-cache.php
```

---

## 13. Redis Cache Keys

```
{typeNum}_daybook_schedule_{date}      # Schedule for date (TTL: 5 min)
{typeNum}_daybook_kpi_{date}           # KPI data for date (TTL: 5 min)
{typeNum}_daybook_tasks_{date}         # Task lists for date (TTL: 1 min)
{typeNum}_daybook_notes_{date}         # Notes for date (TTL: 1 min)
{typeNum}_daybook_whiteboard           # Active whiteboard items (TTL: 1 min)
{typeNum}_daybook_backstock_{date}     # Backstock summary (TTL: 5 min)
```

---

## 14. Open Questions / Decisions Needed

1. **Employee Identification**: Since employees aren't logged in individually, how should we track who completes tasks or adds comments?
   - Option A: Employee selector popup each time
   - Option B: Session-based "I am [Employee]" selection at start of shift
   - Option C: Require individual login for daybook features

2. **Whiteboard Persistence**: How long should whiteboard items persist by default?
   - Suggested: 24 hours with option to extend

3. **Schedule Provider Priority**: If both WhenIWork and Homebase are configured, which takes precedence?
   - Suggested: Store-level setting to choose primary provider

4. **KPI Refresh Frequency**: How often should KPIs update on the dashboard?
   - Suggested: Every 5 minutes, with manual refresh button

5. **Mobile Support**: Is this primarily for tablet/desktop, or do we need full mobile support?
   - Suggested: Responsive design, optimized for tablet

---

## 15. Legacy Frontend Refactoring (CRITICAL)

**⚠️ IMPORTANT: This is the most critical piece of BuyerKiosk software. All functionality and look/feel MUST remain exactly the same. This refactoring modernizes the codebase to integrate with Daybook while preserving every feature.**

### 15.1 Current Legacy System Analysis

#### 15.1.1 Files to Refactor

| File | Type | Location | Purpose |
|------|------|----------|---------|
| `queue.html` | Template | `userfrosting/templates/themes/default/` | Main buy queue display |
| `printLog.html` | Template | `userfrosting/templates/themes/default/` | Daily transaction log |
| `completedBuys.html` | Template | `userfrosting/templates/themes/default/` | Completed transactions view |
| `buyQueue.js` | JavaScript | `public_html/view/common/js/` | Queue logic, Ably, UI (~800 lines) |
| `functions.js` | JavaScript | `public_html/view/common/js/` | Utility functions |
| `completedBuys.js` | JavaScript | `public_html/view/common/js/` | Completed buys logic |
| `styles.css` | Stylesheet | `public_html/view/common/css/` | Common styles |
| `frontHead.html` | Component | `userfrosting/templates/themes/default/components/` | Header include |
| `frontFoot.html` | Component | `userfrosting/templates/themes/default/components/` | Footer include |

#### 15.1.2 Current Architecture Problems

1. **Inline JavaScript**: `queue.html` has ~50 lines inline JS, `printLog.html` has ~150 lines
2. **Script Loading**: Scripts loaded via CDN + local mix, no bundling
3. **Global Scope Pollution**: `buyQueue.js` defines many global functions
4. **Hardcoded Ably Key**: Ably API key visible in client-side code
5. **No Module System**: All JS relies on global `$` and function hoisting
6. **Mixed Concerns**: UI rendering, API calls, and Ably sync all in one file
7. **No Error Boundaries**: Failures cascade without graceful degradation
8. **Legacy jQuery Patterns**: `$.ajax` with `async: false` (blocking)

#### 15.1.3 Current Feature Inventory (MUST PRESERVE)

**Queue Page (`queue.html` + `buyQueue.js`):**
- [ ] Real-time queue display with 5 categories: Processing, Sorted, Sorting, Queued, Remote
- [ ] Ably real-time sync for all queue actions
- [ ] Enhanced View toggle (panel/classic views)
- [ ] Wait time display and adjustment buttons
- [ ] Floating action button (FAB) with Add Buy, Clock In, Schedule
- [ ] Add Buy modal with 4-step wizard
- [ ] Customer lookup by phone number
- [ ] SMS link sending
- [ ] Customer information form with validation (Parsley.js)
- [ ] State selector defaulting to store state
- [ ] Container count selection
- [ ] Return time picker (DateTimePicker)
- [ ] Queue item actions: Start Buy, Check In, Delete, Add/Remove Container
- [ ] Color-coded time indicators (yellow/red based on store settings)
- [ ] Search functionality within panels
- [ ] Collapsible sections with counts
- [ ] Animations (animate.css) for queue changes
- [ ] Customer flags and recent buys indicators
- [ ] Reamaze chat widget integration
- [ ] Clock display (real-time)
- [ ] UUID generation for cache busting

**Print Log (`printLog.html`):**
- [ ] Date picker for historical logs
- [ ] Transaction table with: Buy #, Name, Bins, Drop Off, Sorter, Buyer, SD, ST, PT, PT/C
- [ ] Resend SMS functionality
- [ ] Print-optimized layout (screen/print CSS)
- [ ] Today's Buyers/Sorters summary
- [ ] Buyer Stats table
- [ ] Sorter Stats table
- [ ] Store averages row
- [ ] Previous day indicator (**)
- [ ] DataTables integration

**Completed Buys (`completedBuys.html`):**
- [ ] Today's completed transactions table
- [ ] Check-out button functionality
- [ ] Checked-out row styling
- [ ] Previous day indicator (**)
- [ ] DataTables with sorting/filtering

---

### 15.2 Refactoring Strategy

#### 15.2.1 Guiding Principles

1. **Zero Visual Changes**: Every pixel must remain identical
2. **Zero Behavioral Changes**: Every interaction must work exactly as before
3. **Incremental Migration**: Refactor in small, testable steps
4. **Feature Flags**: Toggle between old and new code paths
5. **Comprehensive Testing**: Side-by-side comparison testing
6. **Rollback Ready**: Ability to revert at any point

#### 15.2.2 New Directory Structure

```
userfrosting/
├── templates/themes/default/
│   ├── workspace/                    # NEW: Refactored workspace templates
│   │   ├── workspace.html            # Combined container for queue + daybook
│   │   ├── partials/
│   │   │   ├── queue/
│   │   │   │   ├── queue-classic.html    # Classic queue view
│   │   │   │   ├── queue-enhanced.html   # Enhanced panel view
│   │   │   │   ├── queue-item.html       # Single queue item partial
│   │   │   │   ├── queue-controls.html   # Wait time, view toggle
│   │   │   │   └── queue-fab.html        # Floating action button
│   │   │   ├── modals/
│   │   │   │   ├── add-buy-modal.html    # Add buy wizard
│   │   │   │   ├── clock-in-modal.html   # Clock in
│   │   │   │   └── schedule-modal.html   # Schedule view
│   │   │   ├── print-log/
│   │   │   │   ├── print-log.html        # Main print log
│   │   │   │   ├── log-table.html        # Transaction table
│   │   │   │   └── stats-tables.html     # Buyer/Sorter stats
│   │   │   └── completed/
│   │   │       └── completed-buys.html   # Completed view
│   │   └── layouts/
│   │       ├── workspace-head.html       # Refactored head with module loading
│   │       └── workspace-foot.html       # Refactored foot
│   │
│   └── queue.html                    # KEEP: Original (deprecated, for rollback)
│   └── printLog.html                 # KEEP: Original (deprecated, for rollback)
│   └── completedBuys.html            # KEEP: Original (deprecated, for rollback)

public_html/
├── js/
│   └── workspace/                    # NEW: Refactored JavaScript modules
│       ├── workspace.js              # Main entry point
│       ├── modules/
│       │   ├── queue/
│       │   │   ├── QueueManager.js       # Queue state management
│       │   │   ├── QueueRenderer.js      # DOM rendering
│       │   │   ├── QueueActions.js       # User interactions
│       │   │   ├── QueueAPI.js           # API calls
│       │   │   └── QueueAbly.js          # Real-time sync
│       │   ├── print-log/
│       │   │   ├── PrintLogManager.js    # Print log logic
│       │   │   └── PrintLogAPI.js        # API calls
│       │   ├── completed/
│       │   │   └── CompletedManager.js   # Completed buys logic
│       │   ├── common/
│       │   │   ├── utils.js              # Utility functions
│       │   │   ├── time.js               # Time formatting
│       │   │   ├── api.js                # Base API helper
│       │   │   └── ably-client.js        # Shared Ably client
│       │   └── daybook/                  # (Daybook modules from earlier spec)
│       │       └── ...
│       └── vendor/                   # Third-party (managed)
│           ├── jquery-3.6.0.min.js
│           ├── bootstrap.bundle.min.js
│           └── ...
│
└── css/
    └── workspace/                    # NEW: Refactored stylesheets
        ├── workspace.css             # Main compiled stylesheet
        ├── modules/
        │   ├── _queue.scss           # Queue styles (SCSS)
        │   ├── _print-log.scss       # Print log styles
        │   ├── _completed.scss       # Completed buys styles
        │   ├── _daybook.scss         # Daybook styles
        │   └── _common.scss          # Shared styles
        └── themes/
            ├── _buyerkiosk.scss      # BuyerKiosk brand
            ├── _platoscloset.scss    # Plato's Closet brand
            └── ...
```

---

### 15.3 JavaScript Module Refactoring

#### 15.3.1 Core Module: QueueManager.js

```javascript
/**
 * QueueManager - Central state management for buy queue
 * Replaces global variables and scattered state in buyQueue.js
 */
class QueueManager {
    constructor(options) {
        this.typeNum = options.typeNum;
        this.storeType = options.storeType;
        this.uuid = this.generateUUID();

        // Queue state (replaces globalQueueArrays)
        this.state = {
            processing: [],
            sorted: [],
            sorting: [],
            queued: [],
            remote: []
        };

        // Store settings
        this.settings = {
            yellowTime: options.yellowTime || 30,
            redTime: options.redTime || 60,
            enableQueueSignIn: options.enableQueueSignIn || 0,
            waitTime: options.waitTime || 5
        };

        // View state
        this.viewMode = localStorage.getItem('queueViewMode') || 'classic';

        // Dependencies (injected)
        this.renderer = null;
        this.api = null;
        this.ably = null;
    }

    /**
     * Initialize the queue manager
     */
    async init() {
        try {
            await this.loadInitialQueue();
            this.startTimers();
            return true;
        } catch (error) {
            console.error('[QueueManager] Init failed:', error);
            this.renderer.showError('Failed to load queue. Please refresh the page.');
            return false;
        }
    }

    /**
     * Load initial queue data from API
     */
    async loadInitialQueue() {
        const data = await this.api.getQueue(this.typeNum, this.uuid);

        if (data.error) {
            throw new Error(data.error);
        }

        // Sort by timeEntered (oldest first)
        data.sort((a, b) => new Date(a.timeEntered) - new Date(b.timeEntered));

        // Categorize items (exact logic from buyQueue.js)
        this.categorizeItems(data);

        // Render initial state
        this.renderer.renderAll(this.state);
    }

    /**
     * Categorize queue items into buckets
     * Preserves exact logic from buyQueue.js lines 88-114
     */
    categorizeItems(items) {
        // Reset state
        this.state = {
            processing: [],
            sorted: [],
            sorting: [],
            queued: [],
            remote: []
        };

        items.forEach(item => {
            // Normalize buyId/buyID (from buyQueue.js lines 96-102)
            if (item.buyID && !item.buyId) item.buyId = item.buyID;
            if (item.buyId && !item.buyID) item.buyID = item.buyId;

            // Categorize (exact logic from buyQueue.js lines 104-114)
            if (item.timeStarted !== "0000-00-00 00:00:00") {
                this.state.processing.push(item);
            } else if (item.sortCompleted !== "0000-00-00 00:00:00") {
                this.state.sorted.push(item);
            } else if (item.sortStarted !== "0000-00-00 00:00:00") {
                this.state.sorting.push(item);
            } else if (item.hasDroppedOff == 1 || item.remote == 0) {
                this.state.queued.push(item);
            } else {
                this.state.remote.push(item);
            }
        });
    }

    /**
     * Add item to appropriate category
     */
    addItem(item) {
        // Normalize
        if (item.buyID && !item.buyId) item.buyId = item.buyID;
        if (item.buyId && !item.buyID) item.buyID = item.buyId;

        if (item.remote && !item.hasDroppedOff) {
            this.state.remote.push(item);
            this.renderer.addToCategory('remote', item);
        } else {
            this.state.queued.push(item);
            this.renderer.addToCategory('queued', item);
        }

        this.updateCounts();
        this.updateWaitTime();
    }

    /**
     * Move item between categories
     */
    moveItem(buyId, fromCategory, toCategory) {
        const index = this.state[fromCategory].findIndex(i => i.buyID == buyId);
        if (index === -1) return;

        const [item] = this.state[fromCategory].splice(index, 1);
        this.state[toCategory].push(item);

        this.renderer.moveItem(buyId, fromCategory, toCategory);
        this.updateCounts();
    }

    /**
     * Remove item from queue
     */
    removeItem(buyId) {
        for (const category of Object.keys(this.state)) {
            const index = this.state[category].findIndex(i => i.buyID == buyId);
            if (index !== -1) {
                this.state[category].splice(index, 1);
                this.renderer.removeItem(buyId);
                this.updateCounts();
                return;
            }
        }
    }

    /**
     * Update container count for item
     */
    updateContainers(buyId, delta) {
        for (const category of Object.keys(this.state)) {
            const item = this.state[category].find(i => i.buyID == buyId);
            if (item) {
                item.containers = Math.max(1, (item.containers || 1) + delta);
                this.renderer.updateItem(buyId, item);
                this.updateWaitTime();
                return;
            }
        }
    }

    /**
     * Update all category counts in UI
     */
    updateCounts() {
        this.renderer.updateCounts({
            processing: this.state.processing.length,
            sorted: this.state.sorted.length,
            sorting: this.state.sorting.length,
            queued: this.state.queued.length,
            remote: this.state.remote.length
        });
    }

    /**
     * Calculate and update wait time display
     */
    async updateWaitTime() {
        const waitData = await this.api.getWaitTime(this.typeNum);
        this.renderer.updateWaitTime(waitData);
    }

    /**
     * Start periodic timers
     */
    startTimers() {
        // Update queue item timers every second
        setInterval(() => this.renderer.updateTimers(), 1000);

        // Update wait time every 30 seconds
        setInterval(() => this.updateWaitTime(), 30000);
    }

    generateUUID() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
            const r = Math.random() * 16 | 0;
            return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
        });
    }

    // Getters for view layer
    getState() { return this.state; }
    getSettings() { return this.settings; }
    getViewMode() { return this.viewMode; }

    setViewMode(mode) {
        this.viewMode = mode;
        localStorage.setItem('queueViewMode', mode);
        this.renderer.setViewMode(mode);
    }
}

// Export for module system
if (typeof module !== 'undefined' && module.exports) {
    module.exports = QueueManager;
}
```

#### 15.3.2 Core Module: QueueRenderer.js

```javascript
/**
 * QueueRenderer - DOM rendering for buy queue
 * Extracts all DOM manipulation from buyQueue.js
 */
class QueueRenderer {
    constructor(options) {
        this.typeNum = options.typeNum;
        this.storeType = options.storeType;
        this.settings = options.settings;

        // Cache DOM elements
        this.elements = {
            // Classic view
            processing: document.getElementById('buysProcessing'),
            sorted: document.getElementById('buysSorted'),
            sorting: document.getElementById('buysSorting'),
            queued: document.getElementById('buysQueued'),
            remote: document.getElementById('buysRemote'),

            // Enhanced view
            processingPanels: document.getElementById('buysProcessingPanels'),
            sortedPanels: document.getElementById('buysSortedPanels'),
            sortingPanels: document.getElementById('buysSortingPanels'),
            queuedPanels: document.getElementById('buysQueuedPanels'),
            remotePanels: document.getElementById('buysRemotePanels'),

            // Counts
            processingCount: document.getElementById('processingCount'),
            sortedCount: document.getElementById('sortedCount'),
            sortingCount: document.getElementById('sortingCount'),
            queuedCount: document.getElementById('queuedCount'),
            remoteCount: document.getElementById('remoteCount'),

            // Other
            waitTime: document.getElementById('waitTime'),
            waitLoading: document.getElementById('waitLoading'),
            buyqueueLoad: document.getElementById('buyqueue_load'),
            classicView: document.getElementById('buyQueue'),
            enhancedView: document.getElementById('buyQueuePanels')
        };

        this.viewMode = 'classic';
    }

    /**
     * Render all queue categories
     */
    renderAll(state) {
        this.elements.buyqueueLoad.innerHTML = '';

        Object.keys(state).forEach(category => {
            this.renderCategory(category, state[category]);
        });

        this.updateCounts({
            processing: state.processing.length,
            sorted: state.sorted.length,
            sorting: state.sorting.length,
            queued: state.queued.length,
            remote: state.remote.length
        });
    }

    /**
     * Render a single category
     */
    renderCategory(category, items) {
        const classicEl = this.elements[category];
        const panelEl = this.elements[category + 'Panels'];

        if (classicEl) classicEl.innerHTML = '';
        if (panelEl) panelEl.innerHTML = '';

        items.forEach(item => {
            const html = this.getItemHTML(item);
            if (classicEl) classicEl.insertAdjacentHTML('beforeend', html);
            if (panelEl) panelEl.insertAdjacentHTML('beforeend', html);
        });
    }

    /**
     * Generate HTML for a single queue item
     * Preserves exact HTML structure from buyQueue.js getHTMLfromItem()
     */
    getItemHTML(item) {
        const timeClass = this.getTimeClass(item);
        const flagsHTML = item.hasFlags ? '<span class="flag-indicator"><i class="fa fa-flag"></i></span>' : '';
        const newCustomerHTML = item.isNewCustomer ? '<span class="badge badge-info">NEW</span>' : '';
        const containerHTML = this.getContainersHTML(item);
        const actionsHTML = this.getActionsHTML(item);

        // This HTML structure must match buyQueue.js EXACTLY
        return `
            <li id="buy_${item.buyID}" class="buyqueue-item ${timeClass}" data-buyid="${item.buyID}">
                <div class="buyqueue-time">
                    <span class="time" data-time="${item.timeEntered}">${this.formatTime(item.timeEntered)}</span>
                    <span class="date">${this.formatDate(item.timeEntered)}</span>
                </div>
                <div class="buyqueue-label">
                    <span class="buyqueue-number">#${item.buyNumber}</span>
                </div>
                <div class="buyqueue-content">
                    <h3>
                        ${item.customerFullName}
                        ${newCustomerHTML}
                        ${flagsHTML}
                    </h3>
                    <div class="buyqueue-info">
                        ${containerHTML}
                        ${actionsHTML}
                    </div>
                </div>
            </li>
        `;
    }

    /**
     * Get time-based CSS class for item
     * Replicates yellow/red time logic from buyQueue.js
     */
    getTimeClass(item) {
        const now = new Date();
        const entered = new Date(item.timeEntered);
        const diffMinutes = Math.floor((now - entered) / 60000);

        if (diffMinutes >= this.settings.redTime) {
            return 'buyqueue-danger';
        } else if (diffMinutes >= this.settings.yellowTime) {
            return 'buyqueue-warning';
        }
        return '';
    }

    /**
     * Get containers HTML with +/- buttons
     */
    getContainersHTML(item) {
        return `
            <span class="containers">
                <button class="btn btn-xs btn-default container-minus" data-buyid="${item.buyID}">
                    <i class="fa fa-minus"></i>
                </button>
                <span class="container-count">${item.containers || 1}</span>
                <button class="btn btn-xs btn-default container-plus" data-buyid="${item.buyID}">
                    <i class="fa fa-plus"></i>
                </button>
            </span>
        `;
    }

    /**
     * Get action buttons HTML based on item state
     */
    getActionsHTML(item) {
        // Different actions based on queue category
        if (item.timeStarted !== "0000-00-00 00:00:00") {
            // Processing - show complete button
            return `
                <button class="btn btn-success btn-sm complete-buy" data-buyid="${item.buyID}">
                    <i class="fa fa-check"></i> Complete
                </button>
            `;
        } else if (item.sortCompleted !== "0000-00-00 00:00:00") {
            // Sorted - show start buy button
            return `
                <button class="btn btn-warning btn-sm start-buy" data-buyid="${item.buyID}">
                    <i class="fa fa-play"></i> Start
                </button>
            `;
        } else if (item.sortStarted !== "0000-00-00 00:00:00") {
            // Sorting - show complete sort button
            return `
                <button class="btn btn-info btn-sm complete-sort" data-buyid="${item.buyID}">
                    <i class="fa fa-check"></i> Sort Done
                </button>
            `;
        } else if (item.hasDroppedOff == 1 || item.remote == 0) {
            // Queued - show start sort or start buy
            return `
                <button class="btn btn-primary btn-sm start-sort" data-buyid="${item.buyID}">
                    <i class="fa fa-sort"></i> Sort
                </button>
                <button class="btn btn-warning btn-sm start-buy" data-buyid="${item.buyID}">
                    <i class="fa fa-play"></i> Start
                </button>
            `;
        } else {
            // Remote - show check in button
            return `
                <button class="btn btn-success btn-sm check-in" data-buyid="${item.buyID}">
                    <i class="fa fa-check-circle"></i> Check In
                </button>
            `;
        }
    }

    /**
     * Add item to category with animation
     */
    addToCategory(category, item) {
        const html = this.getItemHTML(item);
        const classicEl = this.elements[category];
        const panelEl = this.elements[category + 'Panels'];

        if (classicEl) {
            classicEl.insertAdjacentHTML('beforeend', html);
            this.animateIn(classicEl.lastElementChild);
        }
        if (panelEl) {
            panelEl.insertAdjacentHTML('beforeend', html);
            this.animateIn(panelEl.lastElementChild);
        }
    }

    /**
     * Remove item with animation
     */
    removeItem(buyId) {
        const items = document.querySelectorAll(`#buy_${buyId}`);
        items.forEach(el => {
            el.classList.add('animate__animated', 'animate__fadeOutLeft');
            setTimeout(() => el.remove(), 500);
        });
    }

    /**
     * Move item between categories (visual only)
     */
    moveItem(buyId, fromCategory, toCategory) {
        // Remove from old location
        const items = document.querySelectorAll(`#buy_${buyId}`);
        items.forEach(el => el.remove());

        // Item will be re-rendered by manager with new state
    }

    /**
     * Update counts in badges
     */
    updateCounts(counts) {
        if (this.elements.processingCount) this.elements.processingCount.textContent = counts.processing;
        if (this.elements.sortedCount) this.elements.sortedCount.textContent = counts.sorted;
        if (this.elements.sortingCount) this.elements.sortingCount.textContent = counts.sorting;
        if (this.elements.queuedCount) this.elements.queuedCount.textContent = counts.queued;
        if (this.elements.remoteCount) this.elements.remoteCount.textContent = counts.remote;
    }

    /**
     * Update wait time display
     */
    updateWaitTime(data) {
        this.elements.waitLoading.style.display = 'none';

        if (data.upperBound && data.upperBound !== 0) {
            this.elements.waitTime.textContent =
                `${this.minutesToReadable(data.lowerBound)} - ${this.minutesToReadable(data.upperBound)}`;
        } else {
            this.elements.waitTime.textContent = this.minutesToReadable(data.waitTime);
        }
    }

    /**
     * Update all timer displays
     */
    updateTimers() {
        const now = new Date();

        document.querySelectorAll('.buyqueue-item').forEach(el => {
            const timeEl = el.querySelector('.time');
            if (!timeEl) return;

            const entered = new Date(timeEl.dataset.time);
            const diffMinutes = Math.floor((now - entered) / 60000);

            // Update time class
            el.classList.remove('buyqueue-danger', 'buyqueue-warning');
            if (diffMinutes >= this.settings.redTime) {
                el.classList.add('buyqueue-danger');
            } else if (diffMinutes >= this.settings.yellowTime) {
                el.classList.add('buyqueue-warning');
            }
        });
    }

    /**
     * Toggle between classic and enhanced views
     */
    setViewMode(mode) {
        this.viewMode = mode;

        if (mode === 'enhanced') {
            this.elements.classicView.style.display = 'none';
            this.elements.enhancedView.style.display = 'block';
        } else {
            this.elements.classicView.style.display = 'block';
            this.elements.enhancedView.style.display = 'none';
        }
    }

    /**
     * Show error message
     */
    showError(message) {
        this.elements.buyqueueLoad.innerHTML = `
            <div class="alert alert-danger">
                <i class="fa fa-warning"></i> ${message}
            </div>
        `;
    }

    // Utility methods
    animateIn(el) {
        el.classList.add('animate__animated', 'animate__lightSpeedInRight');
        setTimeout(() => {
            el.classList.remove('animate__animated', 'animate__lightSpeedInRight');
        }, 1000);
    }

    formatTime(dateStr) {
        return new Date(dateStr).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    }

    formatDate(dateStr) {
        return new Date(dateStr).toLocaleDateString();
    }

    minutesToReadable(minutes) {
        const hours = Math.floor(minutes / 60);
        minutes = minutes % 60;

        if (hours > 0 && minutes > 0) return `${hours}hr ${minutes}min`;
        if (hours === 1) return '1 hr';
        if (hours > 1) return `${hours} hrs`;
        if (minutes > 0) return `${minutes} min`;
        return 'No Wait';
    }
}
```

#### 15.3.3 Core Module: QueueAbly.js

```javascript
/**
 * QueueAbly - Real-time sync for buy queue
 * Extracts Ably logic from buyQueue.js (lines 341-540)
 */
class QueueAbly {
    constructor(options) {
        this.typeNum = options.typeNum;
        this.uuid = options.uuid;
        this.manager = options.manager;
        this.api = options.api;

        this.client = null;
        this.channel = null;
        this.messageIDs = [];
    }

    /**
     * Connect to Ably and subscribe to channel
     */
    connect() {
        // Use same key injection pattern as original
        this.client = new Ably.Realtime({
            key: window.ABLY_KEY,
            idempotentRestPublishing: true
        });

        this.client.connection.on('connected', () => {
            console.log('[QueueAbly] Connected');
            this.subscribe();
        });

        this.client.connection.on('failed', (err) => {
            console.error('[QueueAbly] Connection failed:', err);
        });
    }

    /**
     * Subscribe to store channel
     */
    subscribe() {
        this.channel = this.client.channels.get(this.typeNum);

        this.channel.subscribe((message) => {
            // Dedupe (same as buyQueue.js line 350-354)
            if (this.messageIDs.includes(message.id)) return;
            this.messageIDs.push(message.id);

            // Prevent unbounded growth
            if (this.messageIDs.length > 100) this.messageIDs.shift();

            this.handleMessage(message);
        });
    }

    /**
     * Handle incoming Ably message
     * Replicates exact logic from buyQueue.js lines 355-540
     */
    handleMessage(message) {
        const item = message.data;
        console.log('[QueueAbly] Message:', item.action, item);

        switch (item.action) {
            case 'addNewBuy':
                this.handleAddNewBuy(item);
                break;

            case 'startBuy':
                this.handleStartBuy(item);
                break;

            case 'checkInJS':
                this.handleCheckIn(item);
                break;

            case 'processBuy':
                this.handleProcessBuy(item);
                break;

            case 'deleteBuy':
                this.handleDeleteBuy(item);
                break;

            case 'addContainer':
                if (item.uuid !== this.uuid) {
                    this.manager.updateContainers(item.buyID, 1);
                }
                break;

            case 'deleteContainer':
                if (item.uuid !== this.uuid) {
                    this.manager.updateContainers(item.buyID, -1);
                }
                break;

            case 'startSort':
                this.handleStartSort(item);
                break;

            case 'sortCompleted':
                this.handleSortCompleted(item);
                break;

            // Daybook events (new - handled by daybook modules)
            default:
                if (item.action && item.action.startsWith('daybook:')) {
                    // Forward to daybook handler if available
                    if (window.daybookAbly) {
                        window.daybookAbly.handleMessage(message);
                    }
                }
        }
    }

    handleAddNewBuy(item) {
        // Normalize (from buyQueue.js lines 358-364)
        if (item.buyID && !item.buyId) item.buyId = item.buyID;
        if (item.buyId && !item.buyID) item.buyID = item.buyId;

        this.manager.addItem(item);
    }

    async handleStartBuy(item) {
        const data = await this.api.getBuyDetails(this.typeNum, item.buyID, 'startBuy');
        if (data.buyID && !data.buyId) data.buyId = data.buyID;

        // Move from queued/sorted to processing
        this.manager.moveItem(item.buyID, 'queued', 'processing');
        this.manager.moveItem(item.buyID, 'sorted', 'processing');
    }

    async handleCheckIn(item) {
        const data = await this.api.getBuyDetails(this.typeNum, item.buyID, 'checkin');
        this.manager.moveItem(item.buyID, 'remote', 'queued');
    }

    handleProcessBuy(item) {
        this.manager.removeItem(item.buyID);
    }

    handleDeleteBuy(item) {
        this.manager.removeItem(item.buyID);
    }

    async handleStartSort(item) {
        const data = await this.api.getBuyDetails(this.typeNum, item.buyID, 'startSort');
        this.manager.moveItem(item.buyID, 'queued', 'sorting');
    }

    async handleSortCompleted(item) {
        const data = await this.api.getBuyDetails(this.typeNum, item.buyID, 'sortCompleted');
        this.manager.moveItem(item.buyID, 'sorting', 'sorted');
    }

    /**
     * Disconnect from Ably
     */
    disconnect() {
        if (this.client) {
            this.client.close();
        }
    }
}
```

#### 15.3.4 Main Entry Point: workspace.js

```javascript
/**
 * Workspace - Main entry point for refactored queue + daybook
 * Coordinates all modules and provides backward compatibility
 */
(function(window) {
    'use strict';

    // Configuration from meta tags (preserves original pattern)
    const config = {
        typeNum: document.querySelector('meta[name="typeNum"]')?.content,
        storeType: document.querySelector('meta[name="storeType"]')?.content,
        storeNum: document.querySelector('meta[name="storeNum"]')?.content,
        yellowTime: parseInt(document.querySelector('meta[name="yellowTime"]')?.content) || 30,
        redTime: parseInt(document.querySelector('meta[name="redTime"]')?.content) || 60,
        enableQueueSignIn: parseInt(document.querySelector('meta[name="enableQueueSignIn"]')?.content) || 0,
        csrfToken: document.querySelector('meta[name="csrf_token"]')?.content
    };

    // Validate required config
    if (!config.typeNum) {
        console.error('[Workspace] Missing typeNum configuration');
        return;
    }

    // Create instances
    const api = new QueueAPI(config);
    const renderer = new QueueRenderer(config);
    const manager = new QueueManager({
        ...config,
        settings: {
            yellowTime: config.yellowTime,
            redTime: config.redTime,
            enableQueueSignIn: config.enableQueueSignIn
        }
    });

    // Wire dependencies
    manager.renderer = renderer;
    manager.api = api;

    // Create Ably handler
    const ably = new QueueAbly({
        typeNum: config.typeNum,
        uuid: manager.uuid,
        manager: manager,
        api: api
    });
    manager.ably = ably;

    // Initialize
    document.addEventListener('DOMContentLoaded', async () => {
        // Initialize queue
        await manager.init();

        // Connect Ably
        ably.connect();

        // Setup event handlers
        setupEventHandlers(manager, api, config);

        // Initialize daybook if container exists
        if (document.getElementById('daybook-container')) {
            initDaybook(config);
        }

        console.log('[Workspace] Initialized successfully');
    });

    /**
     * Setup DOM event handlers
     * Preserves all original event bindings from buyQueue.js
     */
    function setupEventHandlers(manager, api, config) {
        // Enhanced view toggle
        document.getElementById('enhancedViewToggle')?.addEventListener('change', (e) => {
            manager.setViewMode(e.target.checked ? 'enhanced' : 'classic');
        });

        // FAB button toggle
        document.querySelector('.material-button-toggle')?.addEventListener('click', toggleButtonState);

        // Add buy button
        document.getElementById('addBuyButton')?.addEventListener('click', () => {
            if (config.enableQueueSignIn > 0) {
                $('#addBuyModal').modal('show');
                toggleButtonState();
            } else {
                alert('Queue Sign In is Disabled');
            }
        });

        // Clock in button
        document.getElementById('clockInButton')?.addEventListener('click', () => {
            $('#clockInModal').modal('show');
            toggleButtonState();
        });

        // Schedule button
        document.getElementById('scheduleButton')?.addEventListener('click', () => {
            $('#scheduleModal').modal('show');
            toggleButtonState();
        });

        // Wait time buttons
        document.querySelectorAll('#waitButtons button').forEach(btn => {
            btn.addEventListener('click', async function() {
                document.querySelectorAll('#waitButtons button').forEach(b => b.classList.remove('active'));
                this.classList.add('active');
                await api.setWaitTime(config.typeNum, parseInt(this.textContent));
                manager.updateWaitTime();
            });
        });

        // Queue item actions (delegated)
        document.addEventListener('click', async (e) => {
            const target = e.target.closest('button');
            if (!target) return;

            const buyId = target.dataset.buyid;
            if (!buyId) return;

            if (target.classList.contains('start-buy')) {
                await api.startBuy(config.typeNum, buyId);
            } else if (target.classList.contains('start-sort')) {
                await api.startSort(config.typeNum, buyId);
            } else if (target.classList.contains('complete-sort')) {
                await api.completeSort(config.typeNum, buyId);
            } else if (target.classList.contains('complete-buy')) {
                await api.completeBuy(config.typeNum, buyId);
            } else if (target.classList.contains('check-in')) {
                await api.checkIn(config.typeNum, buyId);
            } else if (target.classList.contains('container-plus')) {
                await api.addContainer(config.typeNum, buyId, manager.uuid);
                manager.updateContainers(buyId, 1);
            } else if (target.classList.contains('container-minus')) {
                await api.removeContainer(config.typeNum, buyId, manager.uuid);
                manager.updateContainers(buyId, -1);
            }
        });

        // Search functionality in panels
        document.querySelectorAll('.queue-search').forEach(input => {
            input.addEventListener('input', (e) => {
                const searchTerm = e.target.value.toLowerCase();
                const targetId = e.target.dataset.target;
                const container = document.getElementById(targetId);

                container?.querySelectorAll('.buyqueue-item').forEach(item => {
                    const name = item.querySelector('h3')?.textContent.toLowerCase() || '';
                    item.style.display = name.includes(searchTerm) ? '' : 'none';
                });
            });
        });
    }

    // FAB toggle (preserves original)
    function toggleButtonState() {
        const fab = document.querySelector('.material-button-anim');
        fab?.classList.toggle('active');
    }

    // Initialize daybook
    function initDaybook(config) {
        if (typeof DaybookAblySync !== 'undefined') {
            window.daybookAbly = new DaybookAblySync(config.typeNum, {
                onTaskUpdate: (update) => window.daybookTasks?.handleRemoteUpdate(update),
                onNoteUpdate: (update) => window.daybookNotes?.handleRemoteUpdate(update),
                onWhiteboardUpdate: (update) => window.daybookWhiteboard?.handleRemoteUpdate(update),
                onKPIUpdate: () => window.daybookKPI?.refresh(),
                onScheduleUpdate: () => window.daybookSchedule?.refresh()
            });
        }
    }

    // Expose for backward compatibility
    window.QueueManager = QueueManager;
    window.QueueRenderer = QueueRenderer;
    window.QueueAPI = QueueAPI;
    window.QueueAbly = QueueAbly;

})(window);
```

---

### 15.4 Template Refactoring

#### 15.4.1 Main Workspace Template

**`userfrosting/templates/themes/default/workspace/workspace.html`:**

```twig
{% set page_group = "front" %}
{% include 'workspace/layouts/workspace-head.html' %}

{# Store configuration as meta tags (preserves original pattern) #}
<meta name="typeNum" content="{{ store.typeNum }}">
<meta name="storeType" content="{{ store.storeType }}">
<meta name="storeNum" content="{{ store.storeNum }}">
<meta name="storeCity" content="{{ store.city }}">
<meta name="storeState" content="{{ store.state }}">
<meta name="csrf_token" content="{{ token }}"/>
<meta name="yellowTime" content="{{ store.yellowTime }}">
<meta name="redTime" content="{{ store.redTime }}">
<meta name="enableQueueSignIn" content="{{ enableQueueSignIn }}">

<div class="container workspace-container">
    {# Tab Navigation for Queue/Daybook #}
    <ul class="nav nav-tabs workspace-tabs" role="tablist">
        <li role="presentation" class="{{ activeTab == 'queue' ? 'active' : '' }}">
            <a href="#queue-tab" aria-controls="queue-tab" role="tab" data-toggle="tab">
                <i class="fa fa-list"></i> Queue
            </a>
        </li>
        <li role="presentation" class="{{ activeTab == 'daybook' ? 'active' : '' }}">
            <a href="#daybook-tab" aria-controls="daybook-tab" role="tab" data-toggle="tab">
                <i class="fa fa-book"></i> Daybook
            </a>
        </li>
    </ul>

    <div class="tab-content">
        {# Queue Tab #}
        <div role="tabpanel" class="tab-pane {{ activeTab == 'queue' ? 'active' : '' }}" id="queue-tab">
            {% include 'workspace/partials/queue/queue-controls.html' %}
            {% include 'workspace/partials/queue/queue-classic.html' %}
            {% include 'workspace/partials/queue/queue-enhanced.html' %}
        </div>

        {# Daybook Tab #}
        <div role="tabpanel" class="tab-pane {{ activeTab == 'daybook' ? 'active' : '' }}" id="daybook-tab">
            <div id="daybook-container">
                {% include 'workspace/partials/daybook/daybook-main.html' %}
            </div>
        </div>
    </div>
</div>

{# Floating Action Button #}
{% include 'workspace/partials/queue/queue-fab.html' %}

{# Modals #}
{% include 'workspace/partials/modals/add-buy-modal.html' %}
{% include 'workspace/partials/modals/clock-in-modal.html' %}
{% include 'workspace/partials/modals/schedule-modal.html' %}

{% include 'workspace/layouts/workspace-foot.html' %}
```

#### 15.4.2 Workspace Head (Modernized)

**`userfrosting/templates/themes/default/workspace/layouts/workspace-head.html`:**

```twig
<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="Expires" content="0" />
    <title>{{ page.title }}</title>

    {# Core CSS - Same as original frontHead.html #}
    <link href="/view/{{ page.storeType }}/css/bootstrap.min.css" rel="stylesheet">
    <link href="/view/common/js/datepicker/css/bootstrap-datepicker3.min.css" rel="stylesheet">
    <link href="/css/bootstrap-datetimepicker.min.css" rel="stylesheet">
    <link rel="stylesheet" href="/css/font-awesome.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">

    {# Favicons - Same as original #}
    <link rel="apple-touch-icon" sizes="57x57" href="/images/favicon/apple-icon-57x57.png">
    <link rel="apple-touch-icon" sizes="60x60" href="/images/favicon/apple-icon-60x60.png">
    <link rel="apple-touch-icon" sizes="72x72" href="/images/favicon/apple-icon-72x72.png">
    <link rel="apple-touch-icon" sizes="76x76" href="/images/favicon/apple-icon-76x76.png">
    <link rel="apple-touch-icon" sizes="114x114" href="/images/favicon/apple-icon-114x114.png">
    <link rel="apple-touch-icon" sizes="120x120" href="/images/favicon/apple-icon-120x120.png">
    <link rel="apple-touch-icon" sizes="144x144" href="/images/favicon/apple-icon-144x144.png">
    <link rel="apple-touch-icon" sizes="152x152" href="/images/favicon/apple-icon-152x152.png">
    <link rel="apple-touch-icon" sizes="180x180" href="/images/favicon/apple-icon-180x180.png">
    <link rel="icon" type="image/png" sizes="192x192" href="/images/favicon/android-icon-192x192.png">
    <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="96x96" href="/images/favicon/favicon-96x96.png">
    <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon/favicon-16x16.png">

    {# IE Support - Same as original #}
    <!--[if lt IE 9]>
    <script src="/view/common/js/html5shiv.min.js"></script>
    <script src="/view/common/js/respond.min.js"></script>
    <![endif]-->

    {# Additional CSS - Same as original #}
    <link rel="stylesheet" href="/css/bootstrap-select.min.css">
    <link rel="stylesheet" href="/css/bootstrap-switch-3.3.2.css">
    <link href="/view/common/css/styles.css?v1.24" rel="stylesheet">
    <link href="/view/{{ page.storeType }}/css/styles.css?v1.23" rel="stylesheet">
    <link href="/plugins/jBox/jBox.css" rel="stylesheet">
    <link href="/plugins/sweetalert/sweetalert.css" rel="stylesheet">
    <link href="/css/servicequeue/main.css" rel="stylesheet">
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

    {# Daybook CSS (new) #}
    <link href="/css/workspace/daybook.css" rel="stylesheet">

    <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">

    {# Ably key injection (secure) #}
    <script>window.ABLY_KEY = "{{ ablyKey }}";</script>
</head>
<body>
<div class="storeCode" id="{{ page.typeNum }}"></div>
<div class="uuid"></div>
<div class="timeZone" id="{{ page.timeZone }}"></div>
<div id="wrap">
    <div class="navbar navbar-default">
        <div class="container">
            <div class="row">
                <div class="col-xs-4 col-md-3">
                    <a class="navbar-brand" href="/{{ storeCode }}">
                        <img src="/view/{{ page.storeType }}/img/logo.png"/>
                    </a>
                </div>
                <div class="col-xs-8 col-md-9">
                    <ul class="nav navbar-nav" style="width: 100%;">
                        {% include 'menus/frontNav.html' %}
                    </ul>
                </div>
            </div>
        </div>
    </div>
```

#### 15.4.3 Workspace Foot (Modernized)

**`userfrosting/templates/themes/default/workspace/layouts/workspace-foot.html`:**

```twig
<br />
<div id="footer" class="noprint">
    <div class="container">
        <p>&copy; <a href="http://www.v2ts.com">V2 Technology Solutions</a> | <a href="/admin">Administration Panel</a></p>
    </div>
</div>
<div class="modal fade" id="myModal"></div>
</div> <!-- /wrap -->

{# Core Libraries - Same versions as original #}
<script src="/view/common/js/jquery.min.js"></script>
<script src="/view/common/js/bootstrap.min.js"></script>
<script src="/view/common/js/jquery-ui.min.js"></script>
<script src="/js/moment/moment.js"></script>
<script src="/view/common/js/moment/moment-tz.js"></script>
<script src="https://cdn.ably.com/lib/ably.min-1.js"></script>

{# UI Libraries - Same as original #}
<script src="/view/common/js/uuid.js"></script>
<script src="/js/bootstrap-select.min.js"></script>
<script src="/js/parsley.min.js"></script>
<script src="/js/raty.js"></script>
<script src="/js/jquery-bootstrap-modal-steps.js"></script>
<script src="/js/bootstrap-datetimepicker.min.js"></script>
<script src="/plugins/sweetalert/sweetalert.js"></script>

{# Refactored Workspace Modules #}
<script src="/js/workspace/modules/common/utils.js"></script>
<script src="/js/workspace/modules/common/api.js"></script>
<script src="/js/workspace/modules/queue/QueueAPI.js"></script>
<script src="/js/workspace/modules/queue/QueueRenderer.js"></script>
<script src="/js/workspace/modules/queue/QueueManager.js"></script>
<script src="/js/workspace/modules/queue/QueueAbly.js"></script>

{# Daybook Modules #}
<script src="/js/workspace/modules/daybook/ably-sync.js"></script>
<script src="/js/workspace/modules/daybook/task-manager.js"></script>
<script src="/js/workspace/modules/daybook/notes-manager.js"></script>
<script src="/js/workspace/modules/daybook/whiteboard.js"></script>
<script src="/js/workspace/modules/daybook/kpi-display.js"></script>
<script src="/js/workspace/modules/daybook/schedule-display.js"></script>

{# Main Entry Point #}
<script src="/js/workspace/workspace.js"></script>

{# Reamaze Chat Widget - Same as original #}
<script src="http://cdn.reamaze.com/assets/reamaze.js"></script>
<script type="text/javascript">
    var _support = _support || { 'ui': {}, 'user': {} };
    _support['account'] = 'buyerkiosk';
    _support['ui']['contactMode'] = 'default';
    _support['ui']['enableKb'] = 'true';
    _support['ui']['styles'] = {
        widgetColor: 'rgba(72, 173, 200, 1)',
        gradient: true,
    };
    _support['ui']['shoutboxFacesMode'] = "brand-avatar";
    _support['ui']['shoutboxHeaderLogo'] = true;
    _support['ui']['widget'] = {
        icon: 'chat',
        displayOn: 'all',
        label: {
            text: 'Let us know if you have any questions or need assistance! &#128522;',
            mode: "prompt-3",
            delay: 3,
            duration: 30,
        },
        position: 'bottom-left',
        size: 60,
        mobilePosition: 'bottom-right'
    };
</script>

</body>
</html>
```

---

### 15.5 Migration & Testing Strategy

#### 15.5.1 Phased Migration Plan

| Phase | Description | Duration | Risk |
|-------|-------------|----------|------|
| **Phase 1** | Extract JS modules (no functional changes) | 1 week | Low |
| **Phase 2** | Create new templates with partials | 1 week | Low |
| **Phase 3** | Wire up new modules, test in parallel | 2 weeks | Medium |
| **Phase 4** | Add feature flag for A/B routing | 1 day | Low |
| **Phase 5** | QA testing on staging | 1 week | Low |
| **Phase 6** | Gradual rollout (10% → 50% → 100%) | 2 weeks | Medium |
| **Phase 7** | Remove legacy code | 1 day | Low |

#### 15.5.2 Feature Flag Implementation

```php
// userfrosting/routes/groups/queue.php

$app->get('/:typeNum/', function($typeNum) use ($app) {
    $store = getStoreByTypeNum($typeNum);

    // Feature flag check
    $useNewWorkspace = $store->getFeatureFlag('use_new_workspace')
        || isset($_GET['workspace'])  // URL override for testing
        || $_COOKIE['workspace_beta'] === '1';  // Cookie for beta testers

    if ($useNewWorkspace) {
        // New refactored workspace
        return $app->render('workspace/workspace.html', [
            'store' => $store,
            'activeTab' => 'queue',
            'ablyKey' => $_ENV['ABLY_KEY'],
            // ... other vars
        ]);
    }

    // Legacy queue (original code path)
    return $app->render('queue.html', [
        // ... existing vars
    ]);
});
```

#### 15.5.3 Testing Checklist

**Automated Tests:**
```php
// tests/QueueFunctionalTest.php
class QueueFunctionalTest extends TestCase
{
    /** @test */
    public function queue_loads_and_displays_items()
    {
        // Test queue page loads
        // Test all 5 categories render
        // Test item count badges
    }

    /** @test */
    public function add_buy_modal_works()
    {
        // Test modal opens
        // Test phone validation
        // Test customer lookup
        // Test form submission
    }

    /** @test */
    public function ably_sync_works()
    {
        // Test addNewBuy event
        // Test startBuy event
        // Test deleteBuy event
        // etc.
    }

    /** @test */
    public function wait_time_updates()
    {
        // Test wait time display
        // Test wait time button changes
    }
}
```

**Manual QA Script:**
1. [ ] Open queue in two browsers side-by-side
2. [ ] Add a buy in browser A, verify appears in B
3. [ ] Start buy in browser A, verify moves in B
4. [ ] Complete buy, verify removed in both
5. [ ] Test all 5 queue categories
6. [ ] Test enhanced view toggle
7. [ ] Test search in enhanced view
8. [ ] Test FAB buttons
9. [ ] Test add buy wizard (all 4 steps)
10. [ ] Test wait time buttons
11. [ ] Test container +/- buttons
12. [ ] Verify color coding (yellow/red times)
13. [ ] Test on tablet (touch interactions)
14. [ ] Compare pixel-perfect with screenshots

#### 15.5.4 Rollback Procedure

```bash
# Immediate rollback (disable feature flag)
mysql -e "UPDATE stores SET use_new_workspace = 0 WHERE use_new_workspace = 1;"

# Or via admin panel
# Settings > Feature Flags > New Workspace > Disabled

# Clear any caches
redis-cli FLUSHALL
```

---

### 15.6 Backward Compatibility Layer

To ensure zero disruption, maintain these global functions that legacy code may depend on:

```javascript
// public_html/js/workspace/compat.js

/**
 * Backward Compatibility Layer
 * These functions preserve the global API that may be used
 * by inline scripts or other legacy code
 */

// From functions.js
window.secondsToPretty = function(seconds) {
    // Original implementation
};

window.minutesToReadable = function(minutes) {
    // Original implementation
};

window.getWaitTimePretty = function(store) {
    // Original implementation (now async wrapper)
};

window.getStoreTypeNumFromURL = function() {
    // Original implementation
};

window.getDomain = function() {
    // Original implementation
};

window.getCurrentQueue = function(store) {
    // Original implementation (now async wrapper)
};

// From buyQueue.js
window.updateWaitTime = function() {
    if (window.queueManager) {
        window.queueManager.updateWaitTime();
    }
};

window.updateQueueCounts = function() {
    if (window.queueManager) {
        window.queueManager.updateCounts();
    }
};

window.arrows = function() {
    // Original arrow update function
};

window.reorderQueue = function() {
    // Original reorder function
};

// Expose manager globally for debugging
window.getQueueState = function() {
    return window.queueManager?.getState();
};
```

---

## 16. Future Enhancements

- Push notifications for task assignments
- Homebase full integration (scheduling provider abstraction already in place)
- Time clock integration on daybook (punch in/out from Daybook UI)
- Employee kudos/recognition system
- Daily standup notes template
- Shift handoff notes with automatic carry-over
- Advanced KPI analytics (week-over-week trends, projections)
- Mobile-optimized Daybook view for managers on the go
