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/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
# Auto Claude data directory
|
||||||
|
.auto-claude/
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import '../l10n/app_localizations.dart';
|
|||||||
import '../models/delivery_route.dart';
|
import '../models/delivery_route.dart';
|
||||||
import '../theme/spacing_system.dart';
|
import '../theme/spacing_system.dart';
|
||||||
import '../theme/size_system.dart';
|
import '../theme/size_system.dart';
|
||||||
import '../theme/animation_system.dart';
|
|
||||||
import '../theme/color_system.dart';
|
import '../theme/color_system.dart';
|
||||||
import '../utils/breakpoints.dart';
|
import '../utils/breakpoints.dart';
|
||||||
import '../providers/providers.dart';
|
import '../providers/providers.dart';
|
||||||
@ -73,7 +72,7 @@ class _CollapsibleRoutesSidebarState extends ConsumerState<CollapsibleRoutesSide
|
|||||||
final isMobile = context.isMobile;
|
final isMobile = context.isMobile;
|
||||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||||
final isExpanded = ref.watch(collapseStateProvider);
|
final isExpanded = ref.watch(collapseStateProvider);
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
|
|
||||||
// On mobile, always show as collapsible
|
// On mobile, always show as collapsible
|
||||||
if (isMobile) {
|
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) {
|
Widget build(BuildContext context) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
final statusColor = _getStatusColor(widget.delivery);
|
final statusColor = _getStatusColor(widget.delivery);
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
|
|
||||||
// Collapsed view: Show only the badge
|
// Collapsed view: Show only the badge
|
||||||
if (widget.isCollapsed) {
|
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;
|
final VoidCallback? onDecline;
|
||||||
|
|
||||||
const NavigationTermsAndConditionsDialog({
|
const NavigationTermsAndConditionsDialog({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.onAccept,
|
required this.onAccept,
|
||||||
this.onDecline,
|
this.onDecline,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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) {
|
Widget build(BuildContext context) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
final statusColor = _getStatusColor(widget.route);
|
final statusColor = _getStatusColor(widget.route);
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
|
|
||||||
// Collapsed view: Show only the badge
|
// Collapsed view: Show only the badge
|
||||||
if (widget.isCollapsed) {
|
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",
|
"passwordRequired": "Password is required",
|
||||||
"loginButton": "Login",
|
"loginButton": "Login",
|
||||||
"navigate": "Navigate",
|
"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",
|
"passwordRequired": "Le mot de passe est requis",
|
||||||
"loginButton": "Connexion",
|
"loginButton": "Connexion",
|
||||||
"navigate": "Naviguer",
|
"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:
|
/// In en, this message translates to:
|
||||||
/// **'Upload'**
|
/// **'Upload'**
|
||||||
String get 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
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@ -256,4 +256,59 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get upload => 'Upload';
|
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
|
@override
|
||||||
String get upload => 'Téléverser';
|
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 {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
await SystemChrome.setPreferredOrientations([
|
|
||||||
DeviceOrientation.landscapeLeft,
|
|
||||||
DeviceOrientation.landscapeRight,
|
|
||||||
]);
|
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
const ProviderScope(
|
const ProviderScope(
|
||||||
child: PlanBLogisticApp(),
|
child: PlanBLogisticApp(),
|
||||||
@ -71,7 +66,7 @@ class PlanBLogisticApp extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
error: (_, __) => MaterialApp(
|
error: (error, stackTrace) => MaterialApp(
|
||||||
title: 'Plan B Logistics',
|
title: 'Plan B Logistics',
|
||||||
theme: MaterialTheme(const TextTheme()).light(),
|
theme: MaterialTheme(const TextTheme()).light(),
|
||||||
darkTheme: MaterialTheme(const TextTheme()).dark(),
|
darkTheme: MaterialTheme(const TextTheme()).dark(),
|
||||||
|
|||||||
@ -12,7 +12,9 @@ import '../api/openapi_config.dart';
|
|||||||
import '../models/delivery_commands.dart';
|
import '../models/delivery_commands.dart';
|
||||||
import '../components/map_sidebar_layout.dart';
|
import '../components/map_sidebar_layout.dart';
|
||||||
import '../components/dark_mode_map.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';
|
import '../utils/toast_helper.dart';
|
||||||
|
|
||||||
class DeliveriesPage extends ConsumerStatefulWidget {
|
class DeliveriesPage extends ConsumerStatefulWidget {
|
||||||
@ -86,7 +88,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
|
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
|
||||||
final tokenAsync = ref.watch(authTokenProvider);
|
final tokenAsync = ref.watch(authTokenProvider);
|
||||||
final token = tokenAsync.hasValue ? tokenAsync.value : null;
|
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
|
// When embedded in sidebar, show only the delivery list with back button
|
||||||
// This is a responsive sidebar that collapses like routes
|
// This is a responsive sidebar that collapses like routes
|
||||||
@ -298,7 +300,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
ToastHelper.showError(context, l10n.authenticationRequired);
|
ToastHelper.showError(context, l10n.authenticationRequired);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -320,7 +322,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
);
|
);
|
||||||
result.when(
|
result.when(
|
||||||
success: (_) {
|
success: (_) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
// ignore: unused_result
|
// ignore: unused_result
|
||||||
ref.refresh(deliveriesProvider(widget.routeFragmentId));
|
ref.refresh(deliveriesProvider(widget.routeFragmentId));
|
||||||
// ignore: unused_result
|
// ignore: unused_result
|
||||||
@ -328,7 +330,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
ToastHelper.showSuccess(context, l10n.deliverySuccessful);
|
ToastHelper.showSuccess(context, l10n.deliverySuccessful);
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
ToastHelper.showError(context, l10n.error(error.message));
|
ToastHelper.showError(context, l10n.error(error.message));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -341,7 +343,6 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
);
|
);
|
||||||
result.when(
|
result.when(
|
||||||
success: (_) {
|
success: (_) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
// ignore: unused_result
|
// ignore: unused_result
|
||||||
ref.refresh(deliveriesProvider(widget.routeFragmentId));
|
ref.refresh(deliveriesProvider(widget.routeFragmentId));
|
||||||
// ignore: unused_result
|
// ignore: unused_result
|
||||||
@ -349,7 +350,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
ToastHelper.showSuccess(context, 'Delivery marked as uncompleted');
|
ToastHelper.showSuccess(context, 'Delivery marked as uncompleted');
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
ToastHelper.showError(context, l10n.error(error.message));
|
ToastHelper.showError(context, l10n.error(error.message));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -384,7 +385,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
String? token,
|
String? token,
|
||||||
) async {
|
) async {
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
ToastHelper.showError(context, l10n.authenticationRequired);
|
ToastHelper.showError(context, l10n.authenticationRequired);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -410,38 +411,10 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
||||||
final bool? confirmed = await showDialog<bool>(
|
final bool? confirmed = await PhotoCaptureDialog.show(
|
||||||
context: context,
|
context,
|
||||||
builder: (BuildContext dialogContext) {
|
imageFile: File(pickedFile.path),
|
||||||
return AlertDialog(
|
deliveryName: delivery.name,
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed != true) {
|
if (confirmed != true) {
|
||||||
@ -450,27 +423,8 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
||||||
showDialog(
|
final localizations = AppLocalizations.of(context);
|
||||||
context: context,
|
LoadingDialog.show(context, message: localizations.uploadingPhoto);
|
||||||
barrierDismissible: false,
|
|
||||||
builder: (BuildContext dialogContext) {
|
|
||||||
return const Center(
|
|
||||||
child: Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(24.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Text('Uploading photo...'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final Uri uploadUrl = Uri.parse(
|
final Uri uploadUrl = Uri.parse(
|
||||||
@ -485,7 +439,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
final http.Response response = await http.Response.fromStream(streamedResponse);
|
final http.Response response = await http.Response.fromStream(streamedResponse);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).pop();
|
LoadingDialog.hide(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||||
@ -501,237 +455,9 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).pop();
|
LoadingDialog.hide(context);
|
||||||
ToastHelper.showError(context, 'Upload error: $e');
|
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),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context)!.appTitle,
|
AppLocalizations.of(context).appTitle,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.displayMedium?.copyWith(
|
style: Theme.of(context).textTheme.displayMedium?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
@ -88,7 +88,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context)!.appDescription,
|
AppLocalizations.of(context).appDescription,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
@ -98,8 +98,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _usernameController,
|
controller: _usernameController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context)!.username,
|
labelText: AppLocalizations.of(context).username,
|
||||||
hintText: AppLocalizations.of(context)!.usernameHint,
|
hintText: AppLocalizations.of(context).usernameHint,
|
||||||
prefixIcon: const Icon(Icons.person),
|
prefixIcon: const Icon(Icons.person),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
@ -107,7 +107,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
enabled: !_isLoading,
|
enabled: !_isLoading,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
return AppLocalizations.of(context)!.usernameRequired;
|
return AppLocalizations.of(context).usernameRequired;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@ -116,8 +116,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context)!.password,
|
labelText: AppLocalizations.of(context).password,
|
||||||
hintText: AppLocalizations.of(context)!.passwordHint,
|
hintText: AppLocalizations.of(context).passwordHint,
|
||||||
prefixIcon: const Icon(Icons.lock),
|
prefixIcon: const Icon(Icons.lock),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
@ -137,7 +137,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
onFieldSubmitted: (_) => _handleLogin(),
|
onFieldSubmitted: (_) => _handleLogin(),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return AppLocalizations.of(context)!.passwordRequired;
|
return AppLocalizations.of(context).passwordRequired;
|
||||||
}
|
}
|
||||||
return null;
|
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 '../api/client.dart';
|
||||||
import '../utils/toast_helper.dart';
|
import '../utils/toast_helper.dart';
|
||||||
import '../api/openapi_config.dart';
|
import '../api/openapi_config.dart';
|
||||||
import '../utils/breakpoints.dart';
|
|
||||||
import '../utils/http_client_factory.dart';
|
import '../utils/http_client_factory.dart';
|
||||||
import '../components/collapsible_routes_sidebar.dart';
|
import '../components/collapsible_routes_sidebar.dart';
|
||||||
import '../components/dark_mode_map.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 '../services/location_permission_service.dart';
|
||||||
import 'deliveries_page.dart';
|
import 'deliveries_page.dart';
|
||||||
import 'settings_page.dart';
|
import 'settings_page.dart';
|
||||||
@ -81,13 +83,14 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
Delivery delivery,
|
Delivery delivery,
|
||||||
int routeFragmentId,
|
int routeFragmentId,
|
||||||
) async {
|
) async {
|
||||||
|
// Capture l10n before async operations to avoid BuildContext across async gaps
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
final authService = ref.read(authServiceProvider);
|
final authService = ref.read(authServiceProvider);
|
||||||
|
|
||||||
// Ensure we have a valid token (automatically refreshes if needed)
|
// Ensure we have a valid token (automatically refreshes if needed)
|
||||||
final token = await authService.ensureValidToken();
|
final token = await authService.ensureValidToken();
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
ToastHelper.showError(context, l10n.authenticationRequired);
|
ToastHelper.showError(context, l10n.authenticationRequired);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -102,27 +105,7 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
case 'complete':
|
case 'complete':
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showDialog(
|
LoadingDialog.show(context, message: l10n.completingDelivery);
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder: (BuildContext dialogContext) {
|
|
||||||
return const Center(
|
|
||||||
child: Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(24.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Text('Completing delivery...'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await authClient.executeCommand(
|
final result = await authClient.executeCommand(
|
||||||
@ -134,7 +117,7 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
result.when(
|
result.when(
|
||||||
success: (_) async {
|
success: (_) async {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pop();
|
LoadingDialog.hide(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -176,23 +159,21 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
ToastHelper.showSuccess(context, l10n.deliverySuccessful);
|
ToastHelper.showSuccess(context, l10n.deliverySuccessful);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pop();
|
LoadingDialog.hide(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('Complete delivery failed - Type: ${error.type}, Message: ${error.message}');
|
debugPrint('Complete delivery failed - Type: ${error.type}, Message: ${error.message}');
|
||||||
debugPrint('Error details: ${error.details}');
|
debugPrint('Error details: ${error.details}');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
String errorMessage = l10n.error(error.message);
|
String errorMessage = l10n.error(error.message);
|
||||||
if (error.statusCode == 500) {
|
if (error.statusCode == 500) {
|
||||||
errorMessage = 'Server error - Please contact support';
|
errorMessage = l10n.serverError;
|
||||||
}
|
}
|
||||||
ToastHelper.showError(context, errorMessage);
|
ToastHelper.showError(context, errorMessage);
|
||||||
}
|
}
|
||||||
@ -202,37 +183,17 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
|
|
||||||
case 'uncomplete':
|
case 'uncomplete':
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showDialog(
|
LoadingDialog.show(context, message: l10n.markingAsUncompleted);
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder: (BuildContext dialogContext) {
|
|
||||||
return const Center(
|
|
||||||
child: Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(24.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Text('Marking as uncompleted...'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await authClient.executeCommand(
|
final uncompleteResult = await authClient.executeCommand(
|
||||||
endpoint: 'markDeliveryAsUncompleted',
|
endpoint: 'markDeliveryAsUncompleted',
|
||||||
command: MarkDeliveryAsUncompletedCommand(deliveryId: delivery.id),
|
command: MarkDeliveryAsUncompletedCommand(deliveryId: delivery.id),
|
||||||
);
|
);
|
||||||
result.when(
|
uncompleteResult.when(
|
||||||
success: (_) async {
|
success: (_) async {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pop();
|
LoadingDialog.hide(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -257,18 +218,16 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
ToastHelper.showSuccess(context, l10n.deliveryMarkedUncompleted);
|
||||||
ToastHelper.showSuccess(context, 'Delivery marked as uncompleted');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pop();
|
LoadingDialog.hide(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
ToastHelper.showError(context, l10n.error(error.message));
|
ToastHelper.showError(context, l10n.error(error.message));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -289,12 +248,12 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
Delivery delivery,
|
Delivery delivery,
|
||||||
) async {
|
) async {
|
||||||
final authService = ref.read(authServiceProvider);
|
final authService = ref.read(authServiceProvider);
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
|
||||||
// Ensure we have a valid token (automatically refreshes if needed)
|
// Ensure we have a valid token (automatically refreshes if needed)
|
||||||
final token = await authService.ensureValidToken();
|
final token = await authService.ensureValidToken();
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
ToastHelper.showError(context, l10n.authenticationRequired);
|
ToastHelper.showError(context, l10n.authenticationRequired);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -309,7 +268,7 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ToastHelper.showError(context, 'Camera error: $e');
|
ToastHelper.showError(context, l10n.cameraError(e.toString()));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -320,45 +279,11 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
final bool? confirmed = await showDialog<bool>(
|
// Show photo confirmation dialog
|
||||||
context: context,
|
final bool? confirmed = await PhotoCaptureDialog.show(
|
||||||
builder: (BuildContext dialogContext) {
|
context,
|
||||||
return AlertDialog(
|
imageFile: File(pickedFile.path),
|
||||||
title: const Text('Confirm Photo'),
|
deliveryName: delivery.name,
|
||||||
content: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight: MediaQuery.of(dialogContext).size.height * 0.5,
|
|
||||||
maxWidth: MediaQuery.of(dialogContext).size.width * 0.8,
|
|
||||||
),
|
|
||||||
child: Image.file(
|
|
||||||
File(pickedFile!.path),
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'Upload this photo for ${delivery.name}?',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
|
||||||
child: const Text('Upload'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed != true) {
|
if (confirmed != true) {
|
||||||
@ -367,27 +292,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
showDialog(
|
// Show uploading dialog
|
||||||
context: context,
|
LoadingDialog.show(context, message: l10n.uploadingPhoto);
|
||||||
barrierDismissible: false,
|
|
||||||
builder: (BuildContext dialogContext) {
|
|
||||||
return const Center(
|
|
||||||
child: Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(24.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Text('Uploading photo...'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final Uri uploadUrl = Uri.parse(
|
final Uri uploadUrl = Uri.parse(
|
||||||
@ -408,33 +314,31 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
client.close();
|
client.close();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pop();
|
LoadingDialog.hide(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ToastHelper.showSuccess(context, 'Photo uploaded successfully');
|
ToastHelper.showSuccess(context, l10n.photoUploadSuccess);
|
||||||
}
|
}
|
||||||
ref.refresh(allDeliveriesProvider);
|
ref.invalidate(allDeliveriesProvider);
|
||||||
} else {
|
} else {
|
||||||
debugPrint('Photo upload failed - Status: ${response.statusCode}');
|
debugPrint('Photo upload failed - Status: ${response.statusCode}');
|
||||||
debugPrint('Response body: ${response.body}');
|
debugPrint('Response body: ${response.body}');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
String errorMessage = 'Upload failed';
|
String errorMessage = l10n.photoUploadFailed(response.statusCode);
|
||||||
if (response.statusCode == 500) {
|
if (response.statusCode == 500) {
|
||||||
errorMessage = 'Server error - Please contact support';
|
errorMessage = l10n.serverError;
|
||||||
} else if (response.statusCode == 401) {
|
} else if (response.statusCode == 401) {
|
||||||
errorMessage = 'Authentication required - Please log in again';
|
errorMessage = l10n.authenticationRequired;
|
||||||
} else {
|
|
||||||
errorMessage = 'Upload failed: ${response.statusCode}';
|
|
||||||
}
|
}
|
||||||
ToastHelper.showError(context, errorMessage);
|
ToastHelper.showError(context, errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pop();
|
LoadingDialog.hide(context);
|
||||||
ToastHelper.showError(context, 'Upload error: $e');
|
ToastHelper.showError(context, l10n.uploadError(e.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -462,53 +366,14 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showNotesDialog(Delivery delivery) async {
|
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 (!mounted) return;
|
||||||
|
|
||||||
if (notes.isEmpty) {
|
final l10n = AppLocalizations.of(context);
|
||||||
ToastHelper.showInfo(context, 'No notes attached to this delivery');
|
final hasNotes = await NotesDialog.show(context, delivery);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await showDialog(
|
if (!hasNotes && mounted) {
|
||||||
context: context,
|
ToastHelper.showInfo(context, l10n.noNotesMessage);
|
||||||
builder: (BuildContext dialogContext) {
|
}
|
||||||
return AlertDialog(
|
|
||||||
title: Text('Notes for ${delivery.name}'),
|
|
||||||
content: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: notes.map((note) => Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
|
||||||
child: Text(
|
|
||||||
note,
|
|
||||||
style: Theme.of(dialogContext).textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actionsAlignment: MainAxisAlignment.center,
|
|
||||||
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
|
||||||
actions: [
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
|
||||||
child: const Text('Close'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -516,7 +381,7 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
final routesData = ref.watch(deliveryRoutesProvider);
|
final routesData = ref.watch(deliveryRoutesProvider);
|
||||||
final allDeliveriesData = ref.watch(allDeliveriesProvider);
|
final allDeliveriesData = ref.watch(allDeliveriesProvider);
|
||||||
final userProfile = ref.watch(userProfileProvider);
|
final userProfile = ref.watch(userProfileProvider);
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@ -535,8 +400,8 @@ class _RoutesPageState extends ConsumerState<RoutesPage> {
|
|||||||
onPressed: (routesData.isLoading || allDeliveriesData.isLoading)
|
onPressed: (routesData.isLoading || allDeliveriesData.isLoading)
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
ref.refresh(deliveryRoutesProvider);
|
ref.invalidate(deliveryRoutesProvider);
|
||||||
ref.refresh(allDeliveriesProvider);
|
ref.invalidate(allDeliveriesProvider);
|
||||||
},
|
},
|
||||||
tooltip: 'Refresh',
|
tooltip: 'Refresh',
|
||||||
),
|
),
|
||||||
|
|||||||
@ -11,7 +11,7 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
final userProfile = ref.watch(userProfileProvider);
|
final userProfile = ref.watch(userProfileProvider);
|
||||||
final languageAsync = ref.watch(languageProvider);
|
final languageAsync = ref.watch(languageProvider);
|
||||||
final themeMode = ref.watch(themeModeProvider);
|
final themeMode = ref.watch(themeModeProvider);
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
|
|
||||||
final language = languageAsync.maybeWhen(
|
final language = languageAsync.maybeWhen(
|
||||||
data: (value) => value,
|
data: (value) => value,
|
||||||
|
|||||||
@ -57,12 +57,10 @@ final deliveryRoutesProvider = FutureProvider<List<DeliveryRoute>>((ref) async {
|
|||||||
query: _EmptyQuery(),
|
query: _EmptyQuery(),
|
||||||
fromJson: (json) {
|
fromJson: (json) {
|
||||||
// API returns data wrapped in object with "data" field
|
// API returns data wrapped in object with "data" field
|
||||||
if (json is Map<String, dynamic>) {
|
|
||||||
final data = json['data'];
|
final data = json['data'];
|
||||||
if (data is List<dynamic>) {
|
if (data is List<dynamic>) {
|
||||||
return data.map((r) => DeliveryRoute.fromJson(r as Map<String, dynamic>)).toList();
|
return data.map((r) => DeliveryRoute.fromJson(r as Map<String, dynamic>)).toList();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -88,21 +86,19 @@ final deliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, rout
|
|||||||
query: _DeliveriesQuery(routeFragmentId: routeFragmentId),
|
query: _DeliveriesQuery(routeFragmentId: routeFragmentId),
|
||||||
fromJson: (json) {
|
fromJson: (json) {
|
||||||
// API returns data wrapped in object with "data" field
|
// API returns data wrapped in object with "data" field
|
||||||
if (json is Map<String, dynamic>) {
|
|
||||||
final data = json['data'];
|
final data = json['data'];
|
||||||
if (data is List<dynamic>) {
|
if (data is List<dynamic>) {
|
||||||
return data.map((d) => Delivery.fromJson(d as Map<String, dynamic>)).toList();
|
return data.map((d) => Delivery.fromJson(d as Map<String, dynamic>)).toList();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Log error if API call failed
|
// Log error if API call failed
|
||||||
result.whenError((error) {
|
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) {
|
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;
|
final double fontSize;
|
||||||
|
|
||||||
const StatusBadgeWidget({
|
const StatusBadgeWidget({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.status,
|
required this.status,
|
||||||
this.showIcon = true,
|
this.showIcon = true,
|
||||||
this.showLabel = true,
|
this.showLabel = true,
|
||||||
this.fontSize = 12,
|
this.fontSize = 12,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -242,11 +242,11 @@ class StatusAccentBar extends StatelessWidget {
|
|||||||
final double height;
|
final double height;
|
||||||
|
|
||||||
const StatusAccentBar({
|
const StatusAccentBar({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.status,
|
required this.status,
|
||||||
this.width = 4,
|
this.width = 4,
|
||||||
this.height = 60,
|
this.height = 60,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@ -1,26 +1,27 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:http_interceptor/http_interceptor.dart';
|
import 'package:http_interceptor/http_interceptor.dart';
|
||||||
|
|
||||||
class LoggingInterceptor implements InterceptorContract {
|
class LoggingInterceptor implements InterceptorContract {
|
||||||
@override
|
@override
|
||||||
Future<BaseRequest> interceptRequest({required BaseRequest request}) async {
|
Future<BaseRequest> interceptRequest({required BaseRequest request}) async {
|
||||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
debugPrint('----------------------------------------------------');
|
||||||
print('📤 REQUEST: ${request.method} ${request.url}');
|
debugPrint('REQUEST: ${request.method} ${request.url}');
|
||||||
print('Headers: ${request.headers}');
|
debugPrint('Headers: ${request.headers}');
|
||||||
if (request is Request) {
|
if (request is Request) {
|
||||||
print('Body: ${request.body}');
|
debugPrint('Body: ${request.body}');
|
||||||
}
|
}
|
||||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
debugPrint('----------------------------------------------------');
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<BaseResponse> interceptResponse({required BaseResponse response}) async {
|
Future<BaseResponse> interceptResponse({required BaseResponse response}) async {
|
||||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
debugPrint('----------------------------------------------------');
|
||||||
print('📥 RESPONSE: ${response.statusCode} ${response.request?.url}');
|
debugPrint('RESPONSE: ${response.statusCode} ${response.request?.url}');
|
||||||
if (response is Response) {
|
if (response is Response) {
|
||||||
print('Body: ${response.body}');
|
debugPrint('Body: ${response.body}');
|
||||||
}
|
}
|
||||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
debugPrint('----------------------------------------------------');
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
68
pubspec.lock
68
pubspec.lock
@ -25,14 +25,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.11"
|
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:
|
animate_do:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -101,10 +93,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: "7b5b569f3df370590a85029148d6fc66c7d0201fc6f1847c07dd85d365ae9fcd"
|
sha256: b4d854962a32fd9f8efc0b76f98214790b833af8b2e9b2df6bfc927c0415a072
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.10.3"
|
version: "2.10.5"
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -209,22 +201,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
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:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -351,10 +327,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_riverpod
|
name: flutter_riverpod
|
||||||
sha256: "9e2d6907f12cc7d23a846847615941bddee8709bf2bfd274acdf5e80bcf22fde"
|
sha256: "38ec6c303e2c83ee84512f5fc2a82ae311531021938e63d7137eccc107bf3c02"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.1.0"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -457,18 +433,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: c92d18e1fe994cb06d48aa786c46b142a5633067e8297cff6b5a3ac742620104
|
sha256: eff94d2a6fc79fa8b811dde79c7549808c2346037ee107a1121b4a644c745f2a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "17.0.0"
|
version: "17.0.1"
|
||||||
google_navigation_flutter:
|
google_navigation_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: google_navigation_flutter
|
name: google_navigation_flutter
|
||||||
sha256: fdf79ddeda8bbba9d8b9218c41551b918447032004cbe72ea7365287c9d9bf80
|
sha256: "12f8bc6f5cf694b0778772a2e22969c9a2da44e76b496c47488deb482a2fe063"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.0"
|
version: "0.8.2"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -889,42 +865,42 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: riverpod
|
name: riverpod
|
||||||
sha256: c406de02bff19d920b832bddfb8283548bfa05ce41c59afba57ce643e116aa59
|
sha256: "16ff608d21e8ea64364f2b7c049c94a02ab81668f78845862b6e88b71dd4935a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.1.0"
|
||||||
riverpod_analyzer_utils:
|
riverpod_analyzer_utils:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: riverpod_analyzer_utils
|
name: riverpod_analyzer_utils
|
||||||
sha256: a0f68adb078b790faa3c655110a017f9a7b7b079a57bbd40f540e80dce5fcd29
|
sha256: "947b05d04c52a546a2ac6b19ef2a54b08520ff6bdf9f23d67957a4c8df1c3bc0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0-dev.7"
|
version: "1.0.0-dev.8"
|
||||||
riverpod_annotation:
|
riverpod_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: riverpod_annotation
|
name: riverpod_annotation
|
||||||
sha256: "7230014155777fc31ba3351bc2cb5a3b5717b11bfafe52b1553cb47d385f8897"
|
sha256: cc1474bc2df55ec3c1da1989d139dcef22cd5e2bd78da382e867a69a8eca2e46
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "4.0.0"
|
||||||
riverpod_generator:
|
riverpod_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: riverpod_generator
|
name: riverpod_generator
|
||||||
sha256: "49894543a42cf7a9954fc4e7366b6d3cb2e6ec0fa07775f660afcdd92d097702"
|
sha256: e43b1537229cc8f487f09b0c20d15dba840acbadcf5fc6dad7ad5e8ab75950dc
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "4.0.0+1"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.3"
|
version: "2.5.4"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1194,14 +1170,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.4"
|
||||||
uuid:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: uuid
|
|
||||||
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.5.2"
|
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
16
pubspec.yaml
16
pubspec.yaml
@ -13,8 +13,8 @@ dependencies:
|
|||||||
|
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
|
|
||||||
flutter_riverpod: ^3.0.3
|
flutter_riverpod: ^3.1.0
|
||||||
riverpod_annotation: ^3.0.3
|
riverpod_annotation: ^4.0.0
|
||||||
|
|
||||||
animate_do: ^4.2.0
|
animate_do: ^4.2.0
|
||||||
lottie: ^3.0.0
|
lottie: ^3.0.0
|
||||||
@ -37,10 +37,10 @@ dependencies:
|
|||||||
url_launcher: ^6.3.1
|
url_launcher: ^6.3.1
|
||||||
permission_handler: ^12.0.1
|
permission_handler: ^12.0.1
|
||||||
|
|
||||||
go_router: ^17.0.0
|
go_router: ^17.0.1
|
||||||
shared_preferences: ^2.5.3
|
shared_preferences: ^2.5.4
|
||||||
http_interceptor: ^2.0.0
|
http_interceptor: ^2.0.0
|
||||||
google_navigation_flutter: ^0.7.0
|
google_navigation_flutter: ^0.8.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -48,9 +48,9 @@ dev_dependencies:
|
|||||||
|
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
build_runner: ^2.4.14
|
build_runner: ^2.10.5
|
||||||
json_serializable: ^6.9.2
|
json_serializable: ^6.11.1
|
||||||
riverpod_generator: ^3.0.3
|
riverpod_generator: ^4.0.0
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user