ionic-planb-logistic-app-fl.../lib/pages/deliveries_page.dart
Mathias Beaulieu-Duncan 6986a12b91 auto-claude: subtask-6-1 - Run flutter analyze to ensure no errors or warnings
Fixed all 39 analyzer issues:
- Removed unused import (animation_system.dart in collapsible_routes_sidebar.dart)
- Removed unused element (_buildActionButton in dark_mode_map.dart)
- Fixed unnecessary non-null assertions on AppLocalizations.of(context)
- Removed unnecessary type checks in providers.dart
- Used super parameters for key in navigation_tc_dialog.dart and status_colors.dart
- Replaced print statements with debugPrint in providers.dart and logging_interceptor.dart

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-20 11:54:12 -05:00

464 lines
16 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../l10n/app_localizations.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';
import '../api/openapi_config.dart';
import '../models/delivery_commands.dart';
import '../components/map_sidebar_layout.dart';
import '../components/dark_mode_map.dart';
import '../components/unified_delivery_list.dart';
import '../components/loading_dialog.dart';
import '../components/photo_capture_dialog.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({
super.key,
required this.routeFragmentId,
required this.routeName,
this.onBack,
this.showAsEmbedded = false,
this.selectedDelivery,
this.onDeliverySelected,
});
@override
ConsumerState<DeliveriesPage> createState() => _DeliveriesPageState();
}
class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
late ScrollController _listScrollController;
Delivery? _selectedDelivery;
int? _lastRouteFragmentId;
@override
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
void dispose() {
_listScrollController.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
Widget build(BuildContext context) {
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
final tokenAsync = ref.watch(authTokenProvider);
final token = tokenAsync.hasValue ? tokenAsync.value : null;
final l10n = AppLocalizations.of(context);
// When embedded in sidebar, show only the delivery list with back button
// This is a responsive sidebar that collapses like routes
if (widget.showAsEmbedded) {
final isExpanded = ref.watch(collapseStateProvider);
return deliveriesData.when(
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);
});
}
// Responsive sidebar that changes width when collapsed (300px expanded, 80px collapsed)
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: isExpanded ? 300 : 80,
child: Column(
children: [
// Header with back button
Container(
padding: EdgeInsets.symmetric(
horizontal: isExpanded ? 12 : 0,
vertical: 8,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: Row(
mainAxisAlignment: isExpanded
? MainAxisAlignment.start
: MainAxisAlignment.center,
children: [
if (isExpanded) ...[
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: widget.onBack,
tooltip: 'Back to routes',
),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.routeName,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
IconButton(
icon: Icon(isExpanded ? Icons.menu_open : Icons.menu),
onPressed: () {
ref.read(collapseStateProvider.notifier).toggle();
},
tooltip: isExpanded ? 'Collapse' : 'Expand',
),
],
),
),
// Delivery list
Expanded(
child: UnifiedDeliveryListView(
deliveries: deliveries,
selectedDelivery: _selectedDelivery,
scrollController: _listScrollController,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
// Notify parent about delivery selection
widget.onDeliverySelected?.call(delivery);
},
onItemAction: (delivery, action) {
_handleDeliveryAction(context, delivery, action, token);
_autoScrollToFirstPending(deliveries);
},
isCollapsed: !isExpanded,
),
),
],
),
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Center(
child: Text(l10n.error(error.toString())),
),
);
}
// When not embedded, show full page with map
final routesData = ref.watch(deliveryRoutesProvider);
return Scaffold(
appBar: AppBar(
title: Text(widget.routeName),
elevation: 0,
),
body: SafeArea(
child: deliveriesData.when(
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);
});
}
return routesData.when(
data: (routes) {
return MapSidebarLayout(
mapWidget: DarkModeMapComponent(
deliveries: deliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (action) => _selectedDelivery != null
? _handleDeliveryAction(context, _selectedDelivery!, action, token)
: null,
),
sidebarBuilder: (isCollapsed) => UnifiedDeliveryListView(
deliveries: deliveries,
selectedDelivery: _selectedDelivery,
scrollController: _listScrollController,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onItemAction: (delivery, action) {
_handleDeliveryAction(context, delivery, action, token);
_autoScrollToFirstPending(deliveries);
},
isCollapsed: isCollapsed,
),
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => MapSidebarLayout(
mapWidget: DarkModeMapComponent(
deliveries: deliveries,
selectedDelivery: _selectedDelivery,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onAction: (action) => _selectedDelivery != null
? _handleDeliveryAction(context, _selectedDelivery!, action, token)
: null,
),
sidebarBuilder: (isCollapsed) => UnifiedDeliveryListView(
deliveries: deliveries,
selectedDelivery: _selectedDelivery,
scrollController: _listScrollController,
onDeliverySelected: (delivery) {
setState(() {
_selectedDelivery = delivery;
});
},
onItemAction: (delivery, action) {
_handleDeliveryAction(context, delivery, action, token);
_autoScrollToFirstPending(deliveries);
},
isCollapsed: isCollapsed,
),
),
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Center(
child: Text(l10n.error(error.toString())),
),
),
),
);
}
Future<void> _handleDeliveryAction(
BuildContext context,
Delivery delivery,
String action,
String? token,
) async {
// Prevent any actions on warehouse delivery except map navigation
if (delivery.isWarehouseDelivery && action != 'map') {
return;
}
if (token == null) {
final l10n = AppLocalizations.of(context);
ToastHelper.showError(context, l10n.authenticationRequired);
return;
}
final authClient = CqrsApiClient(
config: ApiClientConfig(
baseUrl: ApiClientConfig.production.baseUrl,
defaultHeaders: {'Authorization': 'Bearer $token'},
),
);
switch (action) {
case 'complete':
final result = await authClient.executeCommand(
endpoint: 'completeDelivery',
command: CompleteDeliveryCommand(
deliveryId: delivery.id,
),
);
result.when(
success: (_) {
final l10n = AppLocalizations.of(context);
// ignore: unused_result
ref.refresh(deliveriesProvider(widget.routeFragmentId));
// ignore: unused_result
ref.refresh(deliveryRoutesProvider);
ToastHelper.showSuccess(context, l10n.deliverySuccessful);
},
onError: (error) {
final l10n = AppLocalizations.of(context);
ToastHelper.showError(context, l10n.error(error.message));
},
);
break;
case 'uncomplete':
final result = await authClient.executeCommand(
endpoint: 'markDeliveryAsUncompleted',
command: MarkDeliveryAsUncompletedCommand(deliveryId: delivery.id),
);
result.when(
success: (_) {
// ignore: unused_result
ref.refresh(deliveriesProvider(widget.routeFragmentId));
// ignore: unused_result
ref.refresh(deliveryRoutesProvider);
ToastHelper.showSuccess(context, 'Delivery marked as uncompleted');
},
onError: (error) {
final l10n = AppLocalizations.of(context);
ToastHelper.showError(context, l10n.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
: null;
if (contact?.phoneNumber != null) {
final Uri phoneUri = Uri(scheme: 'tel', path: contact!.phoneNumber);
if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri);
}
}
break;
case 'map':
// Navigation is now handled in-app by the DeliveryMap component
// Just ensure the delivery is selected
break;
}
}
Future<void> _handlePhotoCapture(
BuildContext context,
Delivery delivery,
String? token,
) async {
if (token == null) {
final l10n = AppLocalizations.of(context);
ToastHelper.showError(context, l10n.authenticationRequired);
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 PhotoCaptureDialog.show(
context,
imageFile: File(pickedFile.path),
deliveryName: delivery.name,
);
if (confirmed != true) {
return;
}
if (!context.mounted) return;
final localizations = AppLocalizations.of(context);
LoadingDialog.show(context, message: localizations.uploadingPhoto);
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) {
LoadingDialog.hide(context);
}
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) {
LoadingDialog.hide(context);
ToastHelper.showError(context, 'Upload error: $e');
}
}
}
}