Merge pull request 'auto-claude/001-normalize-code-update-packages-widgetify-component' (#1) from auto-claude/001-normalize-code-update-packages-widgetify-component into main
Reviewed-on: #1
This commit is contained in:
commit
e0f9552cbf
211
.auto-claude-security.json
Normal file
211
.auto-claude-security.json
Normal 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
25
.auto-claude-status
Normal 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
39
.claude_settings.json
Normal 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
3
.gitignore
vendored
@ -43,3 +43,6 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Auto Claude data directory
|
||||
.auto-claude/
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
183
lib/components/delivery_card.dart
Normal file
183
lib/components/delivery_card.dart
Normal 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');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
81
lib/components/loading_dialog.dart
Normal file
81
lib/components/loading_dialog.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
104
lib/components/notes_dialog.dart
Normal file
104
lib/components/notes_dialog.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
133
lib/components/photo_capture_dialog.dart
Normal file
133
lib/components/photo_capture_dialog.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
61
lib/components/unified_delivery_list.dart
Normal file
61
lib/components/unified_delivery_list.dart
Normal 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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',
|
||||
),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -57,12 +57,10 @@ 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();
|
||||
}
|
||||
}
|
||||
return [];
|
||||
},
|
||||
);
|
||||
@ -88,21 +86,19 @@ 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();
|
||||
}
|
||||
}
|
||||
return [];
|
||||
},
|
||||
);
|
||||
|
||||
// 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}');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
68
pubspec.lock
68
pubspec.lock
@ -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:
|
||||
|
||||
16
pubspec.yaml
16
pubspec.yaml
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user