checkpoint

This commit is contained in:
2025-11-26 15:59:20 -05:00
parent d46ac9dc14
commit ef5c0c1a95
14 changed files with 5281 additions and 260 deletions
+157 -18
View File
@@ -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 {
+2 -6
View File
@@ -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
View File
@@ -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);
},
),
],
),
+28 -38
View File
@@ -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(