Fix Google Navigation initialization timing issues

Restructures navigation session initialization to occur after the view is
created, eliminating race conditions. Session initialization now happens in
onViewCreated callback with proper delay before setting destination.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jean-Philippe Brule 2025-11-15 20:49:20 -05:00
parent 46af8f55a2
commit 9cb5b51f6d
11 changed files with 558 additions and 192 deletions

View File

@ -3,6 +3,9 @@
android:label="planb_logistic" android:label="planb_logistic"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyCuYzbusLkVrHcy10bJ8STF6gyOexQWjuk" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

View File

@ -114,7 +114,6 @@
13867C66F1703482B503520B /* Pods-RunnerTests.release.xcconfig */, 13867C66F1703482B503520B /* Pods-RunnerTests.release.xcconfig */,
F864EA92C8601181D927DDF4 /* Pods-RunnerTests.profile.xcconfig */, F864EA92C8601181D927DDF4 /* Pods-RunnerTests.profile.xcconfig */,
); );
name = Pods;
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -489,13 +488,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 833P6TSX55;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic; PRODUCT_BUNDLE_IDENTIFIER = com.local.planbLogistic;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -508,6 +508,7 @@
baseConfigurationReference = 77C9A4AE9C5588D9B699F74C /* Pods-RunnerTests.debug.xcconfig */; baseConfigurationReference = 77C9A4AE9C5588D9B699F74C /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -671,13 +672,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 833P6TSX55;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic; PRODUCT_BUNDLE_IDENTIFIER = com.local.planbLogistic;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -693,13 +695,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 833P6TSX55;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic; PRODUCT_BUNDLE_IDENTIFIER = com.local.planbLogistic;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View File

@ -8,12 +8,14 @@ class DarkModeMapComponent extends StatefulWidget {
final List<Delivery> deliveries; final List<Delivery> deliveries;
final Delivery? selectedDelivery; final Delivery? selectedDelivery;
final ValueChanged<Delivery?>? onDeliverySelected; final ValueChanged<Delivery?>? onDeliverySelected;
final Function(String)? onAction;
const DarkModeMapComponent({ const DarkModeMapComponent({
super.key, super.key,
required this.deliveries, required this.deliveries,
this.selectedDelivery, this.selectedDelivery,
this.onDeliverySelected, this.onDeliverySelected,
this.onAction,
}); });
@override @override
@ -256,6 +258,47 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
} }
} }
Future<void> _zoomIn() async {
if (_navigationController == null) return;
try {
final currentCamera = await _navigationController!.getCameraPosition();
await _navigationController!.animateCamera(
CameraUpdate.newLatLngZoom(
currentCamera.target,
currentCamera.zoom + 1,
),
);
} catch (e) {
debugPrint('Zoom in error: $e');
}
}
Future<void> _zoomOut() async {
if (_navigationController == null) return;
try {
final currentCamera = await _navigationController!.getCameraPosition();
await _navigationController!.animateCamera(
CameraUpdate.newLatLngZoom(
currentCamera.target,
currentCamera.zoom - 1,
),
);
} catch (e) {
debugPrint('Zoom out error: $e');
}
}
Future<void> _recenterMap() async {
if (_navigationController == null || _destinationLocation == null) return;
try {
await _navigationController!.animateCamera(
CameraUpdate.newLatLngZoom(_destinationLocation!, 15),
);
} catch (e) {
debugPrint('Recenter map error: $e');
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final initialPosition = widget.selectedDelivery?.deliveryAddress != null && final initialPosition = widget.selectedDelivery?.deliveryAddress != null &&
@ -283,6 +326,27 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
), ),
), ),
// Custom dark-themed controls overlay // Custom dark-themed controls overlay
Positioned(
top: 16,
right: 16,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Zoom in button
_buildIconButton(
icon: Icons.add,
onPressed: _zoomIn,
),
const SizedBox(height: 8),
// Zoom out button
_buildIconButton(
icon: Icons.remove,
onPressed: _zoomOut,
),
],
),
),
// Navigation and action buttons
Positioned( Positioned(
bottom: 16, bottom: 16,
right: 16, right: 16,
@ -388,10 +452,175 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
), ),
), ),
), ),
// Bottom action button bar
if (widget.selectedDelivery != null)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
children: [
// Recenter button
Expanded(
child: _buildBottomActionButton(
label: 'Recenter',
icon: Icons.location_on,
onPressed: _recenterMap,
),
),
const SizedBox(width: 12),
// Mark Complete button (if not already delivered)
if (!widget.selectedDelivery!.delivered)
Expanded(
child: _buildBottomActionButton(
label: 'Mark Complete',
icon: Icons.check_circle,
onPressed: () => widget.onAction?.call('complete'),
isPrimary: true,
),
),
if (widget.selectedDelivery!.delivered)
Expanded(
child: _buildBottomActionButton(
label: 'Start Navigation',
icon: Icons.directions,
onPressed: _startNavigation,
isPrimary: true,
),
),
if (!_isNavigating && !widget.selectedDelivery!.delivered)
const SizedBox(width: 12),
if (!_isNavigating && !widget.selectedDelivery!.delivered)
Expanded(
child: _buildBottomActionButton(
label: 'Navigate',
icon: Icons.directions,
onPressed: _startNavigation,
),
),
if (_isNavigating)
const SizedBox(width: 12),
if (_isNavigating)
Expanded(
child: _buildBottomActionButton(
label: 'Stop',
icon: Icons.stop,
onPressed: _stopNavigation,
isDanger: true,
),
),
],
),
),
),
], ],
); );
} }
Widget _buildIconButton({
required IconData icon,
required VoidCallback onPressed,
}) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Theme.of(context).colorScheme.surface,
shape: const CircleBorder(),
child: InkWell(
onTap: onPressed,
customBorder: const CircleBorder(),
child: Padding(
padding: const EdgeInsets.all(12),
child: Icon(
icon,
color: Theme.of(context).colorScheme.onSurface,
size: 24,
),
),
),
),
);
}
Widget _buildBottomActionButton({
required String label,
required IconData icon,
required VoidCallback onPressed,
bool isPrimary = false,
bool isDanger = false,
}) {
Color backgroundColor;
Color textColor = Colors.white;
if (isDanger) {
backgroundColor = Colors.red.shade600;
} else if (isPrimary) {
backgroundColor = SvrntyColors.crimsonRed;
} else {
backgroundColor = Theme.of(context).colorScheme.surfaceContainerHighest;
textColor = Theme.of(context).colorScheme.onSurface;
}
return Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(8),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: textColor,
size: 18,
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
],
),
),
),
);
}
Widget _buildActionButton({ Widget _buildActionButton({
required String label, required String label,
required IconData icon, required IconData icon,

View File

@ -8,6 +8,7 @@ class DeliveryListItem extends StatefulWidget {
final bool isSelected; final bool isSelected;
final VoidCallback onTap; final VoidCallback onTap;
final VoidCallback? onCall; final VoidCallback? onCall;
final Function(String)? onAction;
final int? animationIndex; final int? animationIndex;
const DeliveryListItem({ const DeliveryListItem({
@ -16,6 +17,7 @@ class DeliveryListItem extends StatefulWidget {
required this.isSelected, required this.isSelected,
required this.onTap, required this.onTap,
this.onCall, this.onCall,
this.onAction,
this.animationIndex, this.animationIndex,
}); });
@ -112,10 +114,12 @@ class _DeliveryListItemState extends State<DeliveryListItem>
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
color: _isHovered || widget.isSelected color: widget.delivery.delivered
? Theme.of(context).colorScheme.surfaceContainer ? Colors.green.withOpacity(0.15)
: Colors.transparent, : (_isHovered || widget.isSelected
boxShadow: _isHovered || widget.isSelected ? Theme.of(context).colorScheme.surfaceContainer
: Colors.transparent),
boxShadow: (_isHovered || widget.isSelected) && !widget.delivery.delivered
? [ ? [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity( color: Colors.black.withOpacity(

View File

@ -331,6 +331,114 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'{completed}/{total} completed'** /// **'{completed}/{total} completed'**
String completedDeliveries(int completed, int total); String completedDeliveries(int completed, int total);
/// No description provided for @navigationTcTitle.
///
/// In en, this message translates to:
/// **'Navigation Service'**
String get navigationTcTitle;
/// No description provided for @navigationTcDescription.
///
/// In en, this message translates to:
/// **'This app uses Google Navigation to provide turn-by-turn navigation for deliveries.'**
String get navigationTcDescription;
/// No description provided for @navigationTcAttribution.
///
/// In en, this message translates to:
/// **'Attribution: Maps and navigation services provided by Google Maps.'**
String get navigationTcAttribution;
/// No description provided for @navigationTcTerms.
///
/// In en, this message translates to:
/// **'By accepting, you agree to Google\'s Terms of Service and Privacy Policy for Navigation services.'**
String get navigationTcTerms;
/// No description provided for @accept.
///
/// In en, this message translates to:
/// **'Accept'**
String get accept;
/// No description provided for @decline.
///
/// In en, this message translates to:
/// **'Decline'**
String get decline;
/// No description provided for @locationPermissionRequired.
///
/// In en, this message translates to:
/// **'Location Permission'**
String get locationPermissionRequired;
/// No description provided for @locationPermissionMessage.
///
/// In en, this message translates to:
/// **'This app requires location permission to navigate to deliveries.'**
String get locationPermissionMessage;
/// No description provided for @locationPermissionDenied.
///
/// In en, this message translates to:
/// **'Location permission denied. Navigation cannot proceed.'**
String get locationPermissionDenied;
/// No description provided for @permissionPermanentlyDenied.
///
/// In en, this message translates to:
/// **'Permission Required'**
String get permissionPermanentlyDenied;
/// No description provided for @openSettingsMessage.
///
/// In en, this message translates to:
/// **'Location permission is permanently denied. Please enable it in app settings.'**
String get openSettingsMessage;
/// No description provided for @openSettings.
///
/// In en, this message translates to:
/// **'Open Settings'**
String get openSettings;
/// No description provided for @cancel.
///
/// In en, this message translates to:
/// **'Cancel'**
String get cancel;
/// No description provided for @ok.
///
/// In en, this message translates to:
/// **'OK'**
String get ok;
/// No description provided for @requestPermission.
///
/// In en, this message translates to:
/// **'Request Permission'**
String get requestPermission;
/// No description provided for @navigationArrived.
///
/// In en, this message translates to:
/// **'You have arrived at the destination'**
String get navigationArrived;
/// No description provided for @navigatingTo.
///
/// In en, this message translates to:
/// **'Navigating to'**
String get navigatingTo;
/// No description provided for @initializingNavigation.
///
/// In en, this message translates to:
/// **'Initializing navigation...'**
String get initializingNavigation;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View File

@ -134,4 +134,64 @@ class AppLocalizationsEn extends AppLocalizations {
String completedDeliveries(int completed, int total) { String completedDeliveries(int completed, int total) {
return '$completed/$total completed'; return '$completed/$total completed';
} }
@override
String get navigationTcTitle => 'Navigation Service';
@override
String get navigationTcDescription =>
'This app uses Google Navigation to provide turn-by-turn navigation for deliveries.';
@override
String get navigationTcAttribution =>
'Attribution: Maps and navigation services provided by Google Maps.';
@override
String get navigationTcTerms =>
'By accepting, you agree to Google\'s Terms of Service and Privacy Policy for Navigation services.';
@override
String get accept => 'Accept';
@override
String get decline => 'Decline';
@override
String get locationPermissionRequired => 'Location Permission';
@override
String get locationPermissionMessage =>
'This app requires location permission to navigate to deliveries.';
@override
String get locationPermissionDenied =>
'Location permission denied. Navigation cannot proceed.';
@override
String get permissionPermanentlyDenied => 'Permission Required';
@override
String get openSettingsMessage =>
'Location permission is permanently denied. Please enable it in app settings.';
@override
String get openSettings => 'Open Settings';
@override
String get cancel => 'Cancel';
@override
String get ok => 'OK';
@override
String get requestPermission => 'Request Permission';
@override
String get navigationArrived => 'You have arrived at the destination';
@override
String get navigatingTo => 'Navigating to';
@override
String get initializingNavigation => 'Initializing navigation...';
} }

View File

@ -134,4 +134,64 @@ class AppLocalizationsFr extends AppLocalizations {
String completedDeliveries(int completed, int total) { String completedDeliveries(int completed, int total) {
return '$completed/$total livrs'; return '$completed/$total livrs';
} }
@override
String get navigationTcTitle => 'Service de Navigation';
@override
String get navigationTcDescription =>
'Cette application utilise Google Navigation pour fournir une navigation virage par virage pour les livraisons.';
@override
String get navigationTcAttribution =>
'Attribution: Services de cartes et de navigation fournis par Google Maps.';
@override
String get navigationTcTerms =>
'En acceptant, vous acceptez les conditions d\'utilisation et la politique de confidentialit de Google pour les services de navigation.';
@override
String get accept => 'Accepter';
@override
String get decline => 'Refuser';
@override
String get locationPermissionRequired => 'Permission de localisation';
@override
String get locationPermissionMessage =>
'Cette application ncessite la permission de localisation pour naviguer vers les livraisons.';
@override
String get locationPermissionDenied =>
'Permission de localisation refuse. La navigation ne peut pas continuer.';
@override
String get permissionPermanentlyDenied => 'Permission requise';
@override
String get openSettingsMessage =>
'La permission de localisation est dfinitivement refuse. Veuillez l\'activer dans les paramtres de l\'application.';
@override
String get openSettings => 'Ouvrir les paramtres';
@override
String get cancel => 'Annuler';
@override
String get ok => 'OK';
@override
String get requestPermission => 'Demander la permission';
@override
String get navigationArrived => 'Vous tes arriv la destination';
@override
String get navigatingTo => 'Navigation vers';
@override
String get initializingNavigation => 'Initialisation de la navigation...';
} }

View File

@ -11,8 +11,6 @@ import '../utils/breakpoints.dart';
import '../components/map_sidebar_layout.dart'; import '../components/map_sidebar_layout.dart';
import '../components/dark_mode_map.dart'; import '../components/dark_mode_map.dart';
import '../components/delivery_list_item.dart'; import '../components/delivery_list_item.dart';
import '../components/collapsible_routes_sidebar.dart'
show CollapsibleRoutesSidebar;
class DeliveriesPage extends ConsumerStatefulWidget { class DeliveriesPage extends ConsumerStatefulWidget {
final int routeFragmentId; final int routeFragmentId;
@ -29,22 +27,38 @@ class DeliveriesPage extends ConsumerStatefulWidget {
} }
class _DeliveriesPageState extends ConsumerState<DeliveriesPage> { class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
late PageController _pageController; late ScrollController _listScrollController;
int _currentSegment = 0;
Delivery? _selectedDelivery; Delivery? _selectedDelivery;
int? _lastRouteFragmentId;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_pageController = PageController(); _listScrollController = ScrollController();
} }
@override @override
void dispose() { void dispose() {
_pageController.dispose(); _listScrollController.dispose();
super.dispose(); super.dispose();
} }
Future<void> _autoScrollToFirstPending(List<Delivery> deliveries) async {
final firstPendingIndex = deliveries.indexWhere((d) => !d.delivered && !d.isSkipped);
if (_listScrollController.hasClients && firstPendingIndex != -1) {
await Future.delayed(const Duration(milliseconds: 200));
// Scroll to position first pending delivery at top of list
// Each item is approximately 70 pixels tall
final scrollOffset = firstPendingIndex * 70.0;
_listScrollController.animateTo(
scrollOffset.clamp(0, _listScrollController.position.maxScrollExtent),
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId)); final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
@ -58,6 +72,14 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
), ),
body: deliveriesData.when( body: deliveriesData.when(
data: (deliveries) { data: (deliveries) {
// Auto-scroll to first pending delivery when page loads or route changes
if (_lastRouteFragmentId != widget.routeFragmentId) {
_lastRouteFragmentId = widget.routeFragmentId;
WidgetsBinding.instance.addPostFrameCallback((_) {
_autoScrollToFirstPending(deliveries);
});
}
final todoDeliveries = deliveries final todoDeliveries = deliveries
.where((d) => !d.delivered && !d.isSkipped) .where((d) => !d.delivered && !d.isSkipped)
.toList(); .toList();
@ -76,27 +98,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
currentRoute = routes.isNotEmpty ? routes.first : null; currentRoute = routes.isNotEmpty ? routes.first : null;
} }
return Row( return MapSidebarLayout(
children: [
if (context.isDesktop && routes.isNotEmpty)
CollapsibleRoutesSidebar(
routes: routes,
selectedRoute: currentRoute,
onRouteSelected: (route) {
if (route.id != widget.routeFragmentId) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => DeliveriesPage(
routeFragmentId: route.id,
routeName: route.name,
),
),
);
}
},
),
Expanded(
child: MapSidebarLayout(
mapWidget: DarkModeMapComponent( mapWidget: DarkModeMapComponent(
deliveries: deliveries, deliveries: deliveries,
selectedDelivery: _selectedDelivery, selectedDelivery: _selectedDelivery,
@ -105,75 +107,25 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
_selectedDelivery = delivery; _selectedDelivery = delivery;
}); });
}, },
onAction: (action) => _selectedDelivery != null
? _handleDeliveryAction(context, _selectedDelivery!, action, token)
: null,
), ),
sidebarWidget: Column( sidebarWidget: UnifiedDeliveryListView(
children: [ deliveries: deliveries,
Padding( selectedDelivery: _selectedDelivery,
padding: const EdgeInsets.all(16.0), scrollController: _listScrollController,
child: SegmentedButton<int>( onDeliverySelected: (delivery) {
segments: const [ setState(() {
ButtonSegment( _selectedDelivery = delivery;
value: 0, });
label: Text('To Do'), },
), onItemAction: (delivery, action) {
ButtonSegment( _handleDeliveryAction(context, delivery, action, token);
value: 1, _autoScrollToFirstPending(deliveries);
label: Text('Delivered'), },
),
],
selected: <int>{_currentSegment},
onSelectionChanged: (Set<int> newSelection) {
setState(() {
_currentSegment = newSelection.first;
_pageController.animateToPage(
_currentSegment,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
});
},
),
),
Expanded(
child: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentSegment = index;
});
},
children: [
DeliveryListView(
deliveries: todoDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
DeliveryListView(
deliveries: completedDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
],
),
),
],
), ),
), );
),
],
);
}, },
loading: () => const Center( loading: () => const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
@ -187,70 +139,23 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
_selectedDelivery = delivery; _selectedDelivery = delivery;
}); });
}, },
onAction: (action) => _selectedDelivery != null
? _handleDeliveryAction(context, _selectedDelivery!, action, token)
: null,
), ),
sidebarWidget: Column( sidebarWidget: UnifiedDeliveryListView(
children: [ deliveries: deliveries,
Padding( selectedDelivery: _selectedDelivery,
padding: const EdgeInsets.all(16.0), scrollController: _listScrollController,
child: SegmentedButton<int>( onDeliverySelected: (delivery) {
segments: const [ setState(() {
ButtonSegment( _selectedDelivery = delivery;
value: 0, });
label: Text('To Do'), },
), onItemAction: (delivery, action) {
ButtonSegment( _handleDeliveryAction(context, delivery, action, token);
value: 1, _autoScrollToFirstPending(deliveries);
label: Text('Delivered'), },
),
],
selected: <int>{_currentSegment},
onSelectionChanged: (Set<int> newSelection) {
setState(() {
_currentSegment = newSelection.first;
_pageController.animateToPage(
_currentSegment,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
});
},
),
),
Expanded(
child: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentSegment = index;
});
},
children: [
DeliveryListView(
deliveries: todoDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
DeliveryListView(
deliveries: completedDeliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (delivery, action) =>
_handleDeliveryAction(context, delivery, action, token),
),
],
),
),
],
), ),
), ),
); );
@ -291,7 +196,6 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
endpoint: 'completeDelivery', endpoint: 'completeDelivery',
command: CompleteDeliveryCommand( command: CompleteDeliveryCommand(
deliveryId: delivery.id, deliveryId: delivery.id,
deliveredAt: DateTime.now().toIso8601String(),
), ),
); );
result.when( result.when(
@ -351,18 +255,20 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
} }
} }
class DeliveryListView extends StatelessWidget { class UnifiedDeliveryListView extends StatelessWidget {
final List<Delivery> deliveries; final List<Delivery> deliveries;
final Delivery? selectedDelivery; final Delivery? selectedDelivery;
final ScrollController scrollController;
final ValueChanged<Delivery> onDeliverySelected; final ValueChanged<Delivery> onDeliverySelected;
final Function(Delivery, String) onAction; final Function(Delivery, String) onItemAction;
const DeliveryListView({ const UnifiedDeliveryListView({
super.key, super.key,
required this.deliveries, required this.deliveries,
this.selectedDelivery, this.selectedDelivery,
required this.scrollController,
required this.onDeliverySelected, required this.onDeliverySelected,
required this.onAction, required this.onItemAction,
}); });
@override @override
@ -378,6 +284,7 @@ class DeliveryListView extends StatelessWidget {
// Trigger refresh via provider // Trigger refresh via provider
}, },
child: ListView.builder( child: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: deliveries.length, itemCount: deliveries.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
@ -386,7 +293,8 @@ class DeliveryListView extends StatelessWidget {
delivery: delivery, delivery: delivery,
isSelected: selectedDelivery?.id == delivery.id, isSelected: selectedDelivery?.id == delivery.id,
onTap: () => onDeliverySelected(delivery), onTap: () => onDeliverySelected(delivery),
onCall: () => onAction(delivery, 'call'), onCall: () => onItemAction(delivery, 'call'),
onAction: (action) => onItemAction(delivery, action),
animationIndex: index, animationIndex: index,
); );
}, },

View File

@ -52,11 +52,8 @@ class _NavigationPageState extends ConsumerState<NavigationPage> {
setState(() { setState(() {
_hasLocationPermission = true; _hasLocationPermission = true;
_isNavigationInitialized = true;
}); });
if (mounted) {
_initializeNavigationSession();
}
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
_showErrorDialog('Initialization error: ${e.toString()}'); _showErrorDialog('Initialization error: ${e.toString()}');
@ -67,19 +64,10 @@ class _NavigationPageState extends ConsumerState<NavigationPage> {
Future<void> _initializeNavigationSession() async { Future<void> _initializeNavigationSession() async {
try { try {
await GoogleMapsNavigationViewController.initializeNavigationSession(); await GoogleMapsNavigationViewController.initializeNavigationSession();
if (mounted) {
setState(() {
_isNavigationInitialized = true;
});
// Set destination after session is initialized
await _setDestination();
}
} catch (e) { } catch (e) {
if (mounted) { debugPrint('Navigation session initialization error: $e');
_showErrorDialog('Failed to initialize navigation: ${e.toString()}'); // Don't show error dialog, just log it
} // The session might already be initialized
} }
} }
@ -263,8 +251,11 @@ class _NavigationPageState extends ConsumerState<NavigationPage> {
), ),
body: _hasLocationPermission && _isNavigationInitialized body: _hasLocationPermission && _isNavigationInitialized
? GoogleMapsNavigationView( ? GoogleMapsNavigationView(
onViewCreated: (controller) { onViewCreated: (controller) async {
_navigationViewController = controller; _navigationViewController = controller;
await _initializeNavigationSession();
await Future.delayed(const Duration(milliseconds: 500));
await _setDestination();
}, },
initialCameraPosition: CameraPosition( initialCameraPosition: CameraPosition(
target: LatLng( target: LatLng(

View File

@ -588,7 +588,7 @@
338D0CEB231458BD00FA5F75 /* Profile */ = { 338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
}; };
name = Profile; name = Profile;
@ -708,7 +708,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = LD76P8L42W; DEVELOPMENT_TEAM = 833P6TSX55;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -744,7 +744,7 @@
33CC111C2044C6BA0003C045 /* Debug */ = { 33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
}; };
name = Debug; name = Debug;

View File

@ -8,7 +8,7 @@
PRODUCT_NAME = planb_logistic PRODUCT_NAME = planb_logistic
// The application's bundle identifier // The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.goutezplanb.planbLogistic PRODUCT_BUNDLE_IDENTIFIER = com.local.planbLogistic
// The copyright displayed in application information // The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2025 com.goutezplanb. All rights reserved. PRODUCT_COPYRIGHT = Copyright © 2025 com.goutezplanb. All rights reserved.