# PROMPT 17 — Routing & Guards
## Prerequisites: All previous prompts complete

---

## Your Task
Implement routing, guards, and lazy-loading for the collaboration module.

---

## `collaboration.routes.ts` — Complete Route Config
```typescript
export const COLLABORATION_ROUTES: Routes = [
  {
    path: '',
    title: 'Collaboration Hub',
    canActivate: [authGuard],
    loadComponent: () => import('./pages/dashboard/collab-dashboard.component')
      .then(m => m.CollabDashboardComponent),
  },
  {
    path: 'new',
    title: 'New Session',
    canActivate: [authGuard],
    canDeactivate: [unsavedSessionGuard],
    loadComponent: () => import('./pages/session-create/session-create.component')
      .then(m => m.SessionCreateComponent),
  },
  {
    path: 'join/:token',
    title: 'Join Session',
    canActivate: [tokenJoinGuard],
    loadComponent: () => import('./pages/join-via-link/join-via-link.component')
      .then(m => m.JoinViaLinkComponent),
  },
  {
    path: 'config',
    title: 'Collaboration Configuration',
    canActivate: [authGuard, collabAdminGuard],
    data: { requiresRole: 'CollabAdmin' },
    loadComponent: () => import('./pages/config/collab-config.component')
      .then(m => m.CollabConfigComponent),
  },
  { path: '**', redirectTo: '' },
];
```

## Guards to Generate

### `core/guards/collab-admin.guard.ts`
```typescript
// Functional guard
export const collabAdminGuard: CanActivateFn = (route, state) => {
  const auth   = inject(AuthService);
  const router = inject(Router);
  const uiState = inject(CollabUiState);

  if (auth.hasRole('CollabAdmin')) return true;

  uiState.showToast({ type: 'error', message: 'Admin access required for this page.' });
  return router.createUrlTree(['/collaboration']);
};
```

### `core/guards/token-join.guard.ts`
```typescript
// Validates token before loading join page
// Extracts :token from route params
// Makes HEAD/GET request to verify token is valid and not expired
// On invalid: redirect to /collaboration?error=invite_invalid with toast
// On valid: return true
export const tokenJoinGuard: CanActivateFn = (route) => {
  const inviteService = inject(CollabInviteService);
  const router        = inject(Router);
  const token         = route.paramMap.get('token')!;

  return inviteService.validateToken(token).pipe(
    map(valid => valid ? true : router.createUrlTree(['/collaboration'],
      { queryParams: { error: 'invite_expired' } })),
    catchError(() => of(router.createUrlTree(['/collaboration'],
      { queryParams: { error: 'invite_invalid' } }))),
  );
};
// Add validateToken(token): Observable<boolean> to CollabInviteService
```

### `core/guards/unsaved-session.guard.ts`
```typescript
// canDeactivate for Create Session page
// Only prompts if form is dirty AND not submitted
export const unsavedSessionGuard: CanDeactivateFn<SessionCreateComponent> =
  (component) => {
    if (!component.form.dirty || component.submitted()) return true;
    const dialog = inject(Dialog);
    const ref = dialog.open(ConfirmDialogComponent, {
      data: { message: 'Leave without creating the session?', confirmLabel: 'Leave' }
    });
    return ref.closed.pipe(map(result => !!result));
  };
```

## App-level integration (generate instructions):
```typescript
// In app.routes.ts — add:
{
  path: 'collaboration',
  loadChildren: () =>
    import('./modules/collaboration/collaboration.routes')
      .then(m => m.COLLABORATION_ROUTES),
}

// In app.config.ts — add:
provideCollaboration({
  hubUrl: environment.collabHubUrl ?? '/hubs/collab',
  apiBaseUrl: environment.apiUrl ?? '',
  logLevel: environment.production ? 'warning' : 'info',
})
```

## `pages/join-via-link/join-via-link.component.ts`
```typescript
// Simple page that:
// 1. Reads :token from route params (use input binding)
// 2. Shows loading state
// 3. Calls sessionService.joinViaToken(token)
// 4. On success: navigate to /collaboration + show toolbar
// 5. On error: show error message + link back to dashboard
// Uses: withComponentInputBinding() so token is @Input() token: string
```

---
---

# PROMPT 18 — Integration Guide
## Prerequisites: All prompts complete

---

## Your Task
Generate a comprehensive integration guide showing how any existing ERP module
integrates CollabSpace. Create as `INTEGRATION_GUIDE.md` in the project root.

---

## Document Must Cover

### 1. One-time setup (per module)
```typescript
// In module's component imports array, add:
CollabViewOnlyDirective

// No need to import CollabBridgeService in imports — it's providedIn: 'root'
```

### 2. Minimal integration (read-only awareness — 10 lines)
```typescript
// Just showing collab state, no form sync:
private readonly collab = inject(CollabBridgeService);
readonly inSession = this.collab.isSessionActive;
readonly canEdit   = this.collab.hasControl;

// In template:
@if (!inSession()) { <button (click)="startCollab()">👥 Collaborate</button> }
@if (inSession() && !canEdit()) { <div class="view-only-banner">Viewing only</div> }
<input formControlName="amount" collabViewOnly />
```

### 3. Full integration (form sync)
```typescript
ngOnInit() {
  this.collab.notifyRouteChange({ moduleCode: 'AP', recordId: ..., currentRoute: ... });
  this.collab.registerForm('ap-invoice-form', this.form);
}
ngOnDestroy() {
  this.collab.unregisterForm('ap-invoice-form');
}
```

### 4. Starting a session from a record
```typescript
async collaborateOnThisRecord(): Promise<void> {
  await this.collab.startSession({
    moduleCode: 'AP',
    recordId: this.invoice().id,
    recordLabel: `Invoice #${this.invoice().number}`,
    currentRoute: this.router.url,
  });
}
```

### 5. Module code reference table
| Module | Code | Notes |
|--------|------|-------|
| Accounts Payable | AP | |
| Accounts Receivable | AR | |
| HRMS | HRMS | |
| CRM | CRM | |
| Inventory | INV | |
| Procurement | PROC | |
| ... all 40+ modules | | Follow same pattern |

### 6. Fields to EXCLUDE from sync
```html
<!-- Add data-collab-exclude to inputs that should not sync: -->
<!-- Passwords, sensitive personal data, audit fields, computed readonly fields -->
<input formControlName="password" data-collab-exclude />

<!-- The CoBrowseService form watcher respects this attribute -->
<!-- Add this check in the deepDiff utility: skip paths where element has data-collab-exclude -->
```

### 7. Troubleshooting section
- Session doesn't start → check hubUrl in environment
- Form not syncing → check registerForm is called after form is built
- Cursor not showing → check enableCursorBroadcast in org config
- Can't grant control → check user has Host role in session

---
---

# PROMPT 19 — Tests
## Prerequisites: All previous prompts complete

---

## Your Task
Generate comprehensive Jest test suites for all critical paths.

---

## Test File: `core/state/session.state.spec.ts`
```typescript
// Test: initial state is correct
// Test: setCurrentSession updates currentSession
// Test: hasActiveSession computed is false initially, true after setCurrentSession
// Test: clearCurrentSession resets to null
// Test: addParticipant adds to currentSession.participants
// Test: removeParticipant marks isActive false (not splice)
// Test: transferControl updates hasControl on both participants atomically
// Test: participantCount computed follows participants length
// Test: reset() returns all signals to initial values
```

## Test File: `core/services/collab-hub.service.spec.ts`
```typescript
// Mock @microsoft/signalr: jest.mock('@microsoft/signalr')
// MockHubConnectionBuilder that returns mock HubConnection

// Test: connect() builds connection with correct URL
// Test: connect() calls setupEventHandlers
// Test: syncRoute() guards when disconnected (warns, does not throw)
// Test: syncRoute() calls connection.invoke('SyncRoute', payload) when connected
// Test: participantJoined$ emits when hub fires 'ParticipantJoined'
// Test: onreconnected calls joinSession with current session ID
// Test: ngOnDestroy completes all subjects
// Test: heartbeat interval cleared in ngOnDestroy
```

## Test File: `core/services/collab-cobrowse.service.spec.ts`
```typescript
// Mock: Router (provide RouterTestingModule)
// Mock: CollabHubService
// Mock: ParticipantState.hasControl

// Test: route broadcast fires on NavigationEnd
// Test: route broadcast does NOT fire when hasControl = false
// Test: incoming route navigates router when isFollowingHost = true
// Test: incoming route does NOT navigate when isFollowingHost = false
// Test: form sync uses emitEvent: false when applying incoming changes
// Test: form sync does not emit outgoing for a change just received (feedback loop)
// Test: cursor throttle — advance fakeTimers 60ms, verify only 1 call per throttle window
// Test: stopCoBrowse clears all subscriptions and form registry
```

## Test File: `components/toolbar/collab-toolbar.component.spec.ts`
```typescript
// Test: toolbar is not rendered when sessionState.hasActiveSession = false
// Test: toolbar renders when hasActiveSession = true
// Test: elapsed timer shows "00:00" on start
// Test: fakeAsync — advance 1 minute, elapsed shows "01:00"
// Test: endSession() calls sessionService.updateStatus with 'end'
// Test: chat badge shows unreadChatCount value
// Test: chat badge hidden when count = 0
// Test: toggleScreenShare calls navigator.mediaDevices.getDisplayMedia
```

## Test File: `core/services/collab-bridge.service.spec.ts`
```typescript
// Test: isSessionActive reflects sessionState.hasActiveSession
// Test: notifyRouteChange is no-op when isSessionActive = false
// Test: notifyFormChange is no-op when isSessionActive = false
// Test: startSession calls sessionService.createSession + uiState.showToolbar + coBrowse.startCoBrowse
// Test: endSession calls updateStatus('end') + stateManager.resetAll
// Test: hasControl reflects participantState.hasControl
// Test: CollabBridgeService can be safely injected when no session is active
```

## Test File: `shared/directives/collab-view-only.directive.spec.ts`
```typescript
// Setup: TestBed with mock CollabBridgeService
// Test: input is NOT disabled when no session active
// Test: input IS disabled when session active + no control
// Test: input is NOT disabled when session active + has control
// Test: disabled state updates reactively when hasControl signal changes
// Test: collab-readonly CSS class applied/removed correctly
```

## Shared Test Utilities: `testing/collab-test-utils.ts`
```typescript
// Factory functions — fully typed, no any:
createMockSession(overrides?: Partial<CollabSession>): CollabSession
createMockParticipant(overrides?: Partial<CollabParticipant>): CollabParticipant
createMockInvite(overrides?: Partial<CollabSessionInvite>): CollabSessionInvite
createMockConfig(overrides?: Partial<CollabConfig>): CollabConfig

// Mock service factories:
createMockHubService(): jest.Mocked<CollabHubService>
  // All Subjects return empty observables
  // All invoke methods are jest.fn() returning Promise.resolve()
  // isConnected = signal(true)

createMockSessionState(): jest.Mocked<SessionState>
  // Signals return sensible defaults
  // All mutation methods are jest.fn()

createMockCollabBridgeService(): jest.Mocked<CollabBridgeService>
  // isSessionActive = signal(false)
  // hasControl = signal(false)
  // startSession, endSession, notifyRouteChange = jest.fn()
```

---

## General Test Constraints
- No `any` in test files
- Use `TestBed.runInInjectionContext()` for testing functions that use `inject()`
- Use `fakeAsync` + `tick()` for timers
- Use `HttpClientTestingModule` for HTTP
- Each `describe` block has a `beforeEach` that resets mocks
- Target: 80%+ branch coverage on all service files
