BioFlow Requirements
lib\application\view_models\navigation_notifier.dart
Source file coverage
Path:
lib/application/view_models/navigation_notifier.dart
Lines:
406
Non-empty lines:
334
Non-empty lines covered with requirements:
334 / 334 (100.0%)
Functions:
0
Functions covered by requirements:
0 / 0 (0.0%)
1
import 'package:bioflow_pro/application/providers/ui/ui_providers.dart';
2
import 'package:bioflow_pro/domain/entities/ui/navigation_state.dart';
3
import 'package:bioflow_pro/domain/entities/ui/overlay_state.dart';
4
import 'package:flutter_riverpod/flutter_riverpod.dart';
5
 
6
// @relation(ARCH-010, scope=file)
7
/// Notifier for managing navigation state with route history tracking
8
class NavigationNotifier extends Notifier<NavigationState> {
9
  @override
10
  NavigationState build() {
11
    return NavigationState.initial();
12
  }
13
 
14
  /// Navigate to a new route with optional parameters
15
  Future<void> navigateTo(
16
    String newRoute, {
17
    Map<String, dynamic>? params,
18
  }) async {
19
    state = state.navigateTo(newRoute, params: params);
20
  }
21
 
22
  /// Navigate back to the previous route
23
  Future<void> goBack() async {
24
    if (state.canGoBack) {
25
      state = state.goBack();
26
    }
27
  }
28
 
29
  /// Replace current route without adding to history
30
  Future<void> replaceCurrent(
31
    String newRoute, {
32
    Map<String, dynamic>? params,
33
  }) async {
34
    state = state.replaceCurrent(newRoute, params: params);
35
  }
36
 
37
  /// Reset navigation to root route
38
  Future<void> resetToRoot() async {
39
    state = state.resetToRoot();
40
  }
41
 
42
  /// Clear route history but keep current route
43
  Future<void> clearHistory() async {
44
    state = state.clearHistory();
45
  }
46
 
47
  /// Update route parameters without changing route
48
  Future<void> updateParams(Map<String, dynamic> newParams) async {
49
    state = state.updateParams(newParams);
50
  }
51
 
52
  /// Add or update a single route parameter
53
  Future<void> setParam(String key, dynamic value) async {
54
    state = state.setParam(key, value);
55
  }
56
 
57
  /// Remove a route parameter
58
  Future<void> removeParam(String key) async {
59
    state = state.removeParam(key);
60
  }
61
 
62
  /// Navigate to patients overlay
63
  Future<void> navigateToPatients({Map<String, dynamic>? params}) async {
64
    await navigateTo('/patients', params: params);
65
  }
66
 
67
  /// Navigate to montages overlay
68
  Future<void> navigateToMontages({Map<String, dynamic>? params}) async {
69
    await navigateTo('/montages', params: params);
70
  }
71
 
72
  /// Navigate to settings overlay
73
  Future<void> navigateToSettings({Map<String, dynamic>? params}) async {
74
    await navigateTo('/settings', params: params);
75
  }
76
 
77
  /// Navigate to activities overlay
78
  Future<void> navigateToActivities({Map<String, dynamic>? params}) async {
79
    await navigateTo('/activities', params: params);
80
  }
81
 
82
  /// Navigate to home/root
83
  Future<void> navigateToHome() async {
84
    await navigateTo('/');
85
  }
86
 
87
  /// Get current route
88
  String get currentRoute => state.currentRoute;
89
 
90
  /// Get previous route
91
  String? get previousRoute => state.previousRoute;
92
 
93
  /// Get route history
94
  List<String> get routeHistory => state.routeHistory;
95
 
96
  /// Get route parameters
97
  Map<String, dynamic> get routeParams => state.routeParams;
98
 
99
  /// Check if at root route
100
  bool get isAtRoot => state.isAtRoot;
101
 
102
  /// Check if at overlay route
103
  bool get isAtOverlay => state.isAtOverlay;
104
 
105
  /// Get current overlay type
106
  String? get currentOverlayType => state.currentOverlayType;
107
 
108
  /// Check if can navigate back
109
  bool get canGoBack => state.canGoBack;
110
 
111
  /// Get route depth
112
  int get routeDepth => state.routeDepth;
113
 
114
  /// Check if route has parameters
115
  bool get hasParams => state.hasParams;
116
 
117
  /// Get typed parameter value
118
  T? getParam<T>(String key) {
119
    return state.getParam<T>(key);
120
  }
121
 
122
  /// Check if specific parameter exists
123
  bool hasParam(String key) => state.hasParam(key);
124
 
125
  /// Get time since last navigation
126
  Duration? get timeSinceLastNavigation => state.timeSinceLastNavigation;
127
 
128
  /// Check if navigation was recent
129
  bool get wasRecentNavigation => state.wasRecentNavigation;
130
 
131
  /// Get formatted history
132
  String get formattedHistory => state.formattedHistory;
133
 
134
  /// Get current route segments
135
  List<String> get currentRouteSegments => state.currentRouteSegments;
136
 
137
  /// Check if current route matches pattern
138
  bool matchesRoute(String pattern) {
139
    return state.matchesRoute(pattern);
140
  }
141
 
142
  /// Get breadcrumb trail
143
  List<String> get breadcrumbTrail => state.breadcrumbTrail;
144
 
145
  /// Get current route name
146
  String get currentRouteName => state.currentRouteName;
147
 
148
  /// Check if navigation state is valid
149
  bool get isStateValid => state.isValid;
150
 
151
  /// Navigate to route with validation
152
  Future<bool> tryNavigateTo(
153
    String newRoute, {
154
    Map<String, dynamic>? params,
155
  }) async {
156
    try {
157
      await navigateTo(newRoute, params: params);
158
      return true;
159
    } catch (e) {
160
      // Handle navigation errors
161
      return false;
162
    }
163
  }
164
 
165
  /// Navigate to specific overlay by type
166
  Future<void> navigateToOverlay(String overlayType, {Map<String, dynamic>? params}) async {
167
    final route = '/$overlayType';
168
    await navigateTo(route, params: params);
169
  }
170
 
171
  /// Check if currently at specific overlay
172
  bool isAtSpecificOverlay(String overlayType) {
173
    return state.currentOverlayType == overlayType;
174
  }
175
 
176
  /// Go back with safety check
177
  Future<bool> safeGoBack() async {
178
    if (state.canGoBack) {
179
      await goBack();
180
      return true;
181
    }
182
    return false;
183
  }
184
 
185
  /// Navigate with route validation
186
  Future<bool> navigateWithValidation(String route) async {
187
    // Basic route validation
188
    if (route.isEmpty || !route.startsWith('/')) {
189
      return false;
190
    }
191
 
192
    await navigateTo(route);
193
    return true;
194
  }
195
 
196
  /// Push new route onto stack
197
  Future<void> pushRoute(String route, {Map<String, dynamic>? params}) async {
198
    await navigateTo(route, params: params);
199
  }
200
 
201
  /// Pop current route from stack
202
  Future<void> popRoute() async {
203
    await goBack();
204
  }
205
 
206
  /// Replace entire route stack
207
  Future<void> replaceStack(List<String> routes) async {
208
    if (routes.isEmpty) {
209
      await resetToRoot();
210
      return;
211
    }
212
 
213
    // Clear current history and build new stack
214
    state = state.clearHistory();
215
 
216
    // Navigate through each route except the last
217
    for (int i = 0; i < routes.length - 1; i++) {
218
      state = state.navigateTo(routes[i]);
219
    }
220
 
221
    // Navigate to final route
222
    if (routes.isNotEmpty) {
223
      await navigateTo(routes.last);
224
    }
225
  }
226
 
227
  /// Get navigation statistics
228
  Map<String, dynamic> getNavigationStats() {
229
    return {
230
      'current_route': state.currentRoute,
231
      'previous_route': state.previousRoute,
232
      'route_depth': state.routeDepth,
233
      'history_length': state.routeHistory.length,
234
      'is_at_root': state.isAtRoot,
235
      'is_at_overlay': state.isAtOverlay,
236
      'current_overlay_type': state.currentOverlayType,
237
      'can_go_back': state.canGoBack,
238
      'has_params': state.hasParams,
239
      'param_count': state.routeParams.length,
240
      'last_navigation_at': state.lastNavigationAt?.toIso8601String(),
241
      'time_since_last_navigation_ms': state.timeSinceLastNavigation?.inMilliseconds,
242
      'was_recent_navigation': state.wasRecentNavigation,
243
      'is_valid_state': state.isValid,
244
      'route_name': state.currentRouteName,
245
      'breadcrumb_count': state.breadcrumbTrail.length,
246
    };
247
  }
248
 
249
  /// Handle deep link navigation
250
  Future<void> handleDeepLink(String deepLink) async {
251
    // Parse deep link and extract route and parameters
252
    final uri = Uri.tryParse(deepLink);
253
    if (uri != null) {
254
      final route = uri.path.isEmpty ? '/' : uri.path;
255
      final params = uri.queryParameters;
256
 
257
      await navigateTo(route, params: params);
258
    }
259
  }
260
 
261
  /// Generate deep link for current state
262
  String generateDeepLink() {
263
    final uri = Uri(
264
      path: state.currentRoute,
265
      queryParameters: state.routeParams.map(
266
        (key, value) => MapEntry(key, value.toString()),
267
      ),
268
    );
269
    return uri.toString();
270
  }
271
 
272
  /// Check if navigation to route is allowed
273
  bool isNavigationAllowed(String route) {
274
    // Define navigation rules
275
    const restrictedRoutes = <String>[];
276
    const allowedRoutes = ['/', '/patients', '/montages', '/settings', '/activities'];
277
 
278
    if (restrictedRoutes.contains(route)) {
279
      return false;
280
    }
281
 
282
    return allowedRoutes.contains(route) || allowedRoutes.any(route.startsWith);
283
  }
284
 
285
  /// Navigate with permission check
286
  Future<bool> navigateWithPermission(String route) async {
287
    if (isNavigationAllowed(route)) {
288
      await navigateTo(route);
289
      return true;
290
    }
291
    return false;
292
  }
293
 
294
  /// Get route hierarchy
295
  List<String> getRouteHierarchy() {
296
    return state.breadcrumbTrail;
297
  }
298
 
299
  /// Check if route is in history
300
  bool isRouteInHistory(String route) {
301
    return state.routeHistory.contains(route);
302
  }
303
 
304
  /// Get last occurrence of route in history
305
  int? getLastRouteIndex(String route) {
306
    final lastIndex = state.routeHistory.lastIndexOf(route);
307
    return lastIndex >= 0 ? lastIndex : null;
308
  }
309
 
310
  /// Navigate to route by history index
311
  Future<void> navigateToHistoryIndex(int index) async {
312
    if (index >= 0 && index < state.routeHistory.length) {
313
      final targetRoute = state.routeHistory[index];
314
 
315
      // Trim history to target index + 1
316
      final newHistory = state.routeHistory.sublist(0, index + 1);
317
 
318
      state = state.copyWith(
319
        currentRoute: targetRoute,
320
        routeHistory: newHistory,
321
        previousRoute: index > 0 ? newHistory[index - 1] : null,
322
        routeParams: {}, // Clear params when jumping to history
323
        lastNavigationAt: DateTime.now().toUtc(),
324
      );
325
    }
326
  }
327
 
328
  /// Clear parameter by key if exists
329
  Future<void> clearParamIfExists(String key) async {
330
    if (state.hasParam(key)) {
331
      await removeParam(key);
332
    }
333
  }
334
 
335
  /// Update multiple parameters at once
336
  Future<void> updateMultipleParams(Map<String, dynamic> newParams) async {
337
    final updatedParams = Map<String, dynamic>.from(state.routeParams)
338
      ..addAll(newParams);
339
    await updateParams(updatedParams);
340
  }
341
 
342
  /// Clear all parameters
343
  Future<void> clearAllParams() async {
344
    await updateParams({});
345
  }
346
 
347
  // Overlay navigation methods
348
 
349
  /// Select an overlay by index (0-3) or null to close
350
  void selectItem(int? index) {
351
    final overlayNotifier = ref.read(overlayProvider.notifier);
352
 
353
    if (state.selectedIndex == index) {
354
      // Same panel already open — do nothing. Only the central toggle
355
      // button should dismiss panels, not re-clicking an active nav icon.
356
      return;
357
    } else {
358
      // Switch to new overlay or open if none selected
359
      if (index != null) {
360
        final overlayType = _getOverlayTypeForIndex(index);
361
        if (overlayType != null) {
362
          overlayNotifier.show(overlayType);
363
        }
364
      }
365
      state = state.copyWith(selectedIndex: index);
366
    }
367
  }
368
 
369
  /// Clear the current selection (close overlay) and switch to signal mode
370
  void clearSelection() {
371
    // Save current overlay index before clearing (for restoration later)
372
    final currentIndex = state.selectedIndex;
373
    if (currentIndex != null) {
374
      ref.read(bottomBarProvider.notifier).saveOverlayIndex(currentIndex);
375
    }
376
 
377
    // Dismiss overlay and clear selection
378
    ref.read(overlayProvider.notifier).dismiss();
379
    state = state.copyWith(selectedIndex: null);
380
 
381
    // Switch bottom bar to signal mode
382
    ref.read(bottomBarProvider.notifier).setSignalMode();
383
  }
384
 
385
  /// Helper method to map index to OverlayType
386
  OverlayType? _getOverlayTypeForIndex(int index) {
387
    switch (index) {
388
      case 0:
389
        return OverlayType.patients;
390
      case 1:
391
        return OverlayType.montages;
392
      case 2:
393
        return OverlayType.settings;
394
      case 3:
395
        return OverlayType.activities;
396
      default:
397
        return null;
398
    }
399
  }
400
 
401
  /// Check if any overlay is open
402
  bool get isOverlayOpen => state.selectedIndex != null;
403
 
404
  /// Get the current selected overlay index
405
  int? get selectedIndex => state.selectedIndex;
406
}