checkpoint
This commit is contained in:
+157
-18
@@ -1,6 +1,9 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/delivery.dart';
|
||||
import '../providers/providers.dart';
|
||||
import '../api/client.dart';
|
||||
@@ -9,12 +12,14 @@ import '../models/delivery_commands.dart';
|
||||
import '../components/map_sidebar_layout.dart';
|
||||
import '../components/dark_mode_map.dart';
|
||||
import '../components/delivery_list_item.dart';
|
||||
import '../utils/toast_helper.dart';
|
||||
|
||||
class DeliveriesPage extends ConsumerStatefulWidget {
|
||||
final int routeFragmentId;
|
||||
final String routeName;
|
||||
final VoidCallback? onBack;
|
||||
final bool showAsEmbedded;
|
||||
final Delivery? selectedDelivery;
|
||||
final ValueChanged<Delivery?>? onDeliverySelected;
|
||||
|
||||
const DeliveriesPage({
|
||||
@@ -23,6 +28,7 @@ class DeliveriesPage extends ConsumerStatefulWidget {
|
||||
required this.routeName,
|
||||
this.onBack,
|
||||
this.showAsEmbedded = false,
|
||||
this.selectedDelivery,
|
||||
this.onDeliverySelected,
|
||||
});
|
||||
|
||||
@@ -39,6 +45,17 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_listScrollController = ScrollController();
|
||||
_selectedDelivery = widget.selectedDelivery;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(DeliveriesPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedDelivery != oldWidget.selectedDelivery) {
|
||||
setState(() {
|
||||
_selectedDelivery = widget.selectedDelivery;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -84,16 +101,16 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
});
|
||||
}
|
||||
|
||||
// Responsive sidebar that changes width when collapsed (420px expanded, 80px collapsed)
|
||||
// Responsive sidebar that changes width when collapsed (300px expanded, 80px collapsed)
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: isExpanded ? 420 : 80,
|
||||
width: isExpanded ? 300 : 80,
|
||||
child: Column(
|
||||
children: [
|
||||
// Header with back button
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isExpanded ? 12 : 8,
|
||||
horizontal: isExpanded ? 12 : 0,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
@@ -274,9 +291,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
String? token,
|
||||
) async {
|
||||
if (token == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Authentication required')),
|
||||
);
|
||||
ToastHelper.showError(context, 'Authentication required');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -299,14 +314,10 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
success: (_) {
|
||||
// ignore: unused_result
|
||||
ref.refresh(deliveriesProvider(widget.routeFragmentId));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Delivery marked as completed')),
|
||||
);
|
||||
ToastHelper.showSuccess(context, 'Delivery marked as completed');
|
||||
},
|
||||
onError: (error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: ${error.message}')),
|
||||
);
|
||||
ToastHelper.showError(context, 'Error: ${error.message}');
|
||||
},
|
||||
);
|
||||
break;
|
||||
@@ -320,18 +331,18 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
success: (_) {
|
||||
// ignore: unused_result
|
||||
ref.refresh(deliveriesProvider(widget.routeFragmentId));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Delivery marked as uncompleted')),
|
||||
);
|
||||
ToastHelper.showSuccess(context, 'Delivery marked as uncompleted');
|
||||
},
|
||||
onError: (error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: ${error.message}')),
|
||||
);
|
||||
ToastHelper.showError(context, 'Error: ${error.message}');
|
||||
},
|
||||
);
|
||||
break;
|
||||
|
||||
case 'photo':
|
||||
await _handlePhotoCapture(context, delivery, token);
|
||||
break;
|
||||
|
||||
case 'call':
|
||||
final contact = delivery.orders.isNotEmpty && delivery.orders.first.contact != null
|
||||
? delivery.orders.first.contact
|
||||
@@ -350,6 +361,134 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePhotoCapture(
|
||||
BuildContext context,
|
||||
Delivery delivery,
|
||||
String? token,
|
||||
) async {
|
||||
if (token == null) {
|
||||
ToastHelper.showError(context, 'Authentication required');
|
||||
return;
|
||||
}
|
||||
|
||||
final ImagePicker picker = ImagePicker();
|
||||
XFile? pickedFile;
|
||||
|
||||
try {
|
||||
pickedFile = await picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 85,
|
||||
);
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ToastHelper.showError(context, 'Camera error: $e');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pickedFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
final bool? confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('Confirm Photo'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.file(
|
||||
File(pickedFile!.path),
|
||||
height: 300,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Upload this photo for ${delivery.name}?',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Upload'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirmed != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return const Center(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Uploading photo...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
final Uri uploadUrl = Uri.parse(
|
||||
'${ApiClientConfig.production.baseUrl}/api/delivery/uploadDeliveryPicture?deliveryId=${delivery.id}',
|
||||
);
|
||||
|
||||
final http.MultipartRequest request = http.MultipartRequest('POST', uploadUrl);
|
||||
request.headers['Authorization'] = 'Bearer $token';
|
||||
request.files.add(await http.MultipartFile.fromPath('file', pickedFile.path));
|
||||
|
||||
final http.StreamedResponse streamedResponse = await request.send();
|
||||
final http.Response response = await http.Response.fromStream(streamedResponse);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
if (context.mounted) {
|
||||
ToastHelper.showSuccess(context, 'Photo uploaded successfully');
|
||||
}
|
||||
// ignore: unused_result
|
||||
ref.refresh(deliveriesProvider(widget.routeFragmentId));
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
ToastHelper.showError(context, 'Upload failed: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ToastHelper.showError(context, 'Upload error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UnifiedDeliveryListView extends StatelessWidget {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/providers.dart';
|
||||
import '../utils/toast_helper.dart';
|
||||
|
||||
class LoginPage extends ConsumerStatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
@@ -50,12 +51,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
ref.refresh(isAuthenticatedProvider);
|
||||
},
|
||||
onError: (error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
ToastHelper.showError(context, error);
|
||||
},
|
||||
cancelled: () {},
|
||||
);
|
||||
|
||||
+494
-43
@@ -1,9 +1,17 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/delivery.dart';
|
||||
import '../models/delivery_route.dart';
|
||||
import '../models/delivery_commands.dart';
|
||||
import '../providers/providers.dart';
|
||||
import '../api/client.dart';
|
||||
import '../utils/toast_helper.dart';
|
||||
import '../api/openapi_config.dart';
|
||||
import '../utils/breakpoints.dart';
|
||||
import '../utils/http_client_factory.dart';
|
||||
import '../components/collapsible_routes_sidebar.dart';
|
||||
import '../components/dark_mode_map.dart';
|
||||
import '../services/location_permission_service.dart';
|
||||
@@ -67,6 +75,433 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleDeliveryAction(
|
||||
String action,
|
||||
Delivery delivery,
|
||||
int routeFragmentId,
|
||||
) async {
|
||||
final authService = ref.read(authServiceProvider);
|
||||
|
||||
// Ensure we have a valid token (automatically refreshes if needed)
|
||||
final token = await authService.ensureValidToken();
|
||||
if (token == null) {
|
||||
if (mounted) {
|
||||
ToastHelper.showError(context, 'Authentication required');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create API client with auth service for automatic token refresh
|
||||
final authClient = CqrsApiClient(
|
||||
config: ApiClientConfig.development,
|
||||
authService: authService,
|
||||
);
|
||||
|
||||
switch (action) {
|
||||
case 'complete':
|
||||
if (mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return const Center(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Completing delivery...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final result = await authClient.executeCommand(
|
||||
endpoint: 'completeDelivery',
|
||||
command: CompleteDeliveryCommand(
|
||||
deliveryId: delivery.id,
|
||||
),
|
||||
);
|
||||
result.when(
|
||||
success: (_) async {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
// Invalidate both providers to force refresh
|
||||
ref.invalidate(deliveriesProvider(routeFragmentId));
|
||||
ref.invalidate(allDeliveriesProvider);
|
||||
|
||||
// Wait for providers to refresh
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
if (mounted) {
|
||||
// Get refreshed deliveries
|
||||
final allDeliveries = await ref.read(allDeliveriesProvider.future);
|
||||
final routeDeliveries = allDeliveries
|
||||
.where((d) => d.routeFragmentId == routeFragmentId)
|
||||
.toList();
|
||||
|
||||
// Find the next incomplete delivery in the route
|
||||
final nextDelivery = routeDeliveries.firstWhere(
|
||||
(d) => !d.delivered && !d.isSkipped,
|
||||
orElse: () => routeDeliveries.firstWhere(
|
||||
(d) => d.id == delivery.id,
|
||||
orElse: () => delivery,
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_selectedDelivery = nextDelivery;
|
||||
});
|
||||
|
||||
// Auto-show notes for the next delivery if needed
|
||||
_autoShowNotesIfNeeded(nextDelivery);
|
||||
|
||||
// Small delay to let the UI update before map auto-navigates
|
||||
if (nextDelivery.id != delivery.id && mounted) {
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ToastHelper.showSuccess(context, 'Delivery marked as completed');
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
debugPrint('Complete delivery failed - Type: ${error.type}, Message: ${error.message}');
|
||||
debugPrint('Error details: ${error.details}');
|
||||
if (mounted) {
|
||||
String errorMessage = 'Error: ${error.message}';
|
||||
if (error.statusCode == 500) {
|
||||
errorMessage = 'Server error - Please contact support';
|
||||
}
|
||||
ToastHelper.showError(context, errorMessage);
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
|
||||
case 'uncomplete':
|
||||
if (mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return const Center(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Marking as uncompleted...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final result = await authClient.executeCommand(
|
||||
endpoint: 'markDeliveryAsUncompleted',
|
||||
command: MarkDeliveryAsUncompletedCommand(deliveryId: delivery.id),
|
||||
);
|
||||
result.when(
|
||||
success: (_) async {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
// Invalidate both providers to force refresh
|
||||
ref.invalidate(deliveriesProvider(routeFragmentId));
|
||||
ref.invalidate(allDeliveriesProvider);
|
||||
|
||||
// Wait for providers to refresh
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
if (mounted) {
|
||||
// Get refreshed deliveries
|
||||
final allDeliveries = await ref.read(allDeliveriesProvider.future);
|
||||
final updatedDelivery = allDeliveries.firstWhere(
|
||||
(d) => d.id == delivery.id,
|
||||
orElse: () => delivery,
|
||||
);
|
||||
setState(() {
|
||||
_selectedDelivery = updatedDelivery;
|
||||
});
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ToastHelper.showSuccess(context, 'Delivery marked as uncompleted');
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ToastHelper.showError(context, 'Error: ${error.message}');
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
|
||||
case 'photo':
|
||||
await _handlePhotoCapture(delivery);
|
||||
break;
|
||||
|
||||
case 'note':
|
||||
await _showNotesDialog(delivery);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePhotoCapture(
|
||||
Delivery delivery,
|
||||
) async {
|
||||
final authService = ref.read(authServiceProvider);
|
||||
|
||||
// Ensure we have a valid token (automatically refreshes if needed)
|
||||
final token = await authService.ensureValidToken();
|
||||
if (token == null) {
|
||||
if (mounted) {
|
||||
ToastHelper.showError(context, 'Authentication required');
|
||||
}
|
||||
return;
|
||||
}
|
||||
final ImagePicker picker = ImagePicker();
|
||||
XFile? pickedFile;
|
||||
|
||||
try {
|
||||
pickedFile = await picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 85,
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ToastHelper.showError(context, 'Camera error: $e');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pickedFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final bool? confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('Confirm Photo'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(dialogContext).size.height * 0.5,
|
||||
maxWidth: MediaQuery.of(dialogContext).size.width * 0.8,
|
||||
),
|
||||
child: Image.file(
|
||||
File(pickedFile!.path),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Upload this photo for ${delivery.name}?',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Upload'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirmed != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return const Center(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Uploading photo...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
final Uri uploadUrl = Uri.parse(
|
||||
'${ApiClientConfig.development.baseUrl}/api/delivery/uploadDeliveryPicture?deliveryId=${delivery.id}',
|
||||
);
|
||||
|
||||
// Create HTTP client that accepts self-signed certificates
|
||||
final client = HttpClientFactory.createClient(
|
||||
allowSelfSigned: ApiClientConfig.development.allowSelfSignedCertificate,
|
||||
);
|
||||
|
||||
final http.MultipartRequest request = http.MultipartRequest('POST', uploadUrl);
|
||||
request.headers['Authorization'] = 'Bearer $token';
|
||||
request.files.add(await http.MultipartFile.fromPath('file', pickedFile.path));
|
||||
|
||||
final http.StreamedResponse streamedResponse = await client.send(request);
|
||||
final http.Response response = await http.Response.fromStream(streamedResponse);
|
||||
client.close();
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
if (mounted) {
|
||||
ToastHelper.showSuccess(context, 'Photo uploaded successfully');
|
||||
}
|
||||
ref.refresh(allDeliveriesProvider);
|
||||
} else {
|
||||
debugPrint('Photo upload failed - Status: ${response.statusCode}');
|
||||
debugPrint('Response body: ${response.body}');
|
||||
if (mounted) {
|
||||
String errorMessage = 'Upload failed';
|
||||
if (response.statusCode == 500) {
|
||||
errorMessage = 'Server error - Please contact support';
|
||||
} else if (response.statusCode == 401) {
|
||||
errorMessage = 'Authentication required - Please log in again';
|
||||
} else {
|
||||
errorMessage = 'Upload failed: ${response.statusCode}';
|
||||
}
|
||||
ToastHelper.showError(context, errorMessage);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ToastHelper.showError(context, 'Upload error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldAutoShowNotes(Delivery? delivery) {
|
||||
// Only auto-show notes if delivery is not yet delivered and has notes
|
||||
if (delivery == null || delivery.delivered) return false;
|
||||
|
||||
final hasNotes = delivery.orders.any(
|
||||
(order) => order.note != null && order.note!.isNotEmpty,
|
||||
);
|
||||
|
||||
return hasNotes;
|
||||
}
|
||||
|
||||
Future<void> _autoShowNotesIfNeeded(Delivery? delivery) async {
|
||||
if (delivery != null && _shouldAutoShowNotes(delivery)) {
|
||||
// Use post-frame callback to ensure UI is ready
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_showNotesDialog(delivery);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showNotesDialog(Delivery delivery) async {
|
||||
final notes = delivery.orders
|
||||
.where((order) => order.note != null && order.note!.isNotEmpty)
|
||||
.map((order) => order.note!)
|
||||
.toList();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (notes.isEmpty) {
|
||||
ToastHelper.showInfo(context, 'No notes attached to this delivery');
|
||||
return;
|
||||
}
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text('Notes for ${delivery.name}'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: notes.map((note) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
note,
|
||||
style: Theme.of(dialogContext).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
)).toList(),
|
||||
),
|
||||
),
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
actions: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final routesData = ref.watch(deliveryRoutesProvider);
|
||||
@@ -79,46 +514,57 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
ref.refresh(deliveryRoutesProvider);
|
||||
ref.refresh(allDeliveriesProvider);
|
||||
},
|
||||
icon: (routesData.isLoading || allDeliveriesData.isLoading)
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh),
|
||||
onPressed: (routesData.isLoading || allDeliveriesData.isLoading)
|
||||
? null
|
||||
: () {
|
||||
ref.refresh(deliveryRoutesProvider);
|
||||
ref.refresh(allDeliveriesProvider);
|
||||
},
|
||||
tooltip: 'Refresh',
|
||||
),
|
||||
userProfile.when(
|
||||
data: (profile) => PopupMenuButton<String>(
|
||||
onSelected: (value) {
|
||||
if (value == 'settings') {
|
||||
data: (profile) {
|
||||
String getInitials(String? fullName) {
|
||||
if (fullName == null || fullName.isEmpty) return 'U';
|
||||
final names = fullName.trim().split(' ');
|
||||
if (names.length == 1) {
|
||||
return names[0][0].toUpperCase();
|
||||
}
|
||||
return '${names.first[0]}${names.last[0]}'.toUpperCase();
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const SettingsPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(
|
||||
value: 'profile',
|
||||
child: Text(profile?.fullName ?? 'User'),
|
||||
enabled: false,
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem(
|
||||
value: 'settings',
|
||||
child: Text('Settings'),
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
profile?.fullName ?? 'User',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: Text(
|
||||
getInitials(profile?.fullName),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: SizedBox(
|
||||
@@ -157,6 +603,12 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
setState(() {
|
||||
_selectedDelivery = delivery;
|
||||
});
|
||||
_autoShowNotesIfNeeded(delivery);
|
||||
},
|
||||
onAction: (action) {
|
||||
if (_selectedDelivery != null && _selectedRoute != null) {
|
||||
_handleDeliveryAction(action, _selectedDelivery!, _selectedRoute!.id);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -166,19 +618,18 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
||||
selectedRoute: null,
|
||||
onRouteSelected: _selectRoute,
|
||||
)
|
||||
: SizedBox(
|
||||
width: 300,
|
||||
child: DeliveriesPage(
|
||||
routeFragmentId: _selectedRoute!.id,
|
||||
routeName: _selectedRoute!.name,
|
||||
onBack: _backToRoutes,
|
||||
showAsEmbedded: true,
|
||||
onDeliverySelected: (delivery) {
|
||||
setState(() {
|
||||
_selectedDelivery = delivery;
|
||||
});
|
||||
},
|
||||
),
|
||||
: DeliveriesPage(
|
||||
routeFragmentId: _selectedRoute!.id,
|
||||
routeName: _selectedRoute!.name,
|
||||
onBack: _backToRoutes,
|
||||
showAsEmbedded: true,
|
||||
selectedDelivery: _selectedDelivery,
|
||||
onDeliverySelected: (delivery) {
|
||||
setState(() {
|
||||
_selectedDelivery = delivery;
|
||||
});
|
||||
_autoShowNotesIfNeeded(delivery);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -64,6 +64,22 @@ class SettingsPage extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton.filled(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () async {
|
||||
final authService = ref.read(authServiceProvider);
|
||||
await authService.logout();
|
||||
if (context.mounted) {
|
||||
// ignore: unused_result
|
||||
ref.refresh(isAuthenticatedProvider);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/');
|
||||
}
|
||||
}
|
||||
},
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
tooltip: 'Logout',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -90,7 +106,13 @@ class SettingsPage extends ConsumerWidget {
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
title: const Text('Language'),
|
||||
subtitle: Text(language == 'fr' ? 'Franais' : 'English'),
|
||||
subtitle: Text(
|
||||
language == 'system'
|
||||
? 'System'
|
||||
: language == 'fr'
|
||||
? 'Français'
|
||||
: 'English'
|
||||
),
|
||||
trailing: DropdownButton<String>(
|
||||
value: language,
|
||||
onChanged: (String? newValue) {
|
||||
@@ -99,13 +121,17 @@ class SettingsPage extends ConsumerWidget {
|
||||
}
|
||||
},
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: 'system',
|
||||
child: Text('System'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'en',
|
||||
child: Text('English'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'fr',
|
||||
child: Text('Franais'),
|
||||
child: Text('Français'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -148,42 +174,6 @@ class SettingsPage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Account',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.logout),
|
||||
label: const Text('Logout'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
onPressed: () async {
|
||||
final authService = ref.read(authServiceProvider);
|
||||
await authService.logout();
|
||||
if (context.mounted) {
|
||||
// ignore: unused_result
|
||||
ref.refresh(isAuthenticatedProvider);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/');
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
|
||||
Reference in New Issue
Block a user