438 lines
14 KiB
Dart
438 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../../../../core/theme/app_theme.dart';
|
|
import '../../../../core/widgets/status_badge.dart';
|
|
import '../../../../services/location/location_service.dart';
|
|
import '../../../../services/maps/navigation_service.dart';
|
|
import '../../data/models/route_model.dart';
|
|
import '../../data/models/stop_model.dart';
|
|
import '../providers/route_provider.dart';
|
|
import '../widgets/stop_card.dart';
|
|
import 'map_view_page.dart';
|
|
|
|
class RouteDetailsPage extends StatelessWidget {
|
|
const RouteDetailsPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Consumer<RouteProvider>(
|
|
builder: (context, routeProvider, child) {
|
|
final route = routeProvider.currentRoute;
|
|
|
|
if (route == null) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Route Details'),
|
|
),
|
|
body: const Center(
|
|
child: Text('No route selected'),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text('Route ${route.id}'),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.map),
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => MapViewPage(route: route),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: Column(
|
|
children: [
|
|
_buildRouteHeader(context, route),
|
|
const Divider(height: 1),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
itemCount: route.stops.length,
|
|
itemBuilder: (context, index) {
|
|
final stop = route.stops[index];
|
|
return StopCard(
|
|
stop: stop,
|
|
onTap: () {
|
|
_showStopDetails(context, route, stop);
|
|
},
|
|
onNavigate: () {
|
|
_navigateToStop(context, stop);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
floatingActionButton: _buildActionButton(context, route),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildRouteHeader(BuildContext context, RouteModel route) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
color: Colors.white,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
DateFormat('EEEE, MMM dd').format(route.date),
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
StatusBadge(
|
|
status: route.status.toString().split('.').last,
|
|
fontSize: 14,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
_buildInfoItem(
|
|
context,
|
|
Icons.route,
|
|
'${route.totalDistance.toStringAsFixed(1)} km',
|
|
'Distance',
|
|
),
|
|
_buildInfoItem(
|
|
context,
|
|
Icons.access_time,
|
|
'${route.estimatedDuration} min',
|
|
'Duration',
|
|
),
|
|
_buildInfoItem(
|
|
context,
|
|
Icons.location_on,
|
|
'${route.stops.length}',
|
|
'Stops',
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: LinearProgressIndicator(
|
|
value: route.progressPercentage / 100,
|
|
minHeight: 12,
|
|
backgroundColor: Colors.grey[300],
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
route.status == RouteStatus.completed
|
|
? AppTheme.completedColor
|
|
: AppTheme.inProgressColor,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'${route.completedStopsCount}/${route.totalStopsCount} stops completed',
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoItem(
|
|
BuildContext context,
|
|
IconData icon,
|
|
String value,
|
|
String label,
|
|
) {
|
|
return Column(
|
|
children: [
|
|
Icon(icon, color: AppTheme.primaryColor, size: 24),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
value,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
label,
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget? _buildActionButton(BuildContext context, RouteModel route) {
|
|
if (route.status == RouteStatus.completed ||
|
|
route.status == RouteStatus.cancelled) {
|
|
return null;
|
|
}
|
|
|
|
final routeProvider = context.read<RouteProvider>();
|
|
|
|
if (route.status == RouteStatus.notStarted) {
|
|
return FloatingActionButton.extended(
|
|
onPressed: () async {
|
|
final success = await routeProvider.startRoute(route.id);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
success ? 'Route started!' : 'Failed to start route',
|
|
),
|
|
backgroundColor: success ? AppTheme.successColor : AppTheme.errorColor,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
icon: const Icon(Icons.play_arrow),
|
|
label: const Text('Start Route'),
|
|
);
|
|
}
|
|
|
|
if (route.status == RouteStatus.inProgress) {
|
|
final allCompleted =
|
|
route.stops.every((stop) => stop.status == StopStatus.completed);
|
|
|
|
if (allCompleted) {
|
|
return FloatingActionButton.extended(
|
|
onPressed: () async {
|
|
final success = await routeProvider.completeRoute(route.id);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
success ? 'Route completed!' : 'Failed to complete route',
|
|
),
|
|
backgroundColor:
|
|
success ? AppTheme.successColor : AppTheme.errorColor,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
icon: const Icon(Icons.check),
|
|
label: const Text('Complete Route'),
|
|
);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
void _showStopDetails(
|
|
BuildContext context, RouteModel route, StopModel stop) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
builder: (context) => DraggableScrollableSheet(
|
|
initialChildSize: 0.7,
|
|
minChildSize: 0.5,
|
|
maxChildSize: 0.9,
|
|
expand: false,
|
|
builder: (context, scrollController) => Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: ListView(
|
|
controller: scrollController,
|
|
children: [
|
|
Center(
|
|
child: Container(
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[300],
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Stop Details',
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
StatusBadge(
|
|
status: stop.status.toString().split('.').last,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
_buildDetailRow(
|
|
context,
|
|
'Customer',
|
|
stop.customerName,
|
|
Icons.person,
|
|
),
|
|
_buildDetailRow(
|
|
context,
|
|
'Type',
|
|
stop.type == StopType.pickup ? 'Pickup' : 'Dropoff',
|
|
Icons.local_shipping,
|
|
),
|
|
if (stop.customerPhone != null)
|
|
_buildDetailRow(
|
|
context,
|
|
'Phone',
|
|
stop.customerPhone!,
|
|
Icons.phone,
|
|
),
|
|
_buildDetailRow(
|
|
context,
|
|
'Scheduled',
|
|
DateFormat('hh:mm a').format(stop.scheduledTime),
|
|
Icons.schedule,
|
|
),
|
|
if (stop.location.address != null)
|
|
_buildDetailRow(
|
|
context,
|
|
'Address',
|
|
stop.location.address!,
|
|
Icons.location_on,
|
|
),
|
|
if (stop.items.isNotEmpty) ...[
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Items',
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 8),
|
|
...stop.items.map((item) => Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.check_circle_outline, size: 20),
|
|
const SizedBox(width: 8),
|
|
Text(item),
|
|
],
|
|
),
|
|
)),
|
|
],
|
|
const SizedBox(height: 24),
|
|
if (stop.status == StopStatus.pending ||
|
|
stop.status == StopStatus.inProgress) ...[
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
_navigateToStop(context, stop);
|
|
},
|
|
icon: const Icon(Icons.navigation),
|
|
label: const Text('Navigate'),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: () async {
|
|
Navigator.pop(context);
|
|
final success = await context
|
|
.read<RouteProvider>()
|
|
.updateStopStatus(
|
|
route.id,
|
|
stop.id,
|
|
StopStatus.completed,
|
|
);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
success
|
|
? 'Stop completed!'
|
|
: 'Failed to complete stop',
|
|
),
|
|
backgroundColor: success
|
|
? AppTheme.successColor
|
|
: AppTheme.errorColor,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
icon: const Icon(Icons.check),
|
|
label: const Text('Complete'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailRow(
|
|
BuildContext context,
|
|
String label,
|
|
String value,
|
|
IconData icon,
|
|
) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(icon, size: 20, color: Colors.grey),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
value,
|
|
style: Theme.of(context).textTheme.bodyLarge,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _navigateToStop(BuildContext context, StopModel stop) async {
|
|
final locationService = LocationService();
|
|
final navigationService = NavigationService();
|
|
|
|
// Get current location
|
|
final currentPosition = await locationService.getCurrentLocation();
|
|
|
|
final success = await navigationService.openNavigation(
|
|
destinationLat: stop.location.latitude,
|
|
destinationLng: stop.location.longitude,
|
|
originLat: currentPosition?.latitude,
|
|
originLng: currentPosition?.longitude,
|
|
destinationName: stop.customerName,
|
|
);
|
|
|
|
if (!success && context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Failed to open navigation'),
|
|
backgroundColor: AppTheme.errorColor,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|