# CLAUDE.md - BuyerKiosk Team Flutter App

## Project Overview
Flutter mobile app (iOS/Android) for team members to manage scheduling, time clock, shifts, and team communication. Consumes mobile APIs from the buyerkiosk-web backend. Replaces third-party scheduling tools (WhenIWork/Homebase) with a homegrown solution.

## Tech Stack
- **Flutter 3.38.4** / Dart 3.10.3
- **State Management**: Riverpod 3.x (Notifier pattern)
- **Navigation**: GoRouter 17.x with auth redirects
- **Models**: Freezed 3.x (abstract classes required)
- **Entities**: Equatable (for domain layer entities)
- **HTTP**: Dio with interceptors
- **Storage**: flutter_secure_storage for tokens
- **Biometrics**: local_auth for Face ID / Touch ID
- **Geolocation**: geolocator + permission_handler (for geofenced clock-in)
- **Real-time**: Ably Flutter
- **Push Notifications**: Firebase Cloud Messaging (FCM)
- **Forms**: flutter_form_builder
- **Image Processing**: image_picker, image_cropper, flutter_image_compress

## Architecture
Clean Architecture with Riverpod:
```
lib/
├── core/           # Constants, theme, network, utils, errors, services
├── data/           # Models, datasources, repository implementations, mappers
├── domain/         # Entities (Equatable), repository interfaces
├── presentation/   # Providers, screens, widgets
└── router/         # GoRouter configuration
```

## API Configuration
- **Dev**: `https://try.buyerkiosk.com`
- **Prod**: `https://buyerkiosk.com`
- Change in: `lib/core/constants/api_constants.dart`

### Planned Endpoints
| Endpoint | Purpose |
|----------|---------|
| `/api/mobile/auth/login` | Username/password login |
| `/api/mobile/auth/logout` | Logout |
| `/api/mobile/auth/refresh` | Token refresh |
| `/api/mobile/me` | Current user info |
| `/api/mobile/me/schedule` | User's schedule |
| `/api/mobile/:typeNum/schedule/today` | Today's schedule for store |
| `/api/mobile/:typeNum/clock/in` | Clock in (geofenced) |
| `/api/mobile/:typeNum/clock/out` | Clock out |
| `/api/mobile/:typeNum/clock/break/start` | Start break |
| `/api/mobile/:typeNum/clock/break/end` | End break |
| `/api/mobile/:typeNum/timesheets` | View timesheets |

## Key Files

### Core
- `lib/core/constants/api_constants.dart` - API URLs and endpoints
- `lib/core/constants/app_constants.dart` - App-wide constants
- `lib/core/constants/ably_constants.dart` - Real-time messaging config
- `lib/core/network/api_client.dart` - Dio HTTP client
- `lib/core/network/api_interceptors.dart` - Auth & logging interceptors
- `lib/core/theme/app_theme.dart` - Material 3 theme (based on STYLE_GUIDE.md)
- `lib/core/theme/app_colors.dart` - App color palette

### Services
- `lib/core/services/storage_service.dart` - Secure storage for tokens/user data
- `lib/core/services/biometric_service.dart` - Face ID / Touch ID authentication
- `lib/core/services/geolocation_service.dart` - Location & geofencing
- `lib/core/services/push_notification_service.dart` - FCM token management & notification handling
- `lib/core/services/notification_navigation_service.dart` - Deep link routing from notifications
- `lib/core/services/image_service.dart` - Camera capture, gallery selection, image cropping & compression

### Data Models (Freezed 3.x - use `abstract class`)
- `lib/data/models/user_model.dart` - User and login response models
- `lib/data/models/notification_payload_model.dart` - FCM notification payload
- `lib/data/models/notification_preferences_model.dart` - User notification preferences
- `lib/data/models/avatar_upload_response.dart` - Avatar upload API response

### Domain Entities (Equatable - plain classes)
- `lib/domain/entities/user.dart` - User entity with store assignments
- `lib/domain/entities/auth_state.dart` - Authentication state (sealed class)
- `lib/domain/entities/notification_state.dart` - Notification system state (sealed class)
- `lib/domain/entities/notification_payload.dart` - Notification payload entity
- `lib/domain/entities/notification_preferences.dart` - User preferences entity
- `lib/domain/entities/avatar_state.dart` - Avatar upload state machine (sealed class)

### Repositories
- `lib/domain/repositories/notification_repository.dart` - Notification preferences API interface
- `lib/data/repositories/notification_repository_impl.dart` - API implementation
- `lib/domain/repositories/avatar_repository.dart` - Avatar upload/delete interface
- `lib/data/repositories/avatar_repository_impl.dart` - Avatar multipart upload implementation

### Providers (Riverpod 3.x - use `Notifier`)
- `lib/presentation/providers/auth_provider.dart` - Auth state with biometric support
- `lib/presentation/providers/notification_provider.dart` - Notification state and preferences
- `lib/presentation/providers/avatar_provider.dart` - Avatar capture/upload state machine

### Screens
- `lib/presentation/screens/auth/login_screen.dart` - Username/password login
- `lib/presentation/screens/splash/splash_screen.dart` - Splash with inline biometric auth
- `lib/presentation/screens/home/home_screen.dart` - Main dashboard
- `lib/presentation/screens/schedule/schedule_screen.dart` - Schedule view
- `lib/presentation/screens/requests/requests_screen.dart` - Requests hub (time-off + swaps)
- `lib/presentation/screens/requests/time_off_form_screen.dart` - New time-off request form
- `lib/presentation/screens/requests/request_detail_screen.dart` - Request details view
- `lib/presentation/screens/requests/swap_target_screen.dart` - Select shift to swap with
- `lib/presentation/screens/requests/swap_confirm_screen.dart` - Confirm swap request
- `lib/presentation/screens/settings/settings_screen.dart` - App settings
- `lib/presentation/screens/settings/notification_settings_screen.dart` - Notification preferences UI
- `lib/presentation/screens/settings/avatar_preview_screen.dart` - Avatar photo preview & confirmation

### Widgets
- `lib/presentation/widgets/avatar/avatar_widget.dart` - Reusable avatar display with upload progress
- `lib/presentation/widgets/avatar/avatar_bottom_sheet.dart` - Photo source selection (camera/gallery/remove)
- `lib/presentation/widgets/avatar/avatar_action_handler.dart` - Avatar action orchestration
- `lib/presentation/widgets/requests/time_off_card.dart` - Time-off request card
- `lib/presentation/widgets/requests/swap_request_card.dart` - Swap request card
- `lib/presentation/widgets/requests/team_shift_card.dart` - Team schedule shift card
- `lib/presentation/widgets/requests/swap_preview_card.dart` - Swap comparison preview
- `lib/presentation/widgets/requests/offline_queue_indicator.dart` - Offline queue status

### Router
- `lib/router/app_router.dart` - All routes, auth redirect logic

## Navigation Flow
```
/ → Splash (checks auth status, handles biometric inline)
├── /login → LoginScreen (when no tokens)
└── /home → HomeScreen (authenticated)
    ├── /schedule → ScheduleScreen
    ├── /open-shifts → OpenShiftsScreen
    ├── /requests → RequestsScreen
    │   ├── /requests/time-off/new → TimeOffFormScreen
    │   ├── /requests/:id → RequestDetailScreen
    │   ├── /requests/swap/select → SwapTargetScreen
    │   └── /requests/swap/confirm → SwapConfirmScreen
    ├── /chat → ChatScreen
    └── /settings → SettingsScreen
        ├── /settings/notifications → NotificationSettingsScreen
        └── /settings/avatar-preview → AvatarPreviewScreen (modal)
```

## Commands

```bash
# Run app
flutter run

# Run on specific device
flutter run -d iphone

# Regenerate Freezed/JSON code
dart run build_runner build --delete-conflicting-outputs

# Analyze for errors
flutter analyze

# Build APK (debug)
flutter build apk --debug

# Build iOS (debug)
flutter build ios --debug --no-codesign
```

## Riverpod 3.x Pattern
```dart
class MyNotifier extends Notifier<MyType> {
  @override
  MyType build() {
    return initialState;
  }

  void updateState(MyType newState) {
    state = newState;
  }
}

final myProvider = NotifierProvider<MyNotifier, MyType>(MyNotifier.new);
```

## Freezed 3.x Pattern (for Data Models)
```dart
@freezed
abstract class MyModel with _$MyModel {
  const factory MyModel({
    required String id,
    required String name,
  }) = _MyModel;

  factory MyModel.fromJson(Map<String, dynamic> json) => _$MyModelFromJson(json);
}
```

## Equatable Pattern (for Domain Entities)
```dart
class MyEntity extends Equatable {
  final String id;
  final String name;

  const MyEntity({required this.id, required this.name});

  @override
  List<Object?> get props => [id, name];
}
```

## Auth Flow
1. App launches splash screen, checks secure storage for access token
2. If no token → redirect to `/login`
3. If token exists and biometric enabled → show biometric prompt inline on splash
4. Biometric verified → auto-login → redirect to `/home`
5. User logs in with username/password → tokens stored
6. After first login → prompt to enable biometric

## Biometric Authentication
- Uses `local_auth` package
- Supports Face ID (iOS), Touch ID (iOS), Fingerprint (Android)
- Stored preference in SharedPreferences
- Can be enabled/disabled in Settings

## Geofencing for Clock-in
- Uses `geolocator` package for location
- Store geofence defined by lat/lng and radius
- Clock-in/out only allowed within geofence
- Permission required: Location When In Use

## Platform Configuration

### iOS (ios/Runner/Info.plist)
- `NSFaceIDUsageDescription` - Face ID permission
- `NSLocationWhenInUseUsageDescription` - Location permission
- `NSCameraUsageDescription` - Camera for avatar capture
- `NSPhotoLibraryUsageDescription` - Photo gallery access
- `UIBackgroundModes` - remote-notification, fetch

### Android (android/app/src/main/AndroidManifest.xml)
- `ACCESS_FINE_LOCATION` - GPS location
- `USE_BIOMETRIC` - Fingerprint authentication
- `POST_NOTIFICATIONS` - Push notifications
- `CAMERA` - Camera for avatar capture
- `READ_MEDIA_IMAGES` - Photo gallery access (Android 13+)
- `READ_EXTERNAL_STORAGE` - Photo gallery access (Android 12 and below)
- `UCropActivity` - Required activity for image_cropper package (must be declared)

## Brand Style Guide

The app follows `STYLE_GUIDE.md` for consistent UI. Key design tokens:

### Colors (AppColors)
| Purpose | Color | Hex |
|---------|-------|-----|
| Primary (main) | Purple 600 | `#7c3aed` |
| Primary gradient | Indigo → Purple | `#667eea → #764ba2` |
| Secondary | Teal 500 | `#14b8a6` |
| Success | Green 500 | `#22c55e` |
| Warning | Amber 500 | `#f59e0b` |
| Error | Rose 500 | `#f43f5e` |
| Info | Blue 500 | `#3b82f6` |
| Page background | Neutral 50 | `#f8fafc` |
| Text primary | Neutral 900 | `#0f172a` |
| Text secondary | Neutral 600 | `#475569` |

### Typography
- **Font**: Inter (via Google Fonts)
- **Sizes**: XS(12), SM(14), Base(16), LG(18), XL(20), 2XL(24), 3XL(30)

### Spacing (AppTheme)
```dart
AppTheme.space1  // 4px
AppTheme.space2  // 8px
AppTheme.space3  // 12px
AppTheme.space4  // 16px
AppTheme.space6  // 24px
AppTheme.space8  // 32px
```

## Common Issues
- **Freezed errors**: Ensure `abstract class` keyword (Freezed 3.x requirement)
- **Riverpod errors**: Use `Notifier` not `StateNotifier` (Riverpod 3.x)
- **Build errors**: Run `dart run build_runner build --delete-conflicting-outputs`
- **Biometric not working on simulator**: Test on real device
- **Location permission denied**: Check iOS/Android manifest permissions

## Related Backend Specs
- `../buyerkiosk-web/docs/specs/007-unified-users-auth` - User authentication system
- `../buyerkiosk-web/docs/specs/013-employee-scheduling` - Scheduling module
- `../buyerkiosk-web/docs/specs/014-manage-employees-unified` - Team member management
- `../buyerkiosk-web/docs/specs/010-employee-schedule-panel` - Schedule panel

## Push Notifications (Firebase Cloud Messaging)

### Overview
- FCM token registered with backend on login
- Token refreshed automatically and synced
- Deep link navigation from notification taps
- In-app banners for foreground notifications
- User preference management via Settings

### Notification Types
| Type | Deep Link | Payload Data |
|------|-----------|--------------|
| `schedule` | `/schedule?date=...` | `date`, `shift_id` |
| `clock` | `/home` | `action` (reminder) |
| `open_shift` | `/open-shifts/:id` | `shift_id` |
| `chat` | `/chat/:id` | `conversation_id` |

### Key Files
- `lib/firebase_options.dart` - Firebase configuration (auto-generated)
- `lib/core/constants/notification_constants.dart` - Notification type enums, reminder options
- `ios/Runner/GoogleService-Info.plist` - iOS Firebase config
- `android/app/google-services.json` - Android Firebase config

## User Avatar (Profile Photo)

### Overview
Users can capture or select a profile photo that displays throughout the app. Photos are processed client-side and uploaded via multipart form.

### Flow
1. User taps avatar in Dashboard or Settings
2. Bottom sheet shows options: Take Photo / Choose from Gallery / Remove Photo (if exists)
3. Camera/gallery opens (with permission request if needed)
4. Photo is cropped to 1:1 square with circular preview
5. Preview screen shows photo with Retake/Use Photo options
6. On confirm, photo is compressed (<1MB) and uploaded
7. Optimistic update shows local image during upload
8. Avatar displays throughout app with cached network image

### State Machine (AvatarState)
```dart
sealed class AvatarState {
  AvatarInitial        // Ready for capture
  AvatarCapturing      // Camera/gallery active
  AvatarPreviewing     // Showing preview before upload
  AvatarUploading      // Upload in progress (with progress %)
  AvatarSuccess        // Upload complete
  AvatarError          // Upload failed (with retry option)
}
```

### Key Files
- `lib/core/constants/avatar_constants.dart` - Size limits, compression settings
- `lib/core/services/image_service.dart` - Camera, gallery, crop, compress
- `lib/domain/entities/avatar_state.dart` - State machine
- `lib/presentation/providers/avatar_provider.dart` - State management
- `lib/presentation/widgets/avatar/` - AvatarWidget, AvatarBottomSheet, AvatarActionHandler
- `lib/presentation/screens/settings/avatar_preview_screen.dart` - Preview & confirmation

### API Endpoints
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/mobile/me/avatar` | POST | Upload avatar (multipart form) |
| `/api/mobile/me/avatar` | DELETE | Remove avatar |

### Image Constraints
- Max file size: 1MB (client compresses before upload)
- Format: JPEG
- Aspect ratio: 1:1 (square)
- Max dimensions: 1024x1024px

## Employee Shift Requests

### Overview
Employees can submit time-off requests and initiate shift swaps with coworkers. Both request types integrate with push notifications and support offline submission via an optimistic queue.

### Time-Off Requests
1. User navigates to Requests tab → Time Off
2. Taps "+" to create new request
3. Selects date range and category (vacation, sick, personal, etc.)
4. Adds optional notes
5. Submits → pending manager approval
6. Push notifications for status changes

### Shift Swap Requests
1. User navigates to Requests tab → Swap Shifts
2. Taps "Request Swap" to view team schedule
3. Selects a coworker's shift to swap with
4. Reviews swap preview (time difference, overtime warning)
5. Submits → pending coworker acceptance
6. Coworker accepts → pending manager approval
7. Manager approves → shifts exchanged

### State Machines
```dart
sealed class ShiftRequestsState {
  ShiftRequestsInitial    // Ready
  ShiftRequestsLoading    // Fetching
  ShiftRequestsLoaded     // Has data
  ShiftRequestsError      // Error with retry option
}

sealed class TimeOffSubmissionState {
  TimeOffSubmissionIdle       // Ready for input
  TimeOffSubmissionValidating // Checking dates
  TimeOffSubmissionSubmitting // API call in progress
  TimeOffSubmissionSuccess    // Submitted
  TimeOffSubmissionError      // Failed
}

sealed class SwapRequestState {
  SwapRequestIdle       // Ready
  SwapRequestLoading    // Loading target shifts
  SwapRequestReady      // Has eligible shifts
  SwapRequestSubmitting // Sending request
  SwapRequestSuccess    // Submitted
  SwapRequestError      // Failed
}
```

### Key Files
- `lib/core/constants/shift_request_constants.dart` - Categories, statuses, validation
- `lib/core/services/offline_queue_service.dart` - Offline submission queue
- `lib/core/services/analytics_service.dart` - Request analytics tracking
- `lib/domain/entities/time_off_request.dart` - Time-off entity
- `lib/domain/entities/shift_swap_request.dart` - Swap entity
- `lib/domain/entities/team_shift.dart` - Team schedule entity
- `lib/domain/entities/shift_request_state.dart` - State machines
- `lib/data/repositories/shift_requests_repository_impl.dart` - API implementation
- `lib/data/repositories/team_schedule_repository_impl.dart` - Team schedule API
- `lib/presentation/providers/shift_requests_provider.dart` - Request state
- `lib/presentation/providers/team_schedule_provider.dart` - Team schedule state
- `lib/presentation/screens/requests/` - Request screens
- `lib/presentation/widgets/requests/` - Request widgets

### API Endpoints
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/mobile/:typeNum/schedule/time-off` | GET | List time-off requests |
| `/api/mobile/:typeNum/schedule/time-off` | POST | Submit time-off request |
| `/api/mobile/:typeNum/schedule/time-off/:id` | DELETE | Cancel time-off request |
| `/api/mobile/:typeNum/schedule/swaps` | GET | List swap requests |
| `/api/mobile/:typeNum/schedule/swaps` | POST | Initiate swap request |
| `/api/mobile/:typeNum/schedule/swaps/:id/respond` | POST | Accept/decline swap |
| `/api/mobile/:typeNum/schedule/swaps/:id` | DELETE | Cancel swap request |
| `/api/mobile/:typeNum/schedule/team` | GET | Team schedule for swaps |

### Notification Types (additions)
| Type | Deep Link | Payload Data |
|------|-----------|--------------|
| `time_off_submitted` | `/requests/:id` | `request_id` |
| `time_off_approved` | `/requests/:id` | `request_id` |
| `time_off_denied` | `/requests/:id` | `request_id` |
| `swap_request_received` | `/requests/:id` | `request_id` |
| `swap_accepted` | `/requests/:id` | `request_id` |
| `swap_declined` | `/requests/:id` | `request_id` |
| `swap_approved` | `/requests/:id` | `request_id` |
| `swap_denied` | `/requests/:id` | `request_id` |
| `swap_expired` | `/requests/:id` | `request_id` |
| `swap_cancelled` | `/requests/:id` | `request_id` |

### Offline Support
- Time-off and swap submissions queue when offline
- Idempotency keys prevent duplicates
- Automatic retry with exponential backoff
- Visual indicator shows pending requests
- Manual retry available from error state

## Schedule (Full)

### Overview
New schedule experience introduced by spec
`003-schedule-visibility`. Replaces the simpler weekly schedule with a
single screen that supports two scopes (My / Team) and three view modes
(Day / Week / Month). Phase 5 ships only the `me` scope; Phase 6 adds the
`team` scope on top of the same widget scaffolding. Backend feeds it via
`/api/mobile/scheduling/:typeNum/full-schedule` (week + month variants,
delivered in Phase 1).

### Feature Flag
- Gated behind `teamFullScheduleEnabledProvider`
  (`lib/presentation/providers/team_full_schedule_enabled_provider.dart`).
- Defaults to `false` — the legacy `ScheduleScreen` continues to render at
  `/schedule`. Flip to `true` to opt into `FullScheduleScreen`.

### Realtime status
- The schedule Ably pipe (`kiosk_{typeNum}` subscription via
  `scheduleEventStreamProvider`) is **disabled by default** — gated
  behind `teamScheduleRealtimeEnabledProvider`
  (`lib/presentation/providers/team_schedule_realtime_enabled_provider.dart`).
- Rationale: the staff-chat Ably token currently carries only `chat:*` +
  `alert` capability grants. It does NOT include `kiosk_{typeNum}`, so
  a `.subscribe('kiosk_${typeNum}')` call against that token would be
  denied by Ably's server-side capability check (silently in the stream,
  loudly in logs).
- Until the backend either expands the staff-chat token's capability
  list OR issues a separate schedule-capable token, the UI runs on
  REST fetches + manual refresh + pull-to-refresh. Real-time deltas
  are blocked. See pending REQ in
  `../buyerkiosk-web/docs/api/mobile-agent-requests.md`.
- When backend grants the capability: flip the flag to `true`
  (per-environment or globally). Tests already exercise the pipe by
  overriding the flag to `true`.

### Screen Composition
`FullScheduleScreen` composes:
1. `ScheduleTopBar` — back chevron + "Schedule" + refresh + store-name
   affordance (auto-selects single-store, opens picker when multi-store).
2. `ScheduleScopeTabs` — My / Team toggle bound to `scheduleScopeProvider`.
3. `ScheduleViewSegmented` — Day / Week / Month bound to
   `scheduleViewModeProvider`.
4. A scope×view body (six combinations); only the three `me` cells are
   real in Phase 5. Team-scope shows a "Team view coming in Phase 6"
   placeholder.
5. Bottom navigation comes from the existing `MainShell`.

### Deep-Link Query Params
The `/schedule` route accepts (when `teamFullScheduleEnabled = true`):
- `scope=me|team` — overrides `scheduleScopeProvider` on mount.
- `view=day|week|month` — overrides `scheduleViewModeProvider`.
- `date=YYYY-MM-DD` — overrides `selectedScheduleDateProvider`.
- `weekStart=YYYY-MM-DD` — same effect as `date` (used by
  `schedule_published` push notifications).

Unknown values are silently ignored; route falls back to defaults.

### Design Tokens & Theme Reconciliation
- All schedule-feature design tokens live in
  `lib/core/constants/schedule_design_tokens.dart`.
- Brand chrome (bottom nav, top bar accents, logos) uses
  `AppColors.primary` = `#7c3aed`.
- Schedule-feature-local accents (shift cards, scope tabs, view
  segmented, selected day) use `ScheduleDesignTokens.schedulePrimary` =
  `#6B3FE3` per the design handoff. Rationale + escalation path documented
  in a code comment at the top of `schedule_design_tokens.dart`.

### Key Files
- `lib/presentation/screens/schedule/full_schedule_screen.dart` — host screen.
- `lib/presentation/widgets/schedule/` — `schedule_topbar.dart`,
  `schedule_scope_tabs.dart`, `schedule_view_segmented.dart`,
  `date_strip.dart`, `day_header.dart`, `my_shift_card.dart`,
  `shift_row.dart`, `working_with_you_section.dart`, `day_off_card.dart`,
  `week_strip.dart`, `week_day_card.dart`, `week_shift_block.dart`,
  `month_grid.dart`, `month_selected_panel.dart`.
- `lib/presentation/providers/` — `schedule_scope_provider.dart`,
  `schedule_view_mode_provider.dart`, `selected_schedule_date_provider.dart`,
  `full_team_schedule_provider.dart`,
  `team_full_schedule_enabled_provider.dart`.

### Analytics
Emitted via the existing `analyticsServiceProvider`:
- `schedule_full_view_opened` (scope, view)
- `schedule_week_navigated` (direction, view)
- `schedule_store_picked` (type_num)
- `schedule_shift_tapped` (shift_id, source)

### Team Scope (Phase 6)
The `team` scope adds three view-mode renderings on top of the Phase 5
scaffolding:

- **Team × Day** (`_TeamDayView` in `full_schedule_screen.dart`) —
  composes `DateStrip` + `DayHeader` + (optional) `OpenShiftBanner` +
  `TeamDayTimeline`. The timeline renders hour headers covering
  `earliest..latest` (minimum 8 hours per SDD §"Widget Contracts") and
  places non-overlapping shift blocks via `layoutLanes()`. `is-me`
  blocks use `schedulePrimarySoft`; open-shift blocks use
  `scheduleWarnSoft`.
- **Team × Week** (`_TeamWeekView`) — `WeekStrip` (team summary) +
  seven `WeekDayCard(scope=team)` cards, each populated with a
  `TeamWeekRow` (up to 6 avatars, `+N more` overflow chip, and a
  `${minTime} – ${maxTime}` range). Empty days render an inline "No
  shifts" marker.
- **Team × Month** (`_TeamMonthView`) — `WeekStrip` (month label) +
  `MonthGrid(scope=team)` (density-fill bar height
  `min(100, 10 + teamCount * 14)%` plus an amber open-shift dot when
  any shift on the date is open) + `MonthSelectedPanel(scope=team)`
  listing every shift via `ShiftRow` sorted by start time.

### New Team-scope Widgets
- `lib/presentation/widgets/schedule/team_day_timeline.dart` — the
  Gantt-like lane-packed timeline. All lane placements are pre-computed
  before the layout widget is returned (per skill
  `flutter-listview-itembuilder-counter-mutation`).
- `lib/presentation/widgets/schedule/open_shift_banner.dart` —
  conditionally rendered banner above the timeline.
- `lib/presentation/widgets/schedule/team_week_row.dart` — avatar
  stack + min-max time range used inside `WeekDayCard`.

### Lane Layout Utility
- `lib/core/utils/schedule_lane_layout.dart` — greedy first-fit lane
  assignment. Defensive parsing drops shifts with unparseable time
  strings (skill: `flutter-api-resilience-defensive-parsing`).
  **Duplicated** with the Live app's
  `buyerkiosk-live-flutter/lib/core/utils/schedule_lane_layout.dart` —
  the algorithm is identical and the duplication is accepted per SDD
  §"Technical Debt" for v1 to keep the two apps decoupled. If the
  algorithm grows non-trivial logic, promote to a shared package.

### Coworker Tap → Chat
`ShiftRow.onTap` is auto-wired to `tap_to_chat_helper.dart`'s
`resolveChannelFor(employeeId, typeNum, ..., navigate: true)` when the
row is used in a Team-scope context — detected by an `analyticsSource`
prefix of `team_` (e.g. `team_day_list`, `team_week_avatar`,
`team_month_detail`). `TeamDayTimeline` calls the same helper directly
for assigned-shift blocks. Open-shift taps still surface the Phase 7
placeholder snackbar.

### Pickup + Swap Sheets (Phase 7)

Phase 7 ships the two inline modal bottom sheets called out in SDD
§"Widget Contracts (Locked)" — plus the loading-spinner finalization of
tap-to-chat. The Phase 6 `"Pickup UI coming in Phase 7."` placeholders
are now replaced with real sheet invocations.

#### Shared building blocks
- `lib/presentation/widgets/schedule/shift_summary_card.dart` —
  date header (DOW · MON D) + role-tinted left border + large time
  range + hours chip + role badge + store name. Used as the header of
  BOTH sheets. Visual language borrowed from `MyShiftCard`.
- `lib/presentation/widgets/schedule/schedule_bottom_sheet.dart` —
  generic wrapper (drag handle, title row, close button) around a
  `DraggableScrollableSheet`. `ScheduleBottomSheet.show<T>(...)` is
  the canonical entry; both sheets' static `.show(...)` methods
  delegate to it.

#### PickupSheet
- File: `lib/presentation/widgets/schedule/pickup_sheet.dart`.
- Three phases via a sealed `PickupPhase` (`PickupReview`,
  `PickupConfirming`, `PickupDone`) held in a local state machine
  (the sheet manages its own state — no global Riverpod notifier).
- Backend integration uses the EXISTING
  `OpenShiftsRepository.claimShift({ typeNum, shiftId })`
  →  `POST /api/mobile/scheduling/:typeNum/open-shifts/:shiftId/claim`.
  No new endpoint required.
- Error path rolls back to review with a red banner. Plain try/catch
  (NOT `AsyncValue.guard`) per skill
  `riverpod-asyncvalue-guard-false-success` — the done-phase
  transition is gated on an explicit `result.success == true`.
- Success path auto-dismisses after 2s with a "Shift claimed." toast,
  emits `schedule_open_shift_claimed` analytics, and invokes
  `onClaimed` so the caller can refresh the underlying provider.

Call sites (all in `full_schedule_screen.dart`):
- `OpenShiftBanner.onPickupTap` (Team × Day) → first open shift of
  the day.
- `TeamDayTimeline.onOpenShiftTap` (Team × Day timeline block) →
  the tapped shift. (New optional callback; defaults preserve the
  Phase 6 placeholder snackbar so existing tests stay green.)
- `MonthSelectedPanel.onOpenShiftTap` (Team × Month detail row) →
  the tapped shift. (Same pattern — new optional callback.)
- `_openPickupSheet` helper at the bottom of
  `full_schedule_screen.dart` derives `teammateCount` from
  `full.dayFor(date)` and refreshes
  `fullTeamScheduleProvider(week-anchored params)` after a
  successful claim.

#### SwapSheet
- File: `lib/presentation/widgets/schedule/swap_sheet.dart`.
- Two phases via a sealed `SwapPhase` (`SwapPick` + selection,
  `SwapSending`, `SwapDone` + coworker name).
- Backend integration uses the EXISTING
  `ShiftRequestsRepository.initiateSwapRequest`
  → `POST /api/mobile/scheduling/:typeNum/requests/swap` with payload
  `{ myShiftId, targetShiftId, message?, idempotencyKey }`.
- Coworker list is built in `_buildCoworkerRows` BEFORE the layout
  widget returns (skill:
  `flutter-listview-itembuilder-counter-mutation`):
  - Self filtered out (compared against `store.employeeId`).
  - Open shifts filtered out.
  - Day-roster coworkers marked **Busy** (warning tint).
  - Available-elsewhere coworkers marked **Available** (success tint).
  - Both kinds tappable; backend arbitrates eligibility.
- Note: built-in TextField counter hidden; an inline `{len}/500`
  appears only past 400 chars (red past 480).
- Send button is disabled until a coworker is selected.
- Error path returns to `SwapPick` with the selection preserved and
  a red banner carrying the backend message
  (`ShiftRequestException.message` → backend codes mapped to copy by
  the repository's `_mapApiErrorCode`).

Call site:
- `MyShiftCard.onSwap` in `_MyDayView` (Me × Day view) →
  `_openSwapSheet` helper.
- `_openSwapSheet` derives `sameDayShifts` from
  `full.dayFor(shiftDate)` and `availableCoworkers` from a unique
  scan of `full.days` minus the day's roster (each coworker's
  first-seen shift carries the `targetShiftId`).
- Refreshes the week-anchored `fullTeamScheduleProvider` on success.

#### Tap-to-chat finalization (T7.6)
- `ShiftRow` was promoted to `ConsumerStatefulWidget` so it can track
  an in-flight `_resolving` state. While the
  `tap_to_chat_helper.resolveChannelFor` future is pending, the
  right-edge start/end time pills are replaced with a small purple
  `CircularProgressIndicator` (20×20).
- Behavior unchanged: `team_*`-prefixed analytics sources auto-route
  to the helper with `navigate: true`; `working_with_you` rows stay
  inert; self-shift tap skips the resolver; null employeeId on a
  non-open shift surfaces a clear toast; error toasts surface via
  the helper's pre-captured ScaffoldMessenger.
- Coverage: Day timeline blocks (Phase 6), Week-via-Month-panel rows
  (Phase 6 `_OpenShiftRow` excluded by design — open shifts go to
  PickupSheet), Month-panel rows. Every assigned-coworker row in a
  Team-scope context now exercises the helper.

#### Analytics events added
- `schedule_open_shift_claimed` — `{ shift_id, type_num }`.
- `schedule_swap_initiated_inline` — `{ my_shift_id, target_shift_id,
  has_note, overtime_warning }`.

Both are logged via the existing `analyticsServiceProvider`'s generic
`logEvent(name, props)` channel. They are NOT yet first-class methods
on `AnalyticsService` — promote to typed methods (and add to
`AnalyticsEvents`) if the funnel-reporting story for Phase 9 demands
it.

#### Realtime reducer note
The backend does NOT broadcast a dedicated `OpenShiftClaimedEvent` —
claim is handled as a `ShiftUpdatedEvent` (the shift transitions
from `employeeId: null` → assigned employeeId, with bumped
`updatedAt`). The existing `_upsertShift` branch in
`full_team_schedule_provider.dart` already handles this correctly,
and the `isOpenShift` getter is derived from `employeeId == null`
(PR #1 round 3 P1 #3). No new event variant needed in this spec.

#### Spec Reference
`../docs/specs/003-schedule-visibility/` — PRD, SDD (§"Team-App UI Design
(Locked)" is normative), and implementation plan.

## Inter-Agent Communication (Backend Coordination)

The backend codebase lives in two locations for parallel development:
- `../buyerkiosk-web` - Primary backend (try.buyerkiosk.com)
- `../buyerkiosk-web2` - Secondary backend (try.buyerkiosk.com)

### API Request Protocol

When you need API details, endpoint specifications, or backend changes:

1. **Check existing docs first**: `../buyerkiosk-web/docs/api/MOBILE_API.md`
2. **If not found, write a request** to: `../buyerkiosk-web/docs/api/mobile-agent-requests.md`

### Request Format

```markdown
### REQ-[YYYYMMDD]-[NN]: [Brief Title]

**Status**: `pending`
**Requested**: [Date]
**Mobile Feature**: [Spec ID or feature name]
**Priority**: `high` | `medium` | `low`

#### Context
[What mobile feature needs this API]

#### Questions
1. [Specific question about endpoint, payload, response, etc.]

#### Backend Response
*Awaiting response*
```

### When to Use This

- Need endpoint details not in existing API docs
- Requesting new endpoints for mobile features
- Clarifying request/response payload formats
- Asking about error codes and edge cases
- Coordinating API changes for new features

### Key Files
- `../buyerkiosk-web/docs/api/MOBILE_API.md` - Main mobile API reference
- `../buyerkiosk-web/docs/api/mobile-agent-requests.md` - Send requests to backend
- `docs/backend-api-updates.md` - **CHECK THIS** for updates from backend

### Check for Backend Updates

**At session start**, check `docs/backend-api-updates.md` for:
- New endpoints you should implement
- Breaking changes requiring updates
- New response fields to consume
- Deprecation notices to address

Backend agents write here when they make mobile-affecting changes.

## Team Chat (Real-time Messaging via Ably)

### Overview
Team members can communicate in real-time via channels. The feature is implemented as a separate `buyerkiosk_chat` package for clean separation and reusability.

### Architecture
```
packages/buyerkiosk_chat/     # Standalone chat package
├── entities/                 # Channel, Message, Member, MentionItem
├── repositories/             # ChatRepository interface
├── services/                 # AblyRealtimeService
├── state/                    # ChatState, ChannelMessagesState, etc.
└── exceptions/               # ChatException hierarchy

lib/presentation/
├── providers/
│   ├── chat_providers.dart   # Riverpod provider wiring
│   └── chat_notifier.dart    # ChatNotifier (main state manager)
├── screens/chat/
│   ├── chat_screen.dart      # Channel list
│   ├── channel_screen.dart   # Message thread
│   ├── channel_settings_screen.dart
│   ├── channel_search_screen.dart
│   └── create_channel_screen.dart
└── widgets/chat/             # Reusable chat widgets
```

### Features
- **Channels**: Public/private channels, pin/mute, unread counts
- **Messages**: Send, edit (24h window), delete, reply-to-thread
- **Reactions**: Emoji reactions (👍 ❤️ 😄 🎉 😮 😢)
- **Mentions**: @user mentions with notification deep links
- **Typing Indicators**: Real-time "typing..." with presence
- **Search**: Full-text search within channel
- **Offline Resilience**: Optimistic updates, queue pending messages
- **Connection Recovery**: Auto-reconnect with state resync

### State Machine (ChatCompositeState)
```dart
class ChatCompositeState {
  ChannelsState channelsState;      // Initial/Loading/Loaded/Error
  ChannelMessagesState messagesState;
  MentionsState mentionsState;
  ChatConnectionState connectionState;
  List<Member> typingUsers;
  int? activeChannelId;
  String? lastError;
  int? editingMessageId;
}
```

### Real-time Events
| Event | Description |
|-------|-------------|
| `ChatMessageCreatedEvent` | New message from another user |
| `ChatMessageUpdatedEvent` | Message edited |
| `ChatMessageDeletedEvent` | Message deleted |
| `ChatReactionAddedEvent` | Reaction added |
| `ChatReactionRemovedEvent` | Reaction removed |
| `ChatMemberAddedEvent` | User joined channel |
| `ChatMemberRemovedEvent` | User left/removed from channel |

### Notification Types
| Type | Deep Link | Payload Data |
|------|-----------|--------------|
| `chat` | `/chat/channel/:channelId` | `channel_id` |
| `chat_message` | `/chat/channel/:channelId?messageId=:id` | `channel_id`, `message_id` |
| `chat_mention` | `/chat/channel/:channelId?messageId=:id` | `channel_id`, `message_id` |

### API Endpoints
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/mobile/:typeNum/chat/channels` | GET | List channels |
| `/api/mobile/:typeNum/chat/channels/:id` | GET | Channel details + members |
| `/api/mobile/:typeNum/chat/channels/:id/messages` | GET | Paginated messages |
| `/api/mobile/:typeNum/chat/channels/:id/messages` | POST | Send message |
| `/api/mobile/:typeNum/chat/channels/:id/messages/:mid` | PATCH | Edit message |
| `/api/mobile/:typeNum/chat/channels/:id/messages/:mid` | DELETE | Delete message |
| `/api/mobile/:typeNum/chat/channels/:id/messages/:mid/reactions` | POST | Add reaction |
| `/api/mobile/:typeNum/chat/channels/:id/messages/:mid/reactions` | DELETE | Remove reaction |
| `/api/mobile/:typeNum/chat/channels/:id/mute` | POST | Mute/unmute |
| `/api/mobile/:typeNum/chat/channels/:id/search` | GET | Search messages |
| `/api/mobile/:typeNum/chat/mentions` | GET | User's mentions |

### Key Files
- `packages/buyerkiosk_chat/` - Standalone chat package
- `lib/presentation/providers/chat_providers.dart` - Provider wiring
- `lib/presentation/providers/chat_notifier.dart` - Main state manager
- `lib/domain/entities/chat_composite_state.dart` - Composite state
- `lib/core/services/chat_preferences_service.dart` - Local pin preferences
- `test/chat/chat_notifier_test.dart` - 40 comprehensive unit tests

### Testing
```bash
# Run chat tests
flutter test test/chat/

# All chat tests
flutter test --name "Chat"
```

## TODO / Future Features
- [x] Push notifications via Firebase Cloud Messaging
- [x] Notification preferences UI
- [x] User avatar (profile photo) with camera capture
- [x] Time-off requests
- [x] Shift swap requests
- [x] Team chat (real-time messaging via Ably)
- [ ] Timesheet viewing and approval
- [ ] Manager override for clock actions
- [ ] Quiet hours time pickers (notification preferences)
