# PROMPT 02 — Domain Models, Enums & Interfaces
## Prerequisites: Prompt 01 complete

---

## Your Task
Implement all TypeScript domain models for CollabSpace. These mirror the C# backend
entities exactly. Read `@specs/api_reference.md` for backend shapes.

---

## File: `core/enums/collab.enums.ts`

Generate these enums with **string values** (readable in logs and API responses):

```typescript
export enum SessionType    { AdHoc = 'AdHoc', Scheduled = 'Scheduled' }
export enum SessionStatus  { Pending = 'Pending', Active = 'Active', Paused = 'Paused',
                             Ended = 'Ended', Abandoned = 'Abandoned', Cancelled = 'Cancelled' }
export enum SessionMode    { CoBrowse = 'CoBrowse', ScreenShare = 'ScreenShare', Hybrid = 'Hybrid' }
export enum ParticipantRole { Host = 'Host', Presenter = 'Presenter',
                              CoEditor = 'CoEditor', Viewer = 'Viewer' }
export enum InviteStatus   { Pending = 'Pending', Accepted = 'Accepted', Declined = 'Declined',
                             Expired = 'Expired', Revoked = 'Revoked' }
export enum ControlConsentMode { NotRequired = 'NotRequired', Required = 'Required',
                                  RoleDependent = 'RoleDependent' }
export enum SessionEventType {
  SessionCreated = 'SessionCreated', SessionStarted = 'SessionStarted',
  SessionPaused = 'SessionPaused', SessionResumed = 'SessionResumed',
  SessionEnded = 'SessionEnded', SessionAbandoned = 'SessionAbandoned',
  ParticipantJoined = 'ParticipantJoined', ParticipantLeft = 'ParticipantLeft',
  RoleChanged = 'RoleChanged', ControlGranted = 'ControlGranted',
  ControlRevoked = 'ControlRevoked', ControlRequested = 'ControlRequested',
  RouteSync = 'RouteSync', FormSync = 'FormSync',
  ModeChanged = 'ModeChanged', InviteSent = 'InviteSent',
  InviteAccepted = 'InviteAccepted', InviteDeclined = 'InviteDeclined',
}
```

Also generate these **pure helper functions** (no side effects, fully typed):
```typescript
isSessionActive(status: SessionStatus): boolean
canUserEdit(role: ParticipantRole, hasControl: boolean): boolean
getRoleLabel(role: ParticipantRole): string
getRolePriority(role: ParticipantRole): number  // Host=0, Presenter=1, CoEditor=2, Viewer=3
getModeIcon(mode: SessionMode): string           // emoji: 🌐 📺 ⚡
getModeLabel(mode: SessionMode): string
getStatusColor(status: SessionStatus): 'teal' | 'amber' | 'blue' | 'muted'
```

---

## File: `core/models/session.model.ts`

```typescript
// Generate ALL of these — fully typed, readonly properties, JSDoc on key fields:

export interface CollabSession {
  readonly sessionId: string;
  readonly sessionCode: string;        // Format: "XK-4291"
  readonly title: string;
  readonly sessionType: SessionType;
  readonly status: SessionStatus;
  readonly mode: SessionMode;
  readonly contextModuleCode: string | null;   // e.g. "AP", "HRMS"
  readonly contextRecordId: string | null;
  readonly contextRecordLabel: string | null;  // e.g. "Invoice #4521"
  readonly contextRouteUrl: string | null;
  readonly scheduledAt: Date | null;
  readonly scheduledDurationMinutes: number | null;
  readonly startedAt: Date | null;
  readonly endedAt: Date | null;
  readonly createdAt: Date;
  readonly createdBy: string;          // userId
  readonly maxParticipants: number;
  readonly participants: readonly CollabParticipant[];
}

export interface CollabSessionSummary {
  // Lightweight version for list views — no full participant array
  readonly sessionId: string;
  readonly sessionCode: string;
  readonly title: string;
  readonly status: SessionStatus;
  readonly mode: SessionMode;
  readonly contextModuleCode: string | null;
  readonly contextRecordLabel: string | null;
  readonly scheduledAt: Date | null;
  readonly startedAt: Date | null;
  readonly activeParticipantCount: number;
}

export interface CreateSessionRequest {
  title: string;
  sessionType: SessionType;
  mode: SessionMode;
  contextModuleCode?: string;
  contextRecordId?: string;
  contextRecordLabel?: string;
  contextRouteUrl?: string;
  scheduledAt?: Date;
  scheduledDurationMinutes?: number;
  maxParticipants?: number;
}

export interface UpdateSessionStatusRequest {
  action: 'start' | 'pause' | 'resume' | 'end';
}

export interface JoinSessionResponse {
  session: CollabSession;
  yourRole: ParticipantRole;
  youHaveControl: boolean;
  yourCursorColor: string;
  config: CollabConfigClient;
}
```

---

## File: `core/models/participant.model.ts`

```typescript
export interface CollabParticipant {
  readonly participantId: string;
  readonly sessionId: string;
  readonly userId: string;
  readonly displayName: string;
  readonly role: ParticipantRole;
  readonly isActive: boolean;
  readonly hasControl: boolean;
  readonly isFollowingHost: boolean;
  readonly cursorColor: string;     // hex, assigned at join
  readonly joinedAt: Date;
  readonly leftAt: Date | null;
}

// In-memory only — not persisted to backend
export interface ParticipantPresence {
  readonly userId: string;
  readonly displayName: string;
  readonly cursorColor: string;
  cursorX: number;                  // viewport % (mutable for perf)
  cursorY: number;
  lastActiveAt: Date;
  isOnline: boolean;
}

export interface GrantControlRequest {
  targetUserId: string;
}
```

---

## File: `core/models/invite.model.ts`

```typescript
export interface CollabSessionInvite {
  readonly inviteId: string;
  readonly sessionId: string;
  readonly sessionTitle: string;
  readonly inviteToken: string;
  readonly joinUrl: string;
  readonly offeredRole: ParticipantRole;
  readonly status: InviteStatus;
  readonly expiresAt: Date;
  readonly createdAt: Date;
}

export interface InviteUsersRequest {
  userIds: string[];
  offeredRole: ParticipantRole;
  expiryMinutes?: number;
}

export interface CreateLinkInviteRequest {
  offeredRole: ParticipantRole;
  expiryMinutes?: number;
}

export interface RespondToInviteRequest {
  accept: boolean;
}
```

---

## File: `core/models/hub-payloads.model.ts`

```typescript
// These MUST match the C# HubPayloads exactly (camelCase from JSON serializer)
export interface RouteSyncPayload {
  sessionId: string;
  triggeredByUserId: string;
  route: string;
  moduleCode: string | null;
  recordId: string | null;
  recordLabel: string | null;
  timestamp: number;             // Unix ms
}

export interface CursorPayload {
  sessionId: string;
  userId: string;
  cursorColor: string;
  xPercent: number;              // 0–100, % of viewport width
  yPercent: number;
  elementPath: string | null;
  timestamp: number;
}

export interface FormSyncPayload {
  sessionId: string;
  triggeredByUserId: string;
  formId: string;
  fieldPath: string;             // dot notation: "invoice.lineItems[0].amount"
  value: unknown;
  timestamp: number;
}

export interface ChatMessage {
  sessionId: string;
  fromUserId: string;
  fromDisplayName: string;
  cursorColor: string;
  text: string;
  sentAt: Date;
}

export interface AnnotationPayload {
  sessionId: string;
  fromUserId: string;
  cursorColor: string;
  type: 'draw' | 'highlight' | 'arrow' | 'clear';
  data: string | null;
  timestamp: number;
}
```

---

## File: `core/models/collab-config.model.ts`

```typescript
export interface CollabConfig {
  readonly orgId: string;
  maxParticipantsDefault: number;
  maxParticipantsAbsolute: number;
  sessionTimeoutMinutes: number;
  inviteLinkExpiryMinutes: number;
  maxScheduledSessionsPerUser: number;
  allowScreenShare: boolean;
  allowAnonymousJoin: boolean;
  allowGuestJoinViaLink: boolean;
  enableAuditLog: boolean;
  enableCoBrowse: boolean;
  enableInSessionChat: boolean;
  enableAnnotations: boolean;
  enableCursorBroadcast: boolean;
  enableFormSync: boolean;
  controlConsentMode: ControlConsentMode;
  allowedModules: string[] | null;    // null = all modules allowed
}

// Subset sent to client with JoinSessionResponse
export interface CollabConfigClient {
  allowScreenShare: boolean;
  enableCoBrowse: boolean;
  enableInSessionChat: boolean;
  enableAnnotations: boolean;
  enableCursorBroadcast: boolean;
  enableFormSync: boolean;
  controlConsentMode: ControlConsentMode;
}

export type UpdateCollabConfigRequest = Omit<CollabConfig, 'orgId'>;

export const DEFAULT_COLLAB_CONFIG: Omit<CollabConfig, 'orgId'> = {
  maxParticipantsDefault: 10,
  maxParticipantsAbsolute: 25,
  sessionTimeoutMinutes: 30,
  inviteLinkExpiryMinutes: 60,
  maxScheduledSessionsPerUser: 20,
  allowScreenShare: true,
  allowAnonymousJoin: false,
  allowGuestJoinViaLink: true,
  enableAuditLog: true,
  enableCoBrowse: true,
  enableInSessionChat: true,
  enableAnnotations: true,
  enableCursorBroadcast: true,
  enableFormSync: true,
  controlConsentMode: ControlConsentMode.RoleDependent,
  allowedModules: null,
};
```

---

## File: `core/models/collab-bridge.model.ts`

```typescript
// The public contract that other ERP modules use
export interface CollabContext {
  moduleCode: string;          // 'AP' | 'HRMS' | 'CRM' | etc.
  recordId?: string;
  recordLabel?: string;        // "Invoice #4521"
  currentRoute: string;
}

export interface FieldChange {
  formId: string;
  fieldPath: string;           // dot notation
  value: unknown;
}

// Interface for the bridge service (used for mocking in tests)
export interface ICollabBridgeService {
  readonly isSessionActive: Signal<boolean>;
  readonly hasControl: Signal<boolean>;
  readonly activeSession: Signal<CollabSessionSummary | null>;
  readonly participantCount: Signal<number>;
  startSession(context: CollabContext): Promise<void>;
  notifyRouteChange(context: CollabContext): void;
  notifyFormChange(change: FieldChange): void;
  registerForm(formId: string, form: AbstractControl): void;
  unregisterForm(formId: string): void;
  endSession(): Promise<void>;
}
```

---

## File: `core/services/collab-api.mapper.ts`

Generate a mapper that converts raw API JSON responses to typed models:
```typescript
// API returns dates as ISO strings — convert to Date
// Handle both camelCase (from .NET default) and any field variations
// Functions to generate:
// mapSession(raw: unknown): CollabSession
// mapSessionSummary(raw: unknown): CollabSessionSummary
// mapParticipant(raw: unknown): CollabParticipant
// mapInvite(raw: unknown): CollabSessionInvite
// mapConfig(raw: unknown): CollabConfig
// mapJoinResponse(raw: unknown): JoinSessionResponse
// mapChatMessage(raw: unknown): ChatMessage
// Each function validates required fields and throws descriptive error if missing
```

---

## File: `core/models/index.ts`

Export everything from all model files above.

---

## Verification Checklist
- [ ] No `any` types anywhere
- [ ] All `Date` fields typed as `Date` (not string)
- [ ] Readonly on all interface properties that should not be mutated
- [ ] Enums use string values (not numeric)
- [ ] Helper functions are pure (no side effects, no injection)
- [ ] Mapper handles null/undefined API fields gracefully
