# Solution Design Document

## Validation Checklist

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

---

## Constraints

CON-1 **Framework**: PHP 8.x, Slim 2.6.2, Twig 1.44.8, Bootstrap 5.3.3, Syncfusion EJ2 Schedule. No framework migration permitted.

CON-2 **Database**: MySQL multi-store (central `kiosk_users` + per-store databases). All schema changes MUST use the conductor migration system. Column naming is camelCase.

CON-3 **Backward Compatibility**: Existing shifts without `positionId` set must continue to function identically. Position assignment remains optional.

CON-4 **Performance**: Clock-in confirmation data must be included in the existing clock-in API response — no additional API calls from the frontend. The response must complete within the existing 2-second target.

CON-5 **UI Components**: Use Syncfusion components over Bootstrap or custom implementations when possible (per CLAUDE.md). The schedule calendar uses Syncfusion EJ2 Schedule.

CON-6 **BuyerKiosk Provider Only**: Position validation and enhanced clock-in apply to the BuyerKiosk native scheduling provider. WhenIWork provider displays its own position data as-is.

## Implementation Context

### Required Context Sources

- ICO-1 General Application Context
```yaml
- doc: CLAUDE.md
  relevance: CRITICAL
  why: "Project stack, conventions, migration system requirement, Syncfusion preference"

- doc: docs/specs/038-shift-role-assignment/product-requirements.md
  relevance: CRITICAL
  why: "PRD defining all feature requirements, acceptance criteria, and business rules"
```

- ICO-2 Scheduling System
```yaml
- file: userfrosting/src/BuyerKiosk/Scheduling/Models/Shift.php
  relevance: CRITICAL
  why: "Shift model with existing positionId field, getPositionId/setPositionId methods"

- file: userfrosting/src/BuyerKiosk/Scheduling/Controllers/SchedulingController.php
  relevance: CRITICAL
  sections: [createShift (line 605), updateShift (line 706)]
  why: "Shift CRUD API - must add position validation here"

- file: userfrosting/src/BuyerKiosk/Scheduling/Repositories/ShiftRepository.php
  relevance: HIGH
  why: "Shift persistence - already handles positionId in queries"

- file: userfrosting/src/BuyerKiosk/Scheduling/Repositories/EmployeePositionRepository.php
  relevance: CRITICAL
  why: "Provides getPositionsForEmployee(), getPositionIdsForEmployee() for validation"

- file: userfrosting/src/BuyerKiosk/Scheduling/Repositories/PositionRepository.php
  relevance: HIGH
  why: "Position lookup by ID for validation"

- file: public_html/js/scheduling/ScheduleCalendar.js
  relevance: CRITICAL
  sections: [onPopupOpen (line 1742), createShift (line 1255), getEventTemplate (line 355)]
  why: "Syncfusion schedule editor customization - must add position dropdown"
```

- ICO-3 Clock-In System
```yaml
- file: userfrosting/src/BuyerKiosk/Workbook/Controllers/TimePunchController.php
  relevance: CRITICAL
  sections: [clockInForBuyerKiosk (line 1049), findScheduledShiftForUser (line 428), jsonResponse (line 1179)]
  why: "Clock-in API - must enrich response with position and task data"

- file: public_html/js/workspace/modules/workbook/time-punch.js
  relevance: CRITICAL
  sections: [clockIn (line 693), showSuccessMessage (line 1056)]
  why: "Frontend clock-in flow - must replace toast with confirmation screen"

- file: userfrosting/templates/themes/default/workspace/partials/modals/clock-in-modal.html
  relevance: CRITICAL
  why: "Clock-in modal template - must add confirmation screen step (Step 6)"
```

- ICO-4 Task Assignment System
```yaml
- file: userfrosting/src/BuyerKiosk/Workbook/Services/DailyTaskAssignmentService.php
  relevance: HIGH
  why: "Resolves tasks by position for a date - used to populate clock-in briefing"

- file: userfrosting/src/BuyerKiosk/Workbook/Services/ResolvedAssignment.php
  relevance: MEDIUM
  why: "Assignment value object with matchesUser() for position matching"
```

### Implementation Boundaries

- **Must Preserve**: Existing clock-in/out flow for employees without positions. Existing shift CRUD for shifts without positions. WhenIWork provider clock-in behavior. Audit trail integrity.
- **Can Modify**: `SchedulingController::createShift()`, `SchedulingController::updateShift()`, `TimePunchController::clockInForBuyerKiosk()`, `time-punch.js` clock-in success handler, clock-in modal template, `ScheduleCalendar.js` editor popup.
- **Must Not Touch**: Database tables directly (use migrations). WhenIWork integration logic. Mobile API endpoints (separate spec). UserFrosting core framework files.

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    Manager[Manager - Schedule App] --> ScheduleAPI[Schedule API<br/>SchedulingController]
    Employee[Employee - Kiosk] --> ClockAPI[Clock-In API<br/>TimePunchController]

    ScheduleAPI --> StoreDB[(Store DB<br/>scheduleShifts<br/>schedulePositions<br/>employeeSchedulePositions)]
    ScheduleAPI --> CentralDB[(Central DB<br/>kiosk_users.users<br/>userStoreAssignments)]

    ClockAPI --> StoreDB
    ClockAPI --> CentralDB
    ClockAPI --> TaskService[DailyTaskAssignmentService]
    TaskService --> StoreDB

    ClockAPI --> Ably[Ably Realtime]
    ClockAPI --> Redis[(Redis Cache)]
```

#### Interface Specifications

```yaml
inbound:
  - name: "Schedule Calendar (Syncfusion)"
    type: HTTPS
    format: REST JSON
    authentication: Session cookie
    data_flow: "Shift CRUD operations with position assignment"

  - name: "Kiosk Clock-In Modal"
    type: HTTPS
    format: REST JSON
    authentication: Employee PIN
    data_flow: "Clock-in with shift/position context, returns position + tasks"

data:
  - name: "Store Database"
    type: MySQL
    connection: PDO
    data_flow: "scheduleShifts (positionId), schedulePositions, employeeSchedulePositions"

  - name: "Central Database"
    type: MySQL
    connection: PDO
    data_flow: "User lookup and store assignment validation"

  - name: "Redis"
    type: Redis
    connection: Predis
    data_flow: "Schedule panel cache invalidation"
```

### Project Commands

```bash
# Testing
./test.sh                           # Run all tests
./test.sh --testsuite unit          # Unit tests only
./test.sh --testsuite integration   # Integration tests only

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

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

# Database Migrations
php userfrosting/conductor run
```

## Solution Strategy

- **Architecture Pattern**: Layered enhancement of existing Controller → Repository → Model pattern. No new architectural layers needed — this feature extends existing components.
- **Integration Approach**: Surgical additions to existing controllers and frontend modules. Position validation added as a new private method in SchedulingController. Clock-in enrichment added to the existing clockInForBuyerKiosk response builder.
- **Justification**: The infrastructure is already in place (positionId on shifts, employeeSchedulePositions junction table, EmployeePositionRepository). This is a validation + UI enhancement, not a new subsystem.
- **Key Decisions**: Position dropdown uses Syncfusion DropDownList in the schedule editor; clock-in confirmation is a new Step 6 panel inside the existing modal rather than a separate modal.

## Building Block View

### Components

```mermaid
graph LR
    subgraph Frontend
        ScheduleEditor[Schedule Editor<br/>ScheduleCalendar.js]
        ClockInUI[Clock-In UI<br/>time-punch.js]
        ClockInModal[Clock-In Modal<br/>clock-in-modal.html]
    end

    subgraph Backend API
        ScheduleCtrl[SchedulingController<br/>createShift / updateShift]
        ClockCtrl[TimePunchController<br/>clockInForBuyerKiosk]
    end

    subgraph Services
        PosValidator[Position Validation<br/>validateEmployeePosition]
        TaskResolver[DailyTaskAssignmentService<br/>resolveTasksForPosition]
    end

    subgraph Repositories
        EmpPosRepo[EmployeePositionRepository]
        PosRepo[PositionRepository]
        ShiftRepo[ShiftRepository]
    end

    ScheduleEditor --> ScheduleCtrl
    ClockInUI --> ClockCtrl
    ScheduleCtrl --> PosValidator
    PosValidator --> EmpPosRepo
    PosValidator --> PosRepo
    ClockCtrl --> TaskResolver
    TaskResolver --> EmpPosRepo
```

### Directory Map

**Component**: Backend - Scheduling
```
userfrosting/src/BuyerKiosk/Scheduling/
├── Controllers/
│   └── SchedulingController.php      # MODIFY: Add validateEmployeePosition(), add validation in createShift/updateShift
├── Repositories/
│   └── EmployeePositionRepository.php # EXISTING: Already has getPositionsForEmployee(), getPositionIdsForEmployee()
└── Models/
    └── Shift.php                      # EXISTING: Already has getPositionId(), setPositionId()
```

**Component**: Backend - Workbook/Clock-In
```
userfrosting/src/BuyerKiosk/Workbook/
├── Controllers/
│   └── TimePunchController.php        # MODIFY: Enrich clock-in response with position + tasks
└── Services/
    └── DailyTaskAssignmentService.php # EXISTING: Used to resolve tasks for position
```

**Component**: Frontend - Schedule Calendar
```
public_html/js/scheduling/
└── ScheduleCalendar.js                # MODIFY: Add position dropdown in editor popup
```

**Component**: Frontend - Clock-In
```
public_html/js/workspace/modules/workbook/
└── time-punch.js                      # MODIFY: Add showClockInConfirmation(), replace toast

userfrosting/templates/themes/default/workspace/partials/modals/
└── clock-in-modal.html                # MODIFY: Add Step 6 confirmation panel

userfrosting/templates/themes/default/workbook/modals/
└── clock-in-modal.html                # MODIFY: Add Step 6 confirmation panel (workbook copy)

public_html/css/admin/modules/
└── clock-in-confirmation.css          # NEW: Styles for clock-in confirmation screen
```

**Component**: API Endpoint for Employee Positions
```
userfrosting/src/BuyerKiosk/Scheduling/Controllers/
└── SchedulingController.php           # MODIFY: Add GET /api/:typeNum/schedule/employees/:employeeId/positions endpoint
userfrosting/routes/
└── scheduling.php                     # MODIFY: Add route for employee positions
```

### Interface Specifications

#### Data Storage Changes

No new tables needed. The existing schema already supports this feature:

```yaml
Table: scheduleShifts (per-store, EXISTING)
  positionId: INT NULL FK → schedulePositions.positionId
  # Already exists, currently optional and sparsely populated
  # No schema change needed — just enforcing usage through validation

Table: schedulePositions (per-store, EXISTING)
  positionId: INT PK AUTO_INCREMENT
  name: VARCHAR(100)
  color: VARCHAR(7)
  sortOrder: INT
  isActive: TINYINT(1)
  # No changes needed

Table: employeeSchedulePositions (per-store, EXISTING)
  id: INT PK AUTO_INCREMENT
  userId: INT FK → kiosk_users.users.id
  positionId: INT FK → schedulePositions.positionId
  UNIQUE(userId, positionId)
  # No changes needed
```

#### Internal API Changes

**Existing Endpoint Modified: Create Shift**
```yaml
Endpoint: Create Shift
  Method: POST
  Path: /api/:typeNum/schedule/shifts
  Request (MODIFIED):
    employeeId: int|null (null for open shifts)
    shiftStart: string ISO 8601
    shiftEnd: string ISO 8601
    positionId: int|null (ENHANCED: now validated against employee positions)
    minRoleId: int|null (1-5)
    notes: string|null
  Response:
    success:
      success: true
      shiftId: int
      laborCost: object
    error (NEW - position validation):
      success: false
      error: "Employee is not qualified for this position"
      code: "INVALID_POSITION"
```

**Existing Endpoint Modified: Update Shift**
```yaml
Endpoint: Update Shift
  Method: PUT
  Path: /api/:typeNum/schedule/shifts/:shiftId
  Request (MODIFIED):
    positionId: int|null (ENHANCED: now validated against employee positions)
    # ... all existing fields unchanged
  Response:
    # ... unchanged success response
    error (NEW - position validation):
      success: false
      error: "Employee is not qualified for this position"
      code: "INVALID_POSITION"
```

**New Endpoint: Get Employee Positions**
```yaml
Endpoint: Get Positions for Employee
  Method: GET
  Path: /api/:typeNum/schedule/employees/:employeeId/positions
  Authentication: Session (requires uri_schedule permission)
  Response:
    success:
      positions:
        - positionId: int
          name: string
          color: string
          sortOrder: int
    error:
      error: string
      code: string
```

**Existing Endpoint Modified: Clock In**
```yaml
Endpoint: Clock In (BuyerKiosk provider)
  Method: POST
  Path: /api/:typeNum/workbook/timepunch/clockin/
  Request: unchanged
  Response (ENHANCED):
    success: true
    message: "Successfully clocked in"
    data:
      provider: "buyerkiosk"
      punchId: int
      startTime: string ISO 8601
      shiftId: int|null
      isUnscheduled: bool
      isManagerOverride: bool
      # NEW FIELDS:
      shift:
        positionId: int|null
        positionName: string|null
        positionColor: string|null
        shiftStart: string (local time HH:mm)
        shiftEnd: string (local time HH:mm)
      tasks:
        - taskId: int
          name: string
          groupName: string|null
          isCompleted: bool
        # Empty array if no tasks or no position
      taskSummary:
        total: int
        completed: int
```

#### Application Data Models

```pseudocode
ENTITY: Shift (EXISTING - no changes)
  FIELDS:
    shiftId: int PK
    employeeId: int|null FK
    shiftStart: DateTime UTC
    shiftEnd: DateTime UTC
    positionId: int|null FK  # Already exists
    ...existing fields

  BEHAVIORS:
    getPositionId(): ?int  # Already exists
    setPositionId(?int): self  # Already exists
    getPositionName(): ?string  # Already exists (from JOIN)
```

No new models needed.

#### Integration Points

```yaml
# Internal service integration
- from: TimePunchController
  to: DailyTaskAssignmentService
    - protocol: PHP method call
    - data_flow: "Resolve tasks assigned to position for today's date"
    - method: getTasksForPositionOnDate(positionId, date)

- from: SchedulingController
  to: EmployeePositionRepository
    - protocol: PHP method call
    - data_flow: "Validate employee holds position before saving shift"
    - method: getPositionIdsForEmployee(userId)
```

### Implementation Examples

#### Example: Position Validation in SchedulingController

**Why this example**: Shows the critical validation logic that must be added to both createShift() and updateShift() to prevent invalid position assignments.

```php
/**
 * Validate that an employee holds a specific position.
 *
 * @param int $employeeId Employee user ID
 * @param int $positionId Position ID to validate
 * @return bool True if employee holds the position
 */
private function validateEmployeePosition(int $employeeId, int $positionId): bool
{
    $empPosRepo = new EmployeePositionRepository($this->db);
    $positionIds = $empPosRepo->getPositionIdsForEmployee($employeeId);
    return in_array($positionId, $positionIds, true);
}
```

Usage in createShift() — inserted after the employeeId validation block (around line 647):
```php
if (isset($data['positionId']) && $data['positionId'] !== null && $employeeId !== null) {
    $positionId = (int)$data['positionId'];
    if (!$this->validateEmployeePosition($employeeId, $positionId)) {
        $this->sendErrorResponse(
            'Employee is not qualified for this position',
            400,
            'INVALID_POSITION'
        );
        return;
    }
    $shift->setPositionId($positionId);
}
```

#### Example: Enriched Clock-In Response

**Why this example**: Shows how to add position and task data to the existing clock-in response without adding API calls.

```php
// After creating the punch and before sending the response (around line 1179)
$shiftInfo = null;
$tasks = [];
$taskSummary = ['total' => 0, 'completed' => 0];

if ($scheduledShift !== null) {
    $storeTimezone = new DateTimeZone($this->store->getTimezone() ?: 'America/Chicago');
    $shiftStartLocal = new DateTime($scheduledShift['shiftStart'], $storeTimezone);
    $shiftEndLocal = new DateTime($scheduledShift['shiftEnd'], $storeTimezone);

    $shiftInfo = [
        'positionId' => $scheduledShift['positionId'] ?? null,
        'positionName' => $scheduledShift['positionName'] ?? null,
        'positionColor' => null,
        'shiftStart' => $shiftStartLocal->format('g:i A'),
        'shiftEnd' => $shiftEndLocal->format('g:i A'),
    ];

    // Look up position color if position is set
    if ($shiftInfo['positionId']) {
        $posRepo = new PositionRepository($this->db);
        $position = $posRepo->findById($shiftInfo['positionId']);
        if ($position) {
            $shiftInfo['positionColor'] = $position->getColor();
        }

        // Resolve tasks for this position today
        $tasks = $this->getTasksForPosition($shiftInfo['positionId'], $userId);
        $taskSummary = [
            'total' => count($tasks),
            'completed' => count(array_filter($tasks, fn($t) => $t['isCompleted'])),
        ];
    }
}

$this->jsonResponse([
    'success' => true,
    'message' => 'Successfully clocked in',
    'data' => [
        'provider' => 'buyerkiosk',
        'punchId' => $createdPunch->getPunchId(),
        'startTime' => $startTime,
        'shiftId' => $scheduledShift['shiftId'] ?? null,
        'isUnscheduled' => $scheduledShift === null,
        'isManagerOverride' => $isManagerOverride,
        'shift' => $shiftInfo,
        'tasks' => $tasks,
        'taskSummary' => $taskSummary,
    ]
]);
```

#### Example: Clock-In Confirmation Screen (JavaScript)

**Why this example**: Shows the pattern for replacing the toast with a rich confirmation panel.

```javascript
/**
 * Show clock-in confirmation screen with role and tasks
 * Replaces the simple showSuccessMessage('Successfully clocked in!') call
 */
showClockInConfirmation(responseData) {
    var data = responseData.data || {};
    var shift = data.shift || {};
    var tasks = data.tasks || [];
    var taskSummary = data.taskSummary || { total: 0, completed: 0 };

    var container = document.getElementById('clockStateConfirmation');
    if (!container) return this.showSuccessMessage('Successfully clocked in!'); // Fallback

    // Build position display
    var positionHtml = '';
    if (data.isUnscheduled) {
        positionHtml = '<span class="badge bg-secondary">Unscheduled Shift</span>';
    } else if (shift.positionName) {
        var colorStyle = shift.positionColor
            ? 'background-color:' + shift.positionColor + '; color: #fff;'
            : '';
        positionHtml = '<span class="badge" style="' + colorStyle + '">' +
            shift.positionName + '</span>';
    } else {
        positionHtml = '<span class="text-muted">No specific role assigned</span>';
    }

    // Populate the confirmation panel
    document.getElementById('confirmPositionBadge').innerHTML = positionHtml;
    document.getElementById('confirmShiftTime').textContent =
        shift.shiftStart && shift.shiftEnd
            ? shift.shiftStart + ' - ' + shift.shiftEnd
            : '';

    // Build task list
    this.renderConfirmationTasks(tasks, taskSummary);

    // Show confirmation, hide other states
    this.hideAllStates();
    container.style.display = 'block';

    // Auto-dismiss after 15 seconds
    this.confirmDismissTimer = setTimeout(() => {
        this.dismissConfirmation();
    }, 15000);
}
```

#### Example: Position Dropdown in Syncfusion Editor

**Why this example**: Shows how to add a filtered position dropdown to the Syncfusion schedule editor popup.

```javascript
/**
 * Add position dropdown to the Syncfusion editor popup.
 * Fetches employee positions and creates a Syncfusion DropDownList.
 */
async addPositionDropdownToEditor(popup, shiftData) {
    // Create container after the employee dropdown
    var empContainer = popup.querySelector('.e-resource-container') ||
                       popup.querySelector('[data-name="employeeId"]')?.closest('.e-field');
    if (!empContainer) return;

    // Remove existing position dropdown if re-rendering
    var existing = popup.querySelector('.position-field-container');
    if (existing) existing.remove();

    var container = document.createElement('div');
    container.className = 'e-field position-field-container';
    container.innerHTML =
        '<div class="e-field-label">Position</div>' +
        '<div class="e-field-value"><input id="positionDropdown" /></div>';
    empContainer.after(container);

    // Fetch positions for the selected employee
    var employeeId = shiftData.employeeId;
    var positions = [];

    if (employeeId && employeeId !== OPEN_SHIFTS_RESOURCE_ID) {
        positions = await this.fetchEmployeePositions(employeeId);
    } else {
        positions = await this.fetchAllActivePositions();
    }

    // Create Syncfusion DropDownList
    var dropdown = new ej.dropdowns.DropDownList({
        dataSource: positions,
        fields: { text: 'name', value: 'positionId' },
        placeholder: 'Select position...',
        allowFiltering: false,
        value: shiftData.positionId || null
    });
    dropdown.appendTo('#positionDropdown');
}
```

#### Test Examples as Interface Documentation

```php
// Test: Position validation prevents invalid assignment
public function testCreateShiftRejectsInvalidPosition(): void
{
    // Employee has positions [1 (Owner), 4 (Buyer)] but NOT 3 (Shift Lead)
    $response = $this->createShift([
        'employeeId' => 100,
        'positionId' => 3,   // Shift Lead - employee doesn't have this
        'shiftStart' => '2026-04-03T09:00:00',
        'shiftEnd'   => '2026-04-03T17:00:00',
    ]);

    $this->assertEquals(400, $response->getStatusCode());
    $body = json_decode($response->getBody(), true);
    $this->assertEquals('INVALID_POSITION', $body['code']);
}

// Test: Clock-in response includes position and tasks
public function testClockInResponseIncludesPositionAndTasks(): void
{
    // Employee has shift with positionId = 4 (Buyer)
    // Tasks exist for Buyer position today
    $response = $this->clockIn(['employeeId' => 100, 'pin' => '1234']);

    $body = json_decode($response->getBody(), true);
    $this->assertTrue($body['success']);
    $this->assertEquals('Buyer', $body['data']['shift']['positionName']);
    $this->assertNotEmpty($body['data']['tasks']);
    $this->assertArrayHasKey('total', $body['data']['taskSummary']);
}
```

## Runtime View

### Primary Flow

#### Primary Flow: Manager Creates Shift with Position

1. Manager opens schedule calendar page
2. Manager clicks to create a new shift for an employee
3. Syncfusion editor popup opens with position dropdown
4. Frontend fetches employee's positions via `GET /api/:typeNum/schedule/employees/:employeeId/positions`
5. Manager selects a position from the filtered dropdown
6. Manager clicks Save
7. Frontend sends `POST /api/:typeNum/schedule/shifts` with `positionId`
8. Backend validates employee holds the position
9. Shift is created and schedule refreshes showing position badge on shift block

```mermaid
sequenceDiagram
    actor Manager
    participant Calendar as ScheduleCalendar.js
    participant API as SchedulingController
    participant EmpPosRepo as EmployeePositionRepository
    participant ShiftRepo as ShiftRepository

    Manager->>Calendar: Click to create shift
    Calendar->>API: GET /employees/:id/positions
    API->>EmpPosRepo: getPositionsForEmployee(userId)
    EmpPosRepo-->>API: [positions]
    API-->>Calendar: {positions: [...]}
    Calendar->>Manager: Show editor with position dropdown

    Manager->>Calendar: Select position, click Save
    Calendar->>API: POST /schedule/shifts {employeeId, positionId, ...}
    API->>EmpPosRepo: getPositionIdsForEmployee(userId)
    EmpPosRepo-->>API: [positionIds]
    API->>API: Validate positionId in positionIds
    API->>ShiftRepo: create(shift)
    ShiftRepo-->>API: createdShift
    API-->>Calendar: {success: true, shiftId: 123}
    Calendar->>Manager: Refresh schedule with position badge
```

#### Secondary Flow: Employee Clocks In with Position Briefing

1. Employee selects name on kiosk
2. Employee enters PIN
3. Employee taps "Clock In"
4. Backend finds scheduled shift, resolves position, fetches tasks for position
5. Response includes shift position + tasks
6. Frontend shows confirmation screen instead of toast
7. Employee reads briefing, taps "Got it" or waits 15 seconds for auto-dismiss

```mermaid
sequenceDiagram
    actor Employee
    participant UI as time-punch.js
    participant API as TimePunchController
    participant ShiftRepo as ShiftRepository
    participant TaskSvc as DailyTaskAssignmentService

    Employee->>UI: Enter PIN, tap Clock In
    UI->>API: POST /timepunch/clockin/ {employeeId, pin}
    API->>API: Verify PIN
    API->>ShiftRepo: findScheduledShiftForUser(userId)
    ShiftRepo-->>API: {shiftId, positionId, positionName, ...}
    API->>API: Create TimePunch record
    API->>TaskSvc: getTasksForPosition(positionId, date)
    TaskSvc-->>API: [{taskId, name, isCompleted}, ...]
    API-->>UI: {success, data: {shift: {...}, tasks: [...], taskSummary: {...}}}
    UI->>Employee: Show confirmation screen with role + tasks
    Employee->>UI: Tap "Got it"
    UI->>Employee: Return to clocked-in state
```

### Error Handling

- **Invalid position assignment (400)**: Clear error message "Employee is not qualified for this position" with error code `INVALID_POSITION`. Frontend shows inline error in the editor popup — does not close the popup.
- **Employee has no positions (UI)**: Position dropdown is empty with placeholder text "No positions assigned — assign in Team Members". Link to team members page if possible.
- **Position lookup failure (500)**: Log error, continue without position data in clock-in response. Clock-in still succeeds — position enrichment is non-blocking.
- **Task resolution failure (500)**: Log error, return empty tasks array. Clock-in still succeeds — task enrichment is non-blocking.
- **Employee changed on shift, position now invalid**: Frontend clears position dropdown, shows empty state with positions for new employee. Backend validates on save.

## Deployment View

### Single Application Deployment
- **Environment**: Existing web server (PHP 8.x). No new services required.
- **Configuration**: No new environment variables or settings needed.
- **Dependencies**: No new packages or libraries. Uses existing Syncfusion EJ2 DropDownList (already available).
- **Performance**: Position validation adds one SQL query to shift create/update (indexed lookup on employeeSchedulePositions). Clock-in enrichment adds two queries (position color lookup + task resolution) — both are indexed and fast.
- **Migration**: No database migration needed — all required tables and columns already exist.
- **Cache**: Schedule panel cache (`schedule_panel:{typeNum}:{date}`) is already invalidated on shift changes and clock-in.

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
- pattern: Controller → Repository → Model
  relevance: CRITICAL
  why: "All changes follow existing layered pattern in SchedulingController and TimePunchController"

- pattern: Position validation pattern
  relevance: HIGH
  why: "New validation pattern using EmployeePositionRepository — consistent with existing checkStoreGroup/checkAccess patterns"
```

### System-Wide Patterns

- **Security**: Position validation uses the same session-based auth (`uri_schedule_manage` permission) as existing shift CRUD. Clock-in uses PIN verification (unchanged). No new auth patterns.
- **Error Handling**: Follows existing pattern — `sendErrorResponse(message, code, errorCode)` for API errors. Non-critical enrichment (tasks) uses try/catch with logging and graceful degradation.
- **Performance**: Position lookups are indexed via `employeeSchedulePositions(userId, positionId)` unique constraint. Task resolution uses existing DailyTaskAssignmentService which already queries indexed tables.
- **Logging/Auditing**: Position changes on shifts are already captured by `ShiftAuditRepository::logUpdate()` which diffs old and new values including positionId.

### Implementation Patterns

#### Code Patterns and Conventions
- PHP: camelCase for methods and variables, PSR-4 autoloading under `BuyerKiosk\` namespace
- JavaScript: Vanilla JS (no framework), Syncfusion EJ2 components, DOM manipulation via `getElementById`
- CSS: BEM-like naming within `clock-` namespace, CSS custom properties from `tokens.css`
- Templates: Twig with `{% raw %}{% endraw %}` for Handlebars

#### State Management Patterns
- Backend: Stateless request handling. All state in MySQL and Redis cache.
- Frontend (time-punch.js): Instance properties on the `TimePunch` class. `this.punchState` tracks current clock state. New `this.confirmDismissTimer` for auto-dismiss timeout.
- Frontend (ScheduleCalendar.js): Syncfusion Schedule manages shift data in its internal dataSource. Position dropdown state managed by Syncfusion DropDownList component.

#### Performance Characteristics
- Position validation query: < 1ms (indexed unique constraint lookup)
- Task resolution: < 5ms (already optimized in DailyTaskAssignmentService)
- Clock-in response total: < 50ms additional latency from enrichment
- Frontend position fetch: Cached per editor session (fetched once when popup opens, re-fetched when employee changes)

#### Component Structure Pattern
```pseudocode
COMPONENT: ClockInConfirmation
  INPUT: clockInResponseData (from API)

  RENDER:
    IF data.isUnscheduled: show "Unscheduled Shift"
    ELIF data.shift.positionName: show position badge with color
    ELSE: show "No specific role assigned"

    IF data.tasks.length > 0: render task checklist
    ELSE: show "No tasks assigned for today"

    ALWAYS: show "Got it" button + auto-dismiss timer (15s)
```

#### Data Processing Pattern
```pseudocode
FUNCTION: enrichClockInResponse(scheduledShift, userId, storeDb)
  IF scheduledShift is null: RETURN {shift: null, tasks: [], taskSummary: {total: 0, completed: 0}}

  shift = {
    positionId: scheduledShift.positionId,
    positionName: scheduledShift.positionName,
    positionColor: lookupPositionColor(scheduledShift.positionId),
    shiftStart: formatLocalTime(scheduledShift.shiftStart),
    shiftEnd: formatLocalTime(scheduledShift.shiftEnd)
  }

  IF shift.positionId is not null:
    tasks = resolveTasksForPosition(shift.positionId, userId, today)
  ELSE:
    tasks = []

  RETURN {shift, tasks, taskSummary: {total: len(tasks), completed: countCompleted(tasks)}}
```

#### Error Handling Pattern
```pseudocode
FUNCTION: handlePositionEnrichmentError(error)
  LOG: error details with context
  RETURN: default empty enrichment data
  # Clock-in ALWAYS succeeds — enrichment failure is non-blocking
```

#### Test Pattern
```pseudocode
TEST_SCENARIO: "Position validation prevents invalid assignment"
  SETUP: Employee with positions [Owner, Buyer]
  EXECUTE: createShift with positionId = Shift Lead
  VERIFY: 400 response with INVALID_POSITION code

TEST_SCENARIO: "Clock-in includes position and tasks"
  SETUP: Employee scheduled with Buyer position, 3 tasks assigned to Buyer
  EXECUTE: clockIn for employee
  VERIFY:
    response.data.shift.positionName == 'Buyer'
    response.data.tasks.length == 3
    response.data.taskSummary.total == 3

TEST_SCENARIO: "Clock-in succeeds even if task resolution fails"
  SETUP: Employee scheduled with position, task service throws exception
  EXECUTE: clockIn for employee
  VERIFY:
    response.success == true
    response.data.tasks == []
    error logged to error_log
```

### Integration Points

- **Schedule → Positions**: `ScheduleCalendar.js` calls new `GET /api/:typeNum/schedule/employees/:employeeId/positions` when editor opens and when employee changes in the dropdown
- **Clock-In → Tasks**: `TimePunchController` calls `DailyTaskAssignmentService` to resolve tasks by position. Uses existing cascade resolution (task-specific → group-level)
- **Ably Events**: Existing clock-in Ably broadcast (`workbook:timepunch:clockin`) unchanged — the enriched data is for the kiosk UI only, not broadcast

## Architecture Decisions

- [x] ADR-1 **Confirmation screen as Step 6 inside existing modal**: Add a new `clockStateConfirmation` div to the clock-in modal rather than creating a separate modal or page.
  - Rationale: Maintains the kiosk flow in a single modal. Employee doesn't navigate away. Consistent with existing Step 1-5 pattern (Loading → Employee Select → PIN → Actions → Manager Override).
  - Trade-offs: The modal gets another state panel, but this is consistent with the existing multi-step pattern and doesn't increase complexity for the user.
  - User confirmed: _Pending_

- [x] ADR-2 **Position enrichment in clock-in response (no extra API call)**: Bundle position name, color, and task list directly in the clock-in API response rather than making the frontend fetch tasks separately.
  - Rationale: Keeps the frontend simple (one request), reduces latency on shared kiosk terminals, and follows the PRD constraint.
  - Trade-offs: Slightly larger clock-in response payload (~500 bytes for tasks). Accepted — negligible for a JSON API.
  - User confirmed: _Pending_

- [x] ADR-3 **Graceful degradation for enrichment failures**: Position and task enrichment is wrapped in try/catch. If it fails, clock-in still succeeds with empty enrichment data.
  - Rationale: Clock-in is the critical operation. Showing "no tasks" is better than failing the clock-in entirely because the task service is having issues.
  - Trade-offs: Employee may occasionally not see their tasks on clock-in. Acceptable — they can view tasks in the daily assignment view.
  - User confirmed: _Pending_

- [x] ADR-4 **Syncfusion DropDownList for position selection**: Use Syncfusion's `ej.dropdowns.DropDownList` component for the position dropdown in the schedule editor.
  - Rationale: Consistent with existing employee dropdown in the editor. Follows CLAUDE.md directive to use Syncfusion components over Bootstrap or custom implementations.
  - Trade-offs: None — Syncfusion is already loaded on the schedule page.
  - User confirmed: _Pending_

- [x] ADR-5 **New API endpoint for employee positions**: Add `GET /api/:typeNum/schedule/employees/:employeeId/positions` rather than bundling positions in the shift data or employee list.
  - Rationale: Clean separation — positions are fetched on-demand when the editor opens. Avoids bloating the shifts API or employees API. Reusable by other features.
  - Trade-offs: One additional API call when the editor popup opens. Mitigated: fast query (< 5ms), only happens on editor open.
  - User confirmed: _Pending_

## Quality Requirements

- **Performance**: Clock-in API response time must remain under 2 seconds including enrichment. Position validation must add < 5ms to shift save operations.
- **Usability**: Clock-in confirmation screen must be readable on a shared kiosk terminal (minimum 16px text, clear visual hierarchy). Auto-dismiss ensures kiosk doesn't get stuck.
- **Security**: Backend position validation is mandatory — frontend filtering alone is insufficient. All endpoints use existing auth patterns (session + permission checks).
- **Reliability**: Clock-in must never fail due to enrichment errors. Graceful degradation to empty position/tasks if services fail.
- **Backward Compatibility**: 100% of existing shifts without positions must continue to function identically. No regressions in schedule display or clock-in flow.

## Risks and Technical Debt

### Known Technical Issues
- The clock-in modal template exists in two locations (`workspace/partials/modals/clock-in-modal.html` and `workbook/modals/clock-in-modal.html`). Both must be updated identically.
- The `findScheduledShiftForUser()` method already returns `positionId` and `positionName` from a JOIN, so no additional query is needed for the basic position data.

### Technical Debt
- The clock-in modal has inline `<style>` blocks (800+ lines). The new confirmation CSS should go in a separate CSS module file (`clock-in-confirmation.css`) per design system conventions, not added to the inline styles.
- Two copies of the clock-in modal template is itself technical debt. This spec does not address deduplication but both must be kept in sync.

### Implementation Gotchas
- **Syncfusion editor timing**: The `onPopupOpen` handler uses `setTimeout(() => {...}, 0)` to wait for the popup to render before customizing it. The position dropdown must be created inside this same setTimeout block.
- **DropDownList cleanup**: Syncfusion components must be destroyed on popup close to avoid memory leaks. Follow the existing pattern in `onPopupClose` where `shiftTaskTab.cleanup()` is called.
- **Employee change in editor**: When the employee dropdown changes, the position dropdown must re-fetch positions for the new employee. This requires wiring into the existing `empDropdownObj.change` handler (already present for task tab, around line 1851).
- **The `findScheduledShiftForUser` already returns positionName**: From a LEFT JOIN with schedulePositions. No need for an extra query in the clock-in flow for the position name itself — only the color and tasks need additional lookups.

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Position Validation on Shift Creation (Happy Path)**
```gherkin
Given: Employee 100 has positions [Owner (1), Buyer (4)]
And: Manager has uri_schedule_manage permission
When: Manager creates shift with employeeId=100, positionId=4 (Buyer)
Then: Shift is created successfully with positionId=4
And: Schedule displays "Buyer" badge on the shift block
```

**Scenario 2: Position Validation Rejection**
```gherkin
Given: Employee 100 has positions [Owner (1), Buyer (4)]
When: Manager creates shift with employeeId=100, positionId=3 (Shift Lead)
Then: API returns 400 with code INVALID_POSITION
And: Error message says "Employee is not qualified for this position"
And: Shift is NOT created
```

**Scenario 3: Open Shift Allows Any Position**
```gherkin
Given: Position 5 (Cashier) exists and is active
When: Manager creates open shift with employeeId=null, positionId=5
Then: Shift is created successfully (no position validation for open shifts)
```

**Scenario 4: Clock-In Shows Position and Tasks**
```gherkin
Given: Employee 100 has shift today with positionId=4 (Buyer)
And: 3 tasks are assigned to Buyer position for today (1 completed)
When: Employee 100 clocks in
Then: Confirmation screen shows "Successfully Clocked In"
And: Shows "Today's Shift: Buyer" with Buyer color badge
And: Shows "9:00 AM - 5:00 PM" shift time
And: Shows 3 tasks with 1 marked complete
And: Shows "1 of 3 tasks completed"
And: Screen auto-dismisses after 15 seconds
```

**Scenario 5: Clock-In Without Position**
```gherkin
Given: Employee 100 has shift today with positionId=null
When: Employee 100 clocks in
Then: Confirmation screen shows "Successfully Clocked In"
And: Shows "No specific role assigned"
And: Shows "No tasks assigned for today"
```

**Scenario 6: Unscheduled Clock-In with Override**
```gherkin
Given: Employee 100 has no shift scheduled today
And: Manager approves override
When: Employee 100 clocks in with manager override
Then: Confirmation screen shows "Successfully Clocked In"
And: Shows "Unscheduled Shift" badge
And: Tasks section is hidden
```

**Scenario 7: Employee Change Clears Invalid Position**
```gherkin
Given: Shift editor open with Employee A (has Shift Lead position selected)
When: Manager changes employee to Employee B (who does NOT have Shift Lead)
Then: Position dropdown is cleared
And: Position dropdown repopulates with Employee B's positions
```

**Scenario 8: Backward Compatibility - Shift Without Position**
```gherkin
Given: Existing shift with positionId=null
When: Manager views the schedule
Then: Shift block displays without any position badge
And: Editing the shift shows empty position dropdown (not required)
```

### Test Coverage Requirements

- **Business Logic**: Position validation (positive and negative cases), enrichment data building, task resolution by position, auto-dismiss timer
- **User Interface**: Confirmation screen rendering for all states (position with color, no position, unscheduled, with tasks, without tasks), position dropdown in editor (filtered by employee, re-filtered on employee change, empty state)
- **Integration Points**: Clock-in API response schema validation, schedule API position validation, employee positions API
- **Edge Cases**: Employee with no positions, employee with 10+ positions, shift with position then employee removed, concurrent shift creation, clock-in when task service is down
- **Performance**: Clock-in enrichment adds < 50ms latency, position dropdown loads in < 200ms
- **Security**: Position validation cannot be bypassed by omitting frontend filtering (API-level enforcement)

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Position | A job role defined per store (e.g., Shift Lead, Buyer, Cashier) | Stored in `schedulePositions` table, assigned to employees and shifts |
| Shift | A scheduled work period for an employee with start/end times | Stored in `scheduleShifts`, now with optional per-shift position |
| Clock-In | The act of starting a work session at the kiosk terminal | Handled by TimePunchController, creates a TimePunch record |
| Kiosk Mode | Shared terminal where employees use PIN to clock in/out | The primary UI context for the confirmation screen |
| Daily Task Assignment | Tasks assigned to positions for a specific date | Resolved by DailyTaskAssignmentService using 4-level cascade |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| typeNum | Store identifier pattern `[a-z][a-z]\d+` (e.g., `ou00`) | Used in all API paths and database routing |
| employeeSchedulePositions | Junction table linking employees to their qualified positions | Used for position validation during shift assignment |
| DailyTaskAssignmentService | Service that resolves which tasks belong to which position/person on a date | Used in clock-in enrichment to show daily tasks |
| Syncfusion EJ2 Schedule | Third-party calendar/scheduling component | The visual schedule editor where shifts are created/edited |
| DropDownList | Syncfusion's dropdown component | Used for the position selector in the shift editor |

### API/Interface Terms

| Term | Definition | Context |
|------|------------|---------|
| INVALID_POSITION | Error code returned when employee doesn't hold the assigned position | Returned by create/update shift endpoints |
| shift.positionId | The position assigned to a specific shift (nullable) | Part of shift data model and API payloads |
| enrichment | Additional data (position, tasks) added to the clock-in response | Non-blocking data that enhances the clock-in experience |
