ionic-planb-logistic-app-fl.../lib/providers/providers.dart
Mathias Beaulieu-Duncan 4a9377e0a9 auto-claude: subtask-4-3 - Create gRPC-based deliveries provider
Add grpcDeliveriesProvider as a gRPC-based alternative to deliveriesProvider.
The new provider uses GrpcCqrsApiClient.getDeliveries() for improved
performance and type safety. Follows the existing pattern with:
- FutureProvider.family pattern for route-specific queries
- Authentication check before API calls
- Result.when() for proper error handling
- Warehouse delivery appended at the end

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-20 13:09:56 -05:00

304 lines
9.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../api/types.dart';
import '../api/client.dart';
import '../api/grpc_client.dart';
import '../api/grpc_config.dart';
import '../api/openapi_config.dart';
import '../services/auth_service.dart';
import '../models/user_profile.dart';
import '../models/delivery_route.dart';
import '../models/delivery.dart';
final authServiceProvider = Provider<AuthService>((ref) {
return AuthService(config: AuthConfig.development);
});
final apiClientProvider = Provider<CqrsApiClient>((ref) {
final authService = ref.watch(authServiceProvider);
return CqrsApiClient(
config: ApiClientConfig.development,
authService: authService,
);
});
/// Provider for the gRPC-based CQRS API client.
///
/// Uses auto-dispose to properly shut down the gRPC channel when the provider
/// is no longer being watched. This ensures network resources are cleaned up.
///
/// Example usage:
/// ```dart
/// final grpcClient = ref.watch(grpcClientProvider);
/// final result = await grpcClient.getDeliveryRoutes();
/// ```
final grpcClientProvider = Provider.autoDispose<GrpcCqrsApiClient>((ref) {
final authService = ref.watch(authServiceProvider);
final client = GrpcCqrsApiClient(
config: GrpcConfig.development,
authService: authService,
);
// Register disposal callback to clean up gRPC channel resources
ref.onDispose(() {
client.shutdown();
});
return client;
});
final isAuthenticatedProvider = FutureProvider<bool>((ref) async {
final authService = ref.watch(authServiceProvider);
return await authService.isAuthenticated();
});
final userProfileProvider = FutureProvider<UserProfile?>((ref) async {
final authService = ref.watch(authServiceProvider);
final token = await authService.getToken();
if (token == null) return null;
return authService.decodeToken(token);
});
final authTokenProvider = FutureProvider<String?>((ref) async {
final authService = ref.watch(authServiceProvider);
return await authService.getToken();
});
final deliveryRoutesProvider = FutureProvider<List<DeliveryRoute>>((ref) async {
final authService = ref.watch(authServiceProvider);
final isAuthenticated = await authService.isAuthenticated();
if (!isAuthenticated) {
throw Exception('User not authenticated');
}
// Create a new client with auth service for automatic token refresh
final authClient = CqrsApiClient(
config: ApiClientConfig.development,
authService: authService,
);
final result = await authClient.executeQuery<List<DeliveryRoute>>(
endpoint: 'simpleDeliveryRouteQueryItems',
query: _EmptyQuery(),
fromJson: (json) {
// API returns data wrapped in object with "data" field
final data = json['data'];
if (data is List<dynamic>) {
return data.map((r) => DeliveryRoute.fromJson(r as Map<String, dynamic>)).toList();
}
return [];
},
);
return result.whenSuccess((routes) => routes) ?? [];
});
/// Provider for delivery routes using gRPC.
///
/// This is the gRPC-based alternative to [deliveryRoutesProvider].
/// Uses [GrpcCqrsApiClient] for improved performance and type safety.
///
/// Example usage:
/// ```dart
/// final routes = ref.watch(grpcDeliveryRoutesProvider);
/// routes.when(
/// data: (data) => displayRoutes(data),
/// loading: () => showLoading(),
/// error: (error, stack) => showError(error),
/// );
/// ```
final grpcDeliveryRoutesProvider = FutureProvider<List<DeliveryRoute>>((ref) async {
final authService = ref.watch(authServiceProvider);
final isAuthenticated = await authService.isAuthenticated();
if (!isAuthenticated) {
throw Exception('User not authenticated');
}
final grpcClient = ref.watch(grpcClientProvider);
final result = await grpcClient.getDeliveryRoutes();
return result.when(
success: (routes) => routes,
onError: (error) {
debugPrint('ERROR fetching delivery routes via gRPC: ${error.message}');
if (error.originalException != null) {
debugPrint('Original exception: ${error.originalException}');
}
throw Exception(error.message);
},
);
});
/// Provider for deliveries using gRPC.
///
/// This is the gRPC-based alternative to [deliveriesProvider].
/// Uses [GrpcCqrsApiClient] for improved performance and type safety.
/// Takes a [routeFragmentId] parameter to fetch deliveries for a specific route.
///
/// Example usage:
/// ```dart
/// final deliveries = ref.watch(grpcDeliveriesProvider(routeFragmentId));
/// deliveries.when(
/// data: (data) => displayDeliveries(data),
/// loading: () => showLoading(),
/// error: (error, stack) => showError(error),
/// );
/// ```
final grpcDeliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, routeFragmentId) async {
final authService = ref.watch(authServiceProvider);
final isAuthenticated = await authService.isAuthenticated();
if (!isAuthenticated) {
throw Exception('User not authenticated');
}
final grpcClient = ref.watch(grpcClientProvider);
final result = await grpcClient.getDeliveries(routeFragmentId: routeFragmentId);
final deliveries = result.when(
success: (deliveries) => deliveries,
onError: (error) {
debugPrint('ERROR fetching deliveries for route $routeFragmentId via gRPC: ${error.message}');
if (error.originalException != null) {
debugPrint('Original exception: ${error.originalException}');
}
throw Exception(error.message);
},
);
// Always append the warehouse delivery at the end
return [...deliveries, Delivery.createWarehouseDelivery()];
});
final deliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, routeFragmentId) async {
final authService = ref.watch(authServiceProvider);
final isAuthenticated = await authService.isAuthenticated();
if (!isAuthenticated) {
throw Exception('User not authenticated');
}
final authClient = CqrsApiClient(
config: ApiClientConfig.development,
authService: authService,
);
final result = await authClient.executeQuery<List<Delivery>>(
endpoint: 'simpleDeliveriesQueryItems',
query: _DeliveriesQuery(routeFragmentId: routeFragmentId),
fromJson: (json) {
// API returns data wrapped in object with "data" field
final data = json['data'];
if (data is List<dynamic>) {
return data.map((d) => Delivery.fromJson(d as Map<String, dynamic>)).toList();
}
return [];
},
);
// Log error if API call failed
result.whenError((error) {
debugPrint('ERROR fetching deliveries for route $routeFragmentId: ${error.message}');
if (error.originalException != null) {
debugPrint('Original exception: ${error.originalException}');
}
});
final deliveries = result.whenSuccess((deliveries) => deliveries) ?? [];
// Always append the warehouse delivery at the end
return [...deliveries, Delivery.createWarehouseDelivery()];
});
/// Provider to get all deliveries from all routes
final allDeliveriesProvider = FutureProvider<List<Delivery>>((ref) async {
final routes = await ref.read(deliveryRoutesProvider.future);
if (routes.isEmpty) {
return [];
}
// Fetch deliveries for all routes in parallel using Future.wait
final deliveriesFutures = routes.map((route) {
return ref.read(deliveriesProvider(route.id).future);
}).toList();
// Wait for all futures to complete
final deliveriesLists = await Future.wait(deliveriesFutures);
// Combine all deliveries into a single list
final allDeliveries = <Delivery>[];
for (final deliveries in deliveriesLists) {
allDeliveries.addAll(deliveries);
}
return allDeliveries;
});
// Language notifier for state management with SharedPreferences persistence
class LanguageNotifier extends AsyncNotifier<String> {
static const String _languageKey = 'app_language';
@override
Future<String> build() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_languageKey) ?? 'system';
}
Future<void> setLanguage(String lang) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_languageKey, lang);
state = AsyncValue.data(lang);
}
}
final languageProvider = AsyncNotifierProvider<LanguageNotifier, String>(() {
return LanguageNotifier();
});
// Theme mode notifier for manual theme switching
class ThemeModeNotifier extends Notifier<ThemeMode> {
@override
ThemeMode build() => ThemeMode.dark;
void setThemeMode(ThemeMode mode) => state = mode;
}
final themeModeProvider = NotifierProvider<ThemeModeNotifier, ThemeMode>(() {
return ThemeModeNotifier();
});
// Collapse state notifier for sidebar persistence
class CollapseStateNotifier extends Notifier<bool> {
@override
bool build() => true; // Default: expanded
void toggle() => state = !state;
void setExpanded(bool expanded) => state = expanded;
}
final collapseStateProvider = NotifierProvider<CollapseStateNotifier, bool>(() {
return CollapseStateNotifier();
});
class _EmptyQuery implements Serializable {
@override
Map<String, Object?> toJson() => {};
}
class _DeliveriesQuery implements Serializable {
final int routeFragmentId;
_DeliveriesQuery({required this.routeFragmentId});
@override
Map<String, Object?> toJson() => {
'params': {
'routeFragmentId': routeFragmentId,
},
};
}