auto-claude/001-normalize-code-update-packages-widgetify-component #1

Merged
mathias merged 12 commits from auto-claude/001-normalize-code-update-packages-widgetify-component into main 2026-01-20 12:25:49 -05:00
29 changed files with 1243 additions and 629 deletions

211
.auto-claude-security.json Normal file
View File

@ -0,0 +1,211 @@
{
"base_commands": [
".",
"[",
"[[",
"ag",
"awk",
"basename",
"bash",
"bc",
"break",
"cat",
"cd",
"chmod",
"clear",
"cmp",
"column",
"comm",
"command",
"continue",
"cp",
"curl",
"cut",
"date",
"df",
"diff",
"dig",
"dirname",
"du",
"echo",
"egrep",
"env",
"eval",
"exec",
"exit",
"expand",
"export",
"expr",
"false",
"fd",
"fgrep",
"file",
"find",
"fmt",
"fold",
"gawk",
"gh",
"git",
"grep",
"gunzip",
"gzip",
"head",
"help",
"host",
"iconv",
"id",
"jobs",
"join",
"jq",
"kill",
"killall",
"less",
"let",
"ln",
"ls",
"lsof",
"man",
"mkdir",
"mktemp",
"more",
"mv",
"nl",
"paste",
"pgrep",
"ping",
"pkill",
"popd",
"printenv",
"printf",
"ps",
"pushd",
"pwd",
"read",
"readlink",
"realpath",
"reset",
"return",
"rev",
"rg",
"rm",
"rmdir",
"sed",
"seq",
"set",
"sh",
"shuf",
"sleep",
"sort",
"source",
"split",
"stat",
"tail",
"tar",
"tee",
"test",
"time",
"timeout",
"touch",
"tr",
"tree",
"true",
"type",
"uname",
"unexpand",
"uniq",
"unset",
"unzip",
"watch",
"wc",
"wget",
"whereis",
"which",
"whoami",
"xargs",
"yes",
"yq",
"zip",
"zsh"
],
"stack_commands": [
"ant",
"ar",
"clang",
"clang++",
"cmake",
"dart",
"dart2js",
"dartanalyzer",
"dartdoc",
"dartfmt",
"flutter",
"fvm",
"g++",
"gcc",
"gradle",
"gradlew",
"ipython",
"jar",
"java",
"javac",
"jupyter",
"kotlin",
"kotlinc",
"ld",
"make",
"maven",
"meson",
"mvn",
"ninja",
"nm",
"notebook",
"objdump",
"pdb",
"pip",
"pip3",
"pipx",
"pub",
"pudb",
"python",
"python3",
"strip",
"swift",
"swiftc",
"xcodebuild"
],
"script_commands": [],
"custom_commands": [],
"detected_stack": {
"languages": [
"python",
"java",
"kotlin",
"c",
"cpp",
"swift",
"dart"
],
"package_managers": [
"pub"
],
"frameworks": [
"flutter"
],
"databases": [],
"infrastructure": [],
"cloud_providers": [],
"code_quality_tools": [],
"version_managers": []
},
"custom_scripts": {
"npm_scripts": [],
"make_targets": [],
"poetry_scripts": [],
"cargo_aliases": [],
"shell_scripts": []
},
"project_dir": "/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter",
"created_at": "2026-01-20T11:12:23.419489",
"project_hash": "7fd6fc12b9f5a8ae884a529baeb5487f",
"inherited_from": "/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter"
}

25
.auto-claude-status Normal file
View File

@ -0,0 +1,25 @@
{
"active": true,
"spec": "001-normalize-code-update-packages-widgetify-component",
"state": "building",
"subtasks": {
"completed": 11,
"total": 14,
"in_progress": 1,
"failed": 0
},
"phase": {
"current": "Cleanup and Verification",
"id": null,
"total": 3
},
"workers": {
"active": 0,
"max": 1
},
"session": {
"number": 12,
"started_at": "2026-01-20T11:20:56.182893"
},
"last_update": "2026-01-20T11:47:55.069999"
}

39
.claude_settings.json Normal file
View File

@ -0,0 +1,39 @@
{
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true
},
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Read(./**)",
"Write(./**)",
"Edit(./**)",
"Glob(./**)",
"Grep(./**)",
"Read(/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/.auto-claude/worktrees/tasks/001-normalize-code-update-packages-widgetify-component/**)",
"Write(/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/.auto-claude/worktrees/tasks/001-normalize-code-update-packages-widgetify-component/**)",
"Edit(/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/.auto-claude/worktrees/tasks/001-normalize-code-update-packages-widgetify-component/**)",
"Glob(/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/.auto-claude/worktrees/tasks/001-normalize-code-update-packages-widgetify-component/**)",
"Grep(/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/.auto-claude/worktrees/tasks/001-normalize-code-update-packages-widgetify-component/**)",
"Read(/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/.auto-claude/worktrees/tasks/001-normalize-code-update-packages-widgetify-component/.auto-claude/specs/001-normalize-code-update-packages-widgetify-component/**)",
"Write(/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/.auto-claude/worktrees/tasks/001-normalize-code-update-packages-widgetify-component/.auto-claude/specs/001-normalize-code-update-packages-widgetify-component/**)",
"Edit(/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/.auto-claude/worktrees/tasks/001-normalize-code-update-packages-widgetify-component/.auto-claude/specs/001-normalize-code-update-packages-widgetify-component/**)",
"Read(/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/.auto-claude/**)",
"Write(/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/.auto-claude/**)",
"Edit(/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/.auto-claude/**)",
"Glob(/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/.auto-claude/**)",
"Grep(/Users/mathias/Documents/workspaces/plan-b/ionic-planb-logistic-app-flutter/.auto-claude/**)",
"Bash(*)",
"WebFetch(*)",
"WebSearch(*)",
"mcp__context7__resolve-library-id(*)",
"mcp__context7__get-library-docs(*)",
"mcp__graphiti-memory__search_nodes(*)",
"mcp__graphiti-memory__search_facts(*)",
"mcp__graphiti-memory__add_episode(*)",
"mcp__graphiti-memory__get_episodes(*)",
"mcp__graphiti-memory__get_entity_edge(*)"
]
}
}

3
.gitignore vendored
View File

@ -43,3 +43,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# Auto Claude data directory
.auto-claude/

View File

@ -4,7 +4,6 @@ import '../l10n/app_localizations.dart';
import '../models/delivery_route.dart';
import '../theme/spacing_system.dart';
import '../theme/size_system.dart';
import '../theme/animation_system.dart';
import '../theme/color_system.dart';
import '../utils/breakpoints.dart';
import '../providers/providers.dart';
@ -73,7 +72,7 @@ class _CollapsibleRoutesSidebarState extends ConsumerState<CollapsibleRoutesSide
final isMobile = context.isMobile;
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final isExpanded = ref.watch(collapseStateProvider);
final l10n = AppLocalizations.of(context)!;
final l10n = AppLocalizations.of(context);
// On mobile, always show as collapsible
if (isMobile) {

View File

@ -738,60 +738,4 @@ class _DarkModeMapComponentState extends State<DarkModeMapComponent> {
);
}
Widget _buildActionButton({
required String label,
required IconData icon,
required VoidCallback? onPressed,
required Color color,
}) {
final isDisabled = onPressed == null;
final buttonColor = isDisabled ? color.withValues(alpha: 0.5) : color;
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: buttonColor,
borderRadius: BorderRadius.circular(8),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: Colors.white,
size: 18,
),
const SizedBox(width: 6),
Text(
label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,183 @@
import 'package:flutter/material.dart';
import '../models/delivery.dart';
import '../l10n/app_localizations.dart';
/// A card widget that displays delivery information with action buttons.
///
/// This component shows delivery details including customer name, contact,
/// address, order information, and provides quick action buttons for
/// calling, navigating, and more options.
class DeliveryCard extends StatelessWidget {
final Delivery delivery;
final bool isSelected;
final VoidCallback onTap;
final Function(Delivery, String) onAction;
const DeliveryCard({
super.key,
required this.delivery,
this.isSelected = false,
required this.onTap,
required this.onAction,
});
@override
Widget build(BuildContext context) {
final contact = delivery.orders.isNotEmpty && delivery.orders.first.contact != null
? delivery.orders.first.contact
: null;
final order = delivery.orders.isNotEmpty ? delivery.orders.first : null;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: isSelected
? Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.3)
: null,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
delivery.name,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (contact != null)
Text(
contact.fullName,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
if (delivery.delivered)
Chip(
label: Text(AppLocalizations.of(context).delivered),
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
)
else if (order?.isNewCustomer ?? false)
Chip(
label: Text(AppLocalizations.of(context).newCustomer),
backgroundColor: const Color(0xFFFFFBEB),
),
],
),
const SizedBox(height: 12),
if (delivery.deliveryAddress != null)
Text(
delivery.deliveryAddress!.formattedAddress ?? '',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (order != null) ...[
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (order.totalItems != null)
Text(
AppLocalizations.of(context).items(order.totalItems!),
style: Theme.of(context).textTheme.bodySmall,
),
Text(
AppLocalizations.of(context).moneyCurrency(order.totalAmount),
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
],
),
],
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: [
if (contact?.phoneNumber != null)
OutlinedButton.icon(
onPressed: () => onAction(delivery, 'call'),
icon: const Icon(Icons.phone),
label: Text(AppLocalizations.of(context).call),
),
if (delivery.deliveryAddress != null)
OutlinedButton.icon(
onPressed: () {
onTap(); // Select the delivery
onAction(delivery, 'map');
},
icon: const Icon(Icons.map),
label: Text(AppLocalizations.of(context).navigate),
),
OutlinedButton.icon(
onPressed: () => _showDeliveryActions(context),
icon: const Icon(Icons.more_vert),
label: Text(AppLocalizations.of(context).more),
),
],
),
],
),
),
),
);
}
void _showDeliveryActions(BuildContext context) {
final l10n = AppLocalizations.of(context);
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!delivery.delivered)
ListTile(
leading: const Icon(Icons.check_circle),
title: Text(l10n.markAsCompleted),
onTap: () {
Navigator.pop(context);
onAction(delivery, 'complete');
},
)
else
ListTile(
leading: const Icon(Icons.undo),
title: Text(l10n.markAsUncompleted),
onTap: () {
Navigator.pop(context);
onAction(delivery, 'uncomplete');
},
),
ListTile(
leading: const Icon(Icons.camera_alt),
title: Text(l10n.uploadPhoto),
onTap: () {
Navigator.pop(context);
onAction(delivery, 'photo');
},
),
ListTile(
leading: const Icon(Icons.description),
title: Text(l10n.viewDetails),
onTap: () {
Navigator.pop(context);
onAction(delivery, 'details');
},
),
],
),
),
);
}
}

View File

@ -95,7 +95,7 @@ class _DeliveryListItemState extends State<DeliveryListItem>
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final statusColor = _getStatusColor(widget.delivery);
final l10n = AppLocalizations.of(context)!;
final l10n = AppLocalizations.of(context);
// Collapsed view: Show only the badge
if (widget.isCollapsed) {

View File

@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
/// A reusable loading dialog component with a spinner and message.
///
/// Use the static [show] method to display the dialog and [hide] to dismiss it.
///
/// Example:
/// ```dart
/// // Show loading dialog
/// LoadingDialog.show(context, message: 'Loading...');
///
/// // Perform async operation
/// await someAsyncOperation();
///
/// // Hide loading dialog
/// LoadingDialog.hide(context);
/// ```
class LoadingDialog extends StatelessWidget {
final String message;
const LoadingDialog({
super.key,
required this.message,
});
/// Shows a loading dialog with the specified [message].
///
/// The dialog is non-dismissible by tapping outside.
/// Use [hide] to dismiss the dialog when the operation completes.
static Future<void> show(
BuildContext context, {
required String message,
}) {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return LoadingDialog(message: message);
},
);
}
/// Hides the currently displayed loading dialog.
///
/// Should be called after showing a dialog with [show].
static void hide(BuildContext context) {
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Center(
child: Card(
elevation: 4,
color: colorScheme.surface,
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
color: colorScheme.primary,
),
const SizedBox(height: 16),
Text(
message,
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
}

View File

@ -6,10 +6,10 @@ class NavigationTermsAndConditionsDialog extends StatelessWidget {
final VoidCallback? onDecline;
const NavigationTermsAndConditionsDialog({
Key? key,
super.key,
required this.onAccept,
this.onDecline,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View File

@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:planb_logistic/l10n/app_localizations.dart';
import '../models/delivery.dart';
/// A dialog component for displaying delivery notes.
///
/// This dialog extracts and displays all non-empty notes from the
/// orders associated with a delivery.
class NotesDialog extends StatelessWidget {
/// The delivery whose notes should be displayed.
final Delivery delivery;
const NotesDialog({
super.key,
required this.delivery,
});
/// Extracts non-empty notes from the delivery's orders.
List<String> _extractNotes() {
return delivery.orders
.where((order) => order.note != null && order.note!.isNotEmpty)
.map((order) => order.note!)
.toList();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final colorScheme = Theme.of(context).colorScheme;
final notes = _extractNotes();
return AlertDialog(
title: Text(
l10n.notesTitle(delivery.name),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: colorScheme.onSurface,
),
),
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(context).textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurface,
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(context).pop(),
child: Text(
l10n.close,
style: TextStyle(color: colorScheme.onPrimary),
),
),
),
],
);
}
/// Shows the notes dialog for a delivery.
///
/// Returns `true` if the dialog was shown (i.e., the delivery has notes),
/// `false` otherwise.
///
/// If the delivery has no notes, this method returns `false` without
/// showing the dialog. The caller is responsible for handling this case,
/// typically by showing an info message.
static Future<bool> show(BuildContext context, Delivery delivery) async {
final notes = delivery.orders
.where((order) => order.note != null && order.note!.isNotEmpty)
.map((order) => order.note!)
.toList();
if (notes.isEmpty) {
return false;
}
await showDialog<void>(
context: context,
builder: (BuildContext dialogContext) {
return NotesDialog(delivery: delivery);
},
);
return true;
}
}

View File

@ -0,0 +1,133 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:planb_logistic/l10n/app_localizations.dart';
/// A dialog component for confirming and displaying captured photos before upload.
///
/// This dialog shows a preview of the captured photo and prompts the user
/// to confirm the upload or cancel/retake.
///
/// Example:
/// ```dart
/// final confirmed = await PhotoCaptureDialog.show(
/// context,
/// imageFile: File(pickedFile.path),
/// deliveryName: delivery.name,
/// );
///
/// if (confirmed == true) {
/// // Proceed with upload
/// }
/// ```
class PhotoCaptureDialog extends StatelessWidget {
/// The captured image file to display.
final File imageFile;
/// The name of the delivery for the confirmation message.
final String deliveryName;
const PhotoCaptureDialog({
super.key,
required this.imageFile,
required this.deliveryName,
});
/// Shows the photo capture confirmation dialog.
///
/// Returns `true` if the user confirms the upload, `false` if cancelled.
/// Returns `null` if the dialog is dismissed without selection.
static Future<bool?> show(
BuildContext context, {
required File imageFile,
required String deliveryName,
}) {
return showDialog<bool>(
context: context,
builder: (BuildContext dialogContext) {
return PhotoCaptureDialog(
imageFile: imageFile,
deliveryName: deliveryName,
);
},
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final screenSize = MediaQuery.of(context).size;
return AlertDialog(
title: Text(
l10n.confirmPhoto,
style: textTheme.headlineSmall?.copyWith(
color: colorScheme.onSurface,
),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: screenSize.height * 0.5,
maxWidth: screenSize.width * 0.8,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
imageFile,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Icon(
Icons.broken_image,
size: 48,
color: colorScheme.error,
),
),
);
},
),
),
),
const SizedBox(height: 16),
Text(
l10n.uploadPhotoConfirmation(deliveryName),
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(
l10n.cancel,
style: TextStyle(color: colorScheme.error),
),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
),
child: Text(l10n.upload),
),
],
);
}
}

View File

@ -80,7 +80,7 @@ class _RouteListItemState extends State<RouteListItem>
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final statusColor = _getStatusColor(widget.route);
final l10n = AppLocalizations.of(context)!;
final l10n = AppLocalizations.of(context);
// Collapsed view: Show only the badge
if (widget.isCollapsed) {

View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import '../models/delivery.dart';
import '../l10n/app_localizations.dart';
import 'delivery_list_item.dart';
/// A unified list view for displaying deliveries with selection and action support.
///
/// This component provides a consistent delivery list experience across the app,
/// supporting both expanded and collapsed states for responsive sidebar layouts.
class UnifiedDeliveryListView extends StatelessWidget {
final List<Delivery> deliveries;
final Delivery? selectedDelivery;
final ScrollController scrollController;
final ValueChanged<Delivery> onDeliverySelected;
final Function(Delivery, String) onItemAction;
final bool isCollapsed;
const UnifiedDeliveryListView({
super.key,
required this.deliveries,
this.selectedDelivery,
required this.scrollController,
required this.onDeliverySelected,
required this.onItemAction,
this.isCollapsed = false,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
if (deliveries.isEmpty) {
return Center(
child: Text(l10n.noDeliveries),
);
}
return RefreshIndicator(
onRefresh: () async {
// Trigger refresh via provider
},
child: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.only(top: 4, bottom: 8),
physics: const AlwaysScrollableScrollPhysics(),
itemCount: deliveries.length,
itemBuilder: (context, index) {
final delivery = deliveries[index];
return DeliveryListItem(
delivery: delivery,
isSelected: selectedDelivery?.id == delivery.id,
onTap: () => onDeliverySelected(delivery),
onCall: () => onItemAction(delivery, 'call'),
onAction: (action) => onItemAction(delivery, action),
animationIndex: index,
isCollapsed: isCollapsed,
);
},
),
);
}
}

View File

@ -109,5 +109,45 @@
"passwordRequired": "Password is required",
"loginButton": "Login",
"navigate": "Navigate",
"upload": "Upload"
"upload": "Upload",
"notesTitle": "Notes for {name}",
"@notesTitle": {
"placeholders": {
"name": {"type": "String"}
}
},
"noNotesMessage": "No notes attached to this delivery",
"close": "Close",
"confirmPhoto": "Confirm Photo",
"uploadPhotoConfirmation": "Upload this photo for {name}?",
"@uploadPhotoConfirmation": {
"placeholders": {
"name": {"type": "String"}
}
},
"uploadingPhoto": "Uploading photo...",
"photoUploadSuccess": "Photo uploaded successfully",
"photoUploadFailed": "Upload failed: {statusCode}",
"@photoUploadFailed": {
"placeholders": {
"statusCode": {"type": "int"}
}
},
"cameraError": "Camera error: {message}",
"@cameraError": {
"placeholders": {
"message": {"type": "String"}
}
},
"uploadError": "Upload error: {message}",
"@uploadError": {
"placeholders": {
"message": {"type": "String"}
}
},
"serverError": "Server error - Please contact support",
"retake": "Retake",
"completingDelivery": "Completing delivery...",
"markingAsUncompleted": "Marking as uncompleted...",
"deliveryMarkedUncompleted": "Delivery marked as uncompleted"
}

View File

@ -109,5 +109,45 @@
"passwordRequired": "Le mot de passe est requis",
"loginButton": "Connexion",
"navigate": "Naviguer",
"upload": "Téléverser"
"upload": "Téléverser",
"notesTitle": "Notes pour {name}",
"@notesTitle": {
"placeholders": {
"name": {"type": "String"}
}
},
"noNotesMessage": "Aucune note associée à cette livraison",
"close": "Fermer",
"confirmPhoto": "Confirmer la photo",
"uploadPhotoConfirmation": "Telecharger cette photo pour {name}?",
"@uploadPhotoConfirmation": {
"placeholders": {
"name": {"type": "String"}
}
},
"uploadingPhoto": "Telechargement de la photo...",
"photoUploadSuccess": "Photo telechargee avec succes",
"photoUploadFailed": "Echec du telechargement: {statusCode}",
"@photoUploadFailed": {
"placeholders": {
"statusCode": {"type": "int"}
}
},
"cameraError": "Erreur de camera: {message}",
"@cameraError": {
"placeholders": {
"message": {"type": "String"}
}
},
"uploadError": "Erreur de telechargement: {message}",
"@uploadError": {
"placeholders": {
"message": {"type": "String"}
}
},
"serverError": "Erreur serveur - Veuillez contacter le support",
"retake": "Reprendre",
"completingDelivery": "Completion de la livraison...",
"markingAsUncompleted": "Marquage comme a livrer...",
"deliveryMarkedUncompleted": "Livraison marquee comme a livrer"
}

View File

@ -559,6 +559,96 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Upload'**
String get upload;
/// No description provided for @notesTitle.
///
/// In en, this message translates to:
/// **'Notes for {name}'**
String notesTitle(String name);
/// No description provided for @noNotesMessage.
///
/// In en, this message translates to:
/// **'No notes attached to this delivery'**
String get noNotesMessage;
/// No description provided for @close.
///
/// In en, this message translates to:
/// **'Close'**
String get close;
/// No description provided for @confirmPhoto.
///
/// In en, this message translates to:
/// **'Confirm Photo'**
String get confirmPhoto;
/// No description provided for @uploadPhotoConfirmation.
///
/// In en, this message translates to:
/// **'Upload this photo for {name}?'**
String uploadPhotoConfirmation(String name);
/// No description provided for @uploadingPhoto.
///
/// In en, this message translates to:
/// **'Uploading photo...'**
String get uploadingPhoto;
/// No description provided for @photoUploadSuccess.
///
/// In en, this message translates to:
/// **'Photo uploaded successfully'**
String get photoUploadSuccess;
/// No description provided for @photoUploadFailed.
///
/// In en, this message translates to:
/// **'Upload failed: {statusCode}'**
String photoUploadFailed(int statusCode);
/// No description provided for @cameraError.
///
/// In en, this message translates to:
/// **'Camera error: {message}'**
String cameraError(String message);
/// No description provided for @uploadError.
///
/// In en, this message translates to:
/// **'Upload error: {message}'**
String uploadError(String message);
/// No description provided for @serverError.
///
/// In en, this message translates to:
/// **'Server error - Please contact support'**
String get serverError;
/// No description provided for @retake.
///
/// In en, this message translates to:
/// **'Retake'**
String get retake;
/// No description provided for @completingDelivery.
///
/// In en, this message translates to:
/// **'Completing delivery...'**
String get completingDelivery;
/// No description provided for @markingAsUncompleted.
///
/// In en, this message translates to:
/// **'Marking as uncompleted...'**
String get markingAsUncompleted;
/// No description provided for @deliveryMarkedUncompleted.
///
/// In en, this message translates to:
/// **'Delivery marked as uncompleted'**
String get deliveryMarkedUncompleted;
}
class _AppLocalizationsDelegate

View File

@ -256,4 +256,59 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get upload => 'Upload';
@override
String notesTitle(String name) {
return 'Notes for $name';
}
@override
String get noNotesMessage => 'No notes attached to this delivery';
@override
String get close => 'Close';
@override
String get confirmPhoto => 'Confirm Photo';
@override
String uploadPhotoConfirmation(String name) {
return 'Upload this photo for $name?';
}
@override
String get uploadingPhoto => 'Uploading photo...';
@override
String get photoUploadSuccess => 'Photo uploaded successfully';
@override
String photoUploadFailed(int statusCode) {
return 'Upload failed: $statusCode';
}
@override
String cameraError(String message) {
return 'Camera error: $message';
}
@override
String uploadError(String message) {
return 'Upload error: $message';
}
@override
String get serverError => 'Server error - Please contact support';
@override
String get retake => 'Retake';
@override
String get completingDelivery => 'Completing delivery...';
@override
String get markingAsUncompleted => 'Marking as uncompleted...';
@override
String get deliveryMarkedUncompleted => 'Delivery marked as uncompleted';
}

View File

@ -256,4 +256,59 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get upload => 'Téléverser';
@override
String notesTitle(String name) {
return 'Notes pour $name';
}
@override
String get noNotesMessage => 'Aucune note associée à cette livraison';
@override
String get close => 'Fermer';
@override
String get confirmPhoto => 'Confirmer la photo';
@override
String uploadPhotoConfirmation(String name) {
return 'Telecharger cette photo pour $name?';
}
@override
String get uploadingPhoto => 'Telechargement de la photo...';
@override
String get photoUploadSuccess => 'Photo telechargee avec succes';
@override
String photoUploadFailed(int statusCode) {
return 'Echec du telechargement: $statusCode';
}
@override
String cameraError(String message) {
return 'Erreur de camera: $message';
}
@override
String uploadError(String message) {
return 'Erreur de telechargement: $message';
}
@override
String get serverError => 'Erreur serveur - Veuillez contacter le support';
@override
String get retake => 'Reprendre';
@override
String get completingDelivery => 'Completion de la livraison...';
@override
String get markingAsUncompleted => 'Marquage comme a livrer...';
@override
String get deliveryMarkedUncompleted => 'Livraison marquee comme a livrer';
}

View File

@ -11,11 +11,6 @@ import 'pages/routes_page.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
runApp(
const ProviderScope(
child: PlanBLogisticApp(),
@ -71,7 +66,7 @@ class PlanBLogisticApp extends ConsumerWidget {
),
),
),
error: (_, __) => MaterialApp(
error: (error, stackTrace) => MaterialApp(
title: 'Plan B Logistics',
theme: MaterialTheme(const TextTheme()).light(),
darkTheme: MaterialTheme(const TextTheme()).dark(),

View File

@ -12,7 +12,9 @@ import '../api/openapi_config.dart';
import '../models/delivery_commands.dart';
import '../components/map_sidebar_layout.dart';
import '../components/dark_mode_map.dart';
import '../components/delivery_list_item.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 {
@ -86,7 +88,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
final tokenAsync = ref.watch(authTokenProvider);
final token = tokenAsync.hasValue ? tokenAsync.value : null;
final l10n = AppLocalizations.of(context)!;
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
@ -298,7 +300,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
}
if (token == null) {
final l10n = AppLocalizations.of(context)!;
final l10n = AppLocalizations.of(context);
ToastHelper.showError(context, l10n.authenticationRequired);
return;
}
@ -320,7 +322,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
);
result.when(
success: (_) {
final l10n = AppLocalizations.of(context)!;
final l10n = AppLocalizations.of(context);
// ignore: unused_result
ref.refresh(deliveriesProvider(widget.routeFragmentId));
// ignore: unused_result
@ -328,7 +330,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
ToastHelper.showSuccess(context, l10n.deliverySuccessful);
},
onError: (error) {
final l10n = AppLocalizations.of(context)!;
final l10n = AppLocalizations.of(context);
ToastHelper.showError(context, l10n.error(error.message));
},
);
@ -341,7 +343,6 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
);
result.when(
success: (_) {
final l10n = AppLocalizations.of(context)!;
// ignore: unused_result
ref.refresh(deliveriesProvider(widget.routeFragmentId));
// ignore: unused_result
@ -349,7 +350,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
ToastHelper.showSuccess(context, 'Delivery marked as uncompleted');
},
onError: (error) {
final l10n = AppLocalizations.of(context)!;
final l10n = AppLocalizations.of(context);
ToastHelper.showError(context, l10n.error(error.message));
},
);
@ -384,7 +385,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
String? token,
) async {
if (token == null) {
final l10n = AppLocalizations.of(context)!;
final l10n = AppLocalizations.of(context);
ToastHelper.showError(context, l10n.authenticationRequired);
return;
}
@ -410,38 +411,10 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
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: Text(AppLocalizations.of(context)!.cancel),
),
ElevatedButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text(AppLocalizations.of(context)!.upload),
),
],
);
},
final bool? confirmed = await PhotoCaptureDialog.show(
context,
imageFile: File(pickedFile.path),
deliveryName: delivery.name,
);
if (confirmed != true) {
@ -450,27 +423,8 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
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...'),
],
),
),
),
);
},
);
final localizations = AppLocalizations.of(context);
LoadingDialog.show(context, message: localizations.uploadingPhoto);
try {
final Uri uploadUrl = Uri.parse(
@ -485,7 +439,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
final http.Response response = await http.Response.fromStream(streamedResponse);
if (context.mounted) {
Navigator.of(context).pop();
LoadingDialog.hide(context);
}
if (response.statusCode >= 200 && response.statusCode < 300) {
@ -501,237 +455,9 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
}
} catch (e) {
if (context.mounted) {
Navigator.of(context).pop();
LoadingDialog.hide(context);
ToastHelper.showError(context, 'Upload error: $e');
}
}
}
}
class UnifiedDeliveryListView extends StatelessWidget {
final List<Delivery> deliveries;
final Delivery? selectedDelivery;
final ScrollController scrollController;
final ValueChanged<Delivery> onDeliverySelected;
final Function(Delivery, String) onItemAction;
final bool isCollapsed;
const UnifiedDeliveryListView({
super.key,
required this.deliveries,
this.selectedDelivery,
required this.scrollController,
required this.onDeliverySelected,
required this.onItemAction,
this.isCollapsed = false,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
if (deliveries.isEmpty) {
return Center(
child: Text(l10n.noDeliveries),
);
}
return RefreshIndicator(
onRefresh: () async {
// Trigger refresh via provider
},
child: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.only(top: 4, bottom: 8),
physics: const AlwaysScrollableScrollPhysics(),
itemCount: deliveries.length, // Show all deliveries with scrolling
itemBuilder: (context, index) {
final delivery = deliveries[index];
return DeliveryListItem(
delivery: delivery,
isSelected: selectedDelivery?.id == delivery.id,
onTap: () => onDeliverySelected(delivery),
onCall: () => onItemAction(delivery, 'call'),
onAction: (action) => onItemAction(delivery, action),
animationIndex: index,
isCollapsed: isCollapsed,
);
},
),
);
}
}
class DeliveryCard extends StatelessWidget {
final Delivery delivery;
final bool isSelected;
final VoidCallback onTap;
final Function(Delivery, String) onAction;
const DeliveryCard({
super.key,
required this.delivery,
this.isSelected = false,
required this.onTap,
required this.onAction,
});
@override
Widget build(BuildContext context) {
final contact = delivery.orders.isNotEmpty && delivery.orders.first.contact != null
? delivery.orders.first.contact
: null;
final order = delivery.orders.isNotEmpty ? delivery.orders.first : null;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: isSelected
? Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.3)
: null,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
delivery.name,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (contact != null)
Text(
contact.fullName,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
if (delivery.delivered)
Chip(
label: Text(AppLocalizations.of(context)!.delivered),
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
)
else if (order?.isNewCustomer ?? false)
Chip(
label: Text(AppLocalizations.of(context)!.newCustomer),
backgroundColor: const Color(0xFFFFFBEB),
),
],
),
const SizedBox(height: 12),
if (delivery.deliveryAddress != null)
Text(
delivery.deliveryAddress!.formattedAddress ?? '',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (order != null) ...[
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (order.totalItems != null)
Text(
AppLocalizations.of(context)!.items(order.totalItems!),
style: Theme.of(context).textTheme.bodySmall,
),
Text(
AppLocalizations.of(context)!.moneyCurrency(order.totalAmount),
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
],
),
],
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: [
if (contact?.phoneNumber != null)
OutlinedButton.icon(
onPressed: () => onAction(delivery, 'call'),
icon: const Icon(Icons.phone),
label: Text(AppLocalizations.of(context)!.call),
),
if (delivery.deliveryAddress != null)
OutlinedButton.icon(
onPressed: () {
onTap(); // Select the delivery
onAction(delivery, 'map');
},
icon: const Icon(Icons.map),
label: Text(AppLocalizations.of(context)!.navigate),
),
OutlinedButton.icon(
onPressed: () => _showDeliveryActions(context),
icon: const Icon(Icons.more_vert),
label: Text(AppLocalizations.of(context)!.more),
),
],
),
],
),
),
),
);
}
void _showDeliveryActions(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!delivery.delivered)
ListTile(
leading: const Icon(Icons.check_circle),
title: Text(l10n.markAsCompleted),
onTap: () {
Navigator.pop(context);
onAction(delivery, 'complete');
},
)
else
ListTile(
leading: const Icon(Icons.undo),
title: Text(l10n.markAsUncompleted),
onTap: () {
Navigator.pop(context);
onAction(delivery, 'uncomplete');
},
),
ListTile(
leading: const Icon(Icons.camera_alt),
title: Text(l10n.uploadPhoto),
onTap: () {
Navigator.pop(context);
// TODO: Implement photo upload
},
),
ListTile(
leading: const Icon(Icons.description),
title: Text(l10n.viewDetails),
onTap: () {
Navigator.pop(context);
// TODO: Navigate to delivery details
},
),
],
),
),
);
}
}

View File

@ -79,7 +79,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
),
const SizedBox(height: 24),
Text(
AppLocalizations.of(context)!.appTitle,
AppLocalizations.of(context).appTitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.displayMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
@ -88,7 +88,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context)!.appDescription,
AppLocalizations.of(context).appDescription,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
@ -98,8 +98,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
TextFormField(
controller: _usernameController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.username,
hintText: AppLocalizations.of(context)!.usernameHint,
labelText: AppLocalizations.of(context).username,
hintText: AppLocalizations.of(context).usernameHint,
prefixIcon: const Icon(Icons.person),
border: const OutlineInputBorder(),
),
@ -107,7 +107,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
enabled: !_isLoading,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return AppLocalizations.of(context)!.usernameRequired;
return AppLocalizations.of(context).usernameRequired;
}
return null;
},
@ -116,8 +116,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.password,
hintText: AppLocalizations.of(context)!.passwordHint,
labelText: AppLocalizations.of(context).password,
hintText: AppLocalizations.of(context).passwordHint,
prefixIcon: const Icon(Icons.lock),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
@ -137,7 +137,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
onFieldSubmitted: (_) => _handleLogin(),
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context)!.passwordRequired;
return AppLocalizations.of(context).passwordRequired;
}
return null;
},
@ -159,7 +159,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
),
),
)
: Text(AppLocalizations.of(context)!.loginButton),
: Text(AppLocalizations.of(context).loginButton),
),
],
),

View File

@ -11,10 +11,12 @@ 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 '../components/loading_dialog.dart';
import '../components/notes_dialog.dart';
import '../components/photo_capture_dialog.dart';
import '../services/location_permission_service.dart';
import 'deliveries_page.dart';
import 'settings_page.dart';
@ -81,13 +83,14 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
Delivery delivery,
int routeFragmentId,
) async {
// Capture l10n before async operations to avoid BuildContext across async gaps
final l10n = AppLocalizations.of(context);
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) {
final l10n = AppLocalizations.of(context)!;
ToastHelper.showError(context, l10n.authenticationRequired);
}
return;
@ -102,27 +105,7 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
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...'),
],
),
),
),
);
},
);
LoadingDialog.show(context, message: l10n.completingDelivery);
}
final result = await authClient.executeCommand(
@ -134,7 +117,7 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
result.when(
success: (_) async {
if (mounted) {
Navigator.of(context).pop();
LoadingDialog.hide(context);
}
if (mounted) {
@ -176,23 +159,21 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
}
if (mounted) {
final l10n = AppLocalizations.of(context)!;
ToastHelper.showSuccess(context, l10n.deliverySuccessful);
}
}
},
onError: (error) {
if (mounted) {
Navigator.of(context).pop();
LoadingDialog.hide(context);
}
debugPrint('Complete delivery failed - Type: ${error.type}, Message: ${error.message}');
debugPrint('Error details: ${error.details}');
if (mounted) {
final l10n = AppLocalizations.of(context)!;
String errorMessage = l10n.error(error.message);
if (error.statusCode == 500) {
errorMessage = 'Server error - Please contact support';
errorMessage = l10n.serverError;
}
ToastHelper.showError(context, errorMessage);
}
@ -202,37 +183,17 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
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...'),
],
),
),
),
);
},
);
LoadingDialog.show(context, message: l10n.markingAsUncompleted);
}
final result = await authClient.executeCommand(
final uncompleteResult = await authClient.executeCommand(
endpoint: 'markDeliveryAsUncompleted',
command: MarkDeliveryAsUncompletedCommand(deliveryId: delivery.id),
);
result.when(
uncompleteResult.when(
success: (_) async {
if (mounted) {
Navigator.of(context).pop();
LoadingDialog.hide(context);
}
if (mounted) {
@ -257,18 +218,16 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
}
if (mounted) {
final l10n = AppLocalizations.of(context)!;
ToastHelper.showSuccess(context, 'Delivery marked as uncompleted');
ToastHelper.showSuccess(context, l10n.deliveryMarkedUncompleted);
}
}
},
onError: (error) {
if (mounted) {
Navigator.of(context).pop();
LoadingDialog.hide(context);
}
if (mounted) {
final l10n = AppLocalizations.of(context)!;
ToastHelper.showError(context, l10n.error(error.message));
}
},
@ -289,12 +248,12 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
Delivery delivery,
) async {
final authService = ref.read(authServiceProvider);
final l10n = AppLocalizations.of(context);
// Ensure we have a valid token (automatically refreshes if needed)
final token = await authService.ensureValidToken();
if (token == null) {
if (mounted) {
final l10n = AppLocalizations.of(context)!;
ToastHelper.showError(context, l10n.authenticationRequired);
}
return;
@ -309,7 +268,7 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
);
} catch (e) {
if (mounted) {
ToastHelper.showError(context, 'Camera error: $e');
ToastHelper.showError(context, l10n.cameraError(e.toString()));
}
return;
}
@ -320,45 +279,11 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
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'),
),
],
);
},
// Show photo confirmation dialog
final bool? confirmed = await PhotoCaptureDialog.show(
context,
imageFile: File(pickedFile.path),
deliveryName: delivery.name,
);
if (confirmed != true) {
@ -367,27 +292,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
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...'),
],
),
),
),
);
},
);
// Show uploading dialog
LoadingDialog.show(context, message: l10n.uploadingPhoto);
try {
final Uri uploadUrl = Uri.parse(
@ -408,33 +314,31 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
client.close();
if (mounted) {
Navigator.of(context).pop();
LoadingDialog.hide(context);
}
if (response.statusCode >= 200 && response.statusCode < 300) {
if (mounted) {
ToastHelper.showSuccess(context, 'Photo uploaded successfully');
ToastHelper.showSuccess(context, l10n.photoUploadSuccess);
}
ref.refresh(allDeliveriesProvider);
ref.invalidate(allDeliveriesProvider);
} else {
debugPrint('Photo upload failed - Status: ${response.statusCode}');
debugPrint('Response body: ${response.body}');
if (mounted) {
String errorMessage = 'Upload failed';
String errorMessage = l10n.photoUploadFailed(response.statusCode);
if (response.statusCode == 500) {
errorMessage = 'Server error - Please contact support';
errorMessage = l10n.serverError;
} else if (response.statusCode == 401) {
errorMessage = 'Authentication required - Please log in again';
} else {
errorMessage = 'Upload failed: ${response.statusCode}';
errorMessage = l10n.authenticationRequired;
}
ToastHelper.showError(context, errorMessage);
}
}
} catch (e) {
if (mounted) {
Navigator.of(context).pop();
ToastHelper.showError(context, 'Upload error: $e');
LoadingDialog.hide(context);
ToastHelper.showError(context, l10n.uploadError(e.toString()));
}
}
}
@ -462,53 +366,14 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
}
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;
}
final l10n = AppLocalizations.of(context);
final hasNotes = await NotesDialog.show(context, delivery);
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'),
),
),
],
);
},
);
if (!hasNotes && mounted) {
ToastHelper.showInfo(context, l10n.noNotesMessage);
}
}
@override
@ -516,7 +381,7 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
final routesData = ref.watch(deliveryRoutesProvider);
final allDeliveriesData = ref.watch(allDeliveriesProvider);
final userProfile = ref.watch(userProfileProvider);
final l10n = AppLocalizations.of(context)!;
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
@ -535,8 +400,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
onPressed: (routesData.isLoading || allDeliveriesData.isLoading)
? null
: () {
ref.refresh(deliveryRoutesProvider);
ref.refresh(allDeliveriesProvider);
ref.invalidate(deliveryRoutesProvider);
ref.invalidate(allDeliveriesProvider);
},
tooltip: 'Refresh',
),

View File

@ -11,7 +11,7 @@ class SettingsPage extends ConsumerWidget {
final userProfile = ref.watch(userProfileProvider);
final languageAsync = ref.watch(languageProvider);
final themeMode = ref.watch(themeModeProvider);
final l10n = AppLocalizations.of(context)!;
final l10n = AppLocalizations.of(context);
final language = languageAsync.maybeWhen(
data: (value) => value,

View File

@ -57,11 +57,9 @@ final deliveryRoutesProvider = FutureProvider<List<DeliveryRoute>>((ref) async {
query: _EmptyQuery(),
fromJson: (json) {
// API returns data wrapped in object with "data" field
if (json is Map<String, dynamic>) {
final data = json['data'];
if (data is List<dynamic>) {
return data.map((r) => DeliveryRoute.fromJson(r as Map<String, dynamic>)).toList();
}
final data = json['data'];
if (data is List<dynamic>) {
return data.map((r) => DeliveryRoute.fromJson(r as Map<String, dynamic>)).toList();
}
return [];
},
@ -88,11 +86,9 @@ final deliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, rout
query: _DeliveriesQuery(routeFragmentId: routeFragmentId),
fromJson: (json) {
// API returns data wrapped in object with "data" field
if (json is Map<String, dynamic>) {
final data = json['data'];
if (data is List<dynamic>) {
return data.map((d) => Delivery.fromJson(d as Map<String, dynamic>)).toList();
}
final data = json['data'];
if (data is List<dynamic>) {
return data.map((d) => Delivery.fromJson(d as Map<String, dynamic>)).toList();
}
return [];
},
@ -100,9 +96,9 @@ final deliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, rout
// Log error if API call failed
result.whenError((error) {
print('ERROR fetching deliveries for route $routeFragmentId: ${error.message}');
debugPrint('ERROR fetching deliveries for route $routeFragmentId: ${error.message}');
if (error.originalException != null) {
print('Original exception: ${error.originalException}');
debugPrint('Original exception: ${error.originalException}');
}
});

View File

@ -194,12 +194,12 @@ class StatusBadgeWidget extends StatelessWidget {
final double fontSize;
const StatusBadgeWidget({
Key? key,
super.key,
required this.status,
this.showIcon = true,
this.showLabel = true,
this.fontSize = 12,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
@ -242,11 +242,11 @@ class StatusAccentBar extends StatelessWidget {
final double height;
const StatusAccentBar({
Key? key,
super.key,
required this.status,
this.width = 4,
this.height = 60,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View File

@ -1,26 +1,27 @@
import 'package:flutter/foundation.dart';
import 'package:http_interceptor/http_interceptor.dart';
class LoggingInterceptor implements InterceptorContract {
@override
Future<BaseRequest> interceptRequest({required BaseRequest request}) async {
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print('📤 REQUEST: ${request.method} ${request.url}');
print('Headers: ${request.headers}');
debugPrint('----------------------------------------------------');
debugPrint('REQUEST: ${request.method} ${request.url}');
debugPrint('Headers: ${request.headers}');
if (request is Request) {
print('Body: ${request.body}');
debugPrint('Body: ${request.body}');
}
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
debugPrint('----------------------------------------------------');
return request;
}
@override
Future<BaseResponse> interceptResponse({required BaseResponse response}) async {
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print('📥 RESPONSE: ${response.statusCode} ${response.request?.url}');
debugPrint('----------------------------------------------------');
debugPrint('RESPONSE: ${response.statusCode} ${response.request?.url}');
if (response is Response) {
print('Body: ${response.body}');
debugPrint('Body: ${response.body}');
}
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
debugPrint('----------------------------------------------------');
return response;
}

View File

@ -25,14 +25,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.11"
analyzer_plugin:
dependency: transitive
description:
name: analyzer_plugin
sha256: dd574a0ab77de88b7d9c12bc4b626109a5ca9078216a79041a5c24c3a1bd103c
url: "https://pub.dev"
source: hosted
version: "0.13.7"
animate_do:
dependency: "direct main"
description:
@ -101,10 +93,10 @@ packages:
dependency: "direct dev"
description:
name: build_runner
sha256: "7b5b569f3df370590a85029148d6fc66c7d0201fc6f1847c07dd85d365ae9fcd"
sha256: b4d854962a32fd9f8efc0b76f98214790b833af8b2e9b2df6bfc927c0415a072
url: "https://pub.dev"
source: hosted
version: "2.10.3"
version: "2.10.5"
built_collection:
dependency: transitive
description:
@ -209,22 +201,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
custom_lint_core:
dependency: transitive
description:
name: custom_lint_core
sha256: "85b339346154d5646952d44d682965dfe9e12cae5febd706f0db3aa5010d6423"
url: "https://pub.dev"
source: hosted
version: "0.8.1"
custom_lint_visitor:
dependency: transitive
description:
name: custom_lint_visitor
sha256: "446d68322747ec1c36797090de776aa72228818d3d80685a91ff524d163fee6d"
url: "https://pub.dev"
source: hosted
version: "1.0.0+8.1.1"
dart_style:
dependency: transitive
description:
@ -351,10 +327,10 @@ packages:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "9e2d6907f12cc7d23a846847615941bddee8709bf2bfd274acdf5e80bcf22fde"
sha256: "38ec6c303e2c83ee84512f5fc2a82ae311531021938e63d7137eccc107bf3c02"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.1.0"
flutter_secure_storage:
dependency: "direct main"
description:
@ -457,18 +433,18 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: c92d18e1fe994cb06d48aa786c46b142a5633067e8297cff6b5a3ac742620104
sha256: eff94d2a6fc79fa8b811dde79c7549808c2346037ee107a1121b4a644c745f2a
url: "https://pub.dev"
source: hosted
version: "17.0.0"
version: "17.0.1"
google_navigation_flutter:
dependency: "direct main"
description:
name: google_navigation_flutter
sha256: fdf79ddeda8bbba9d8b9218c41551b918447032004cbe72ea7365287c9d9bf80
sha256: "12f8bc6f5cf694b0778772a2e22969c9a2da44e76b496c47488deb482a2fe063"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
version: "0.8.2"
graphs:
dependency: transitive
description:
@ -889,42 +865,42 @@ packages:
dependency: transitive
description:
name: riverpod
sha256: c406de02bff19d920b832bddfb8283548bfa05ce41c59afba57ce643e116aa59
sha256: "16ff608d21e8ea64364f2b7c049c94a02ab81668f78845862b6e88b71dd4935a"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.1.0"
riverpod_analyzer_utils:
dependency: transitive
description:
name: riverpod_analyzer_utils
sha256: a0f68adb078b790faa3c655110a017f9a7b7b079a57bbd40f540e80dce5fcd29
sha256: "947b05d04c52a546a2ac6b19ef2a54b08520ff6bdf9f23d67957a4c8df1c3bc0"
url: "https://pub.dev"
source: hosted
version: "1.0.0-dev.7"
version: "1.0.0-dev.8"
riverpod_annotation:
dependency: "direct main"
description:
name: riverpod_annotation
sha256: "7230014155777fc31ba3351bc2cb5a3b5717b11bfafe52b1553cb47d385f8897"
sha256: cc1474bc2df55ec3c1da1989d139dcef22cd5e2bd78da382e867a69a8eca2e46
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "4.0.0"
riverpod_generator:
dependency: "direct dev"
description:
name: riverpod_generator
sha256: "49894543a42cf7a9954fc4e7366b6d3cb2e6ec0fa07775f660afcdd92d097702"
sha256: e43b1537229cc8f487f09b0c20d15dba840acbadcf5fc6dad7ad5e8ab75950dc
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "4.0.0+1"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
version: "2.5.4"
shared_preferences_android:
dependency: transitive
description:
@ -1194,14 +1170,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.4"
uuid:
dependency: transitive
description:
name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
url: "https://pub.dev"
source: hosted
version: "4.5.2"
vector_math:
dependency: transitive
description:

View File

@ -13,8 +13,8 @@ dependencies:
cupertino_icons: ^1.0.8
flutter_riverpod: ^3.0.3
riverpod_annotation: ^3.0.3
flutter_riverpod: ^3.1.0
riverpod_annotation: ^4.0.0
animate_do: ^4.2.0
lottie: ^3.0.0
@ -37,10 +37,10 @@ dependencies:
url_launcher: ^6.3.1
permission_handler: ^12.0.1
go_router: ^17.0.0
shared_preferences: ^2.5.3
go_router: ^17.0.1
shared_preferences: ^2.5.4
http_interceptor: ^2.0.0
google_navigation_flutter: ^0.7.0
google_navigation_flutter: ^0.8.2
dev_dependencies:
flutter_test:
@ -48,9 +48,9 @@ dev_dependencies:
flutter_lints: ^6.0.0
build_runner: ^2.4.14
json_serializable: ^6.9.2
riverpod_generator: ^3.0.3
build_runner: ^2.10.5
json_serializable: ^6.11.1
riverpod_generator: ^4.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec