Restructures navigation session initialization to occur after the view is created, eliminating race conditions. Session initialization now happens in onViewCreated callback with proper delay before setting destination. Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
287 lines
11 KiB
Dart
287 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../models/delivery.dart';
|
|
import '../theme/animation_system.dart';
|
|
import '../theme/color_system.dart';
|
|
|
|
class DeliveryListItem extends StatefulWidget {
|
|
final Delivery delivery;
|
|
final bool isSelected;
|
|
final VoidCallback onTap;
|
|
final VoidCallback? onCall;
|
|
final Function(String)? onAction;
|
|
final int? animationIndex;
|
|
|
|
const DeliveryListItem({
|
|
super.key,
|
|
required this.delivery,
|
|
required this.isSelected,
|
|
required this.onTap,
|
|
this.onCall,
|
|
this.onAction,
|
|
this.animationIndex,
|
|
});
|
|
|
|
@override
|
|
State<DeliveryListItem> createState() => _DeliveryListItemState();
|
|
}
|
|
|
|
class _DeliveryListItemState extends State<DeliveryListItem>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _slideAnimation;
|
|
late Animation<double> _fadeAnimation;
|
|
late Animation<double> _scaleAnimation;
|
|
bool _isHovered = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
duration: const Duration(milliseconds: 400),
|
|
vsync: this,
|
|
);
|
|
|
|
final staggerDelay = Duration(
|
|
milliseconds:
|
|
(widget.animationIndex ?? 0) * AppAnimations.staggerDelayMs,
|
|
);
|
|
|
|
Future.delayed(staggerDelay, () {
|
|
if (mounted) {
|
|
_controller.forward();
|
|
}
|
|
});
|
|
|
|
_slideAnimation = Tween<double>(begin: 20, end: 0).animate(
|
|
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
|
);
|
|
|
|
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
|
|
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
|
);
|
|
|
|
_scaleAnimation = Tween<double>(begin: 0.95, end: 1).animate(
|
|
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Color _getStatusColor(Delivery delivery) {
|
|
if (delivery.isSkipped == true) return SvrntyColors.statusCancelled;
|
|
if (delivery.delivered == true) return SvrntyColors.statusCompleted;
|
|
// Default: in-transit or pending deliveries
|
|
return SvrntyColors.statusInTransit;
|
|
}
|
|
|
|
IconData _getStatusIcon(Delivery delivery) {
|
|
if (delivery.isSkipped) return Icons.skip_next;
|
|
if (delivery.delivered) return Icons.check_circle;
|
|
return Icons.schedule;
|
|
}
|
|
|
|
String _getStatusLabel(Delivery delivery) {
|
|
if (delivery.isSkipped) return 'Skipped';
|
|
if (delivery.delivered) return 'Delivered';
|
|
return 'Pending';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
final statusColor = _getStatusColor(widget.delivery);
|
|
|
|
return ScaleTransition(
|
|
scale: _scaleAnimation,
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: Transform.translate(
|
|
offset: Offset(_slideAnimation.value, 0),
|
|
child: MouseRegion(
|
|
onEnter: (_) => setState(() => _isHovered = true),
|
|
onExit: (_) => setState(() => _isHovered = false),
|
|
child: GestureDetector(
|
|
onTap: widget.onTap,
|
|
child: AnimatedContainer(
|
|
duration: AppAnimations.durationFast,
|
|
margin: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 6,
|
|
),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(10),
|
|
color: widget.delivery.delivered
|
|
? Colors.green.withOpacity(0.15)
|
|
: (_isHovered || widget.isSelected
|
|
? Theme.of(context).colorScheme.surfaceContainer
|
|
: Colors.transparent),
|
|
boxShadow: (_isHovered || widget.isSelected) && !widget.delivery.delivered
|
|
? [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(
|
|
isDark ? 0.3 : 0.08,
|
|
),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
]
|
|
: [],
|
|
),
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
children: [
|
|
// Main delivery info row
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Left accent bar
|
|
Container(
|
|
width: 4,
|
|
height: 60,
|
|
decoration: BoxDecoration(
|
|
color: statusColor,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
// Delivery info
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Name
|
|
Text(
|
|
widget.delivery.name,
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.titleSmall
|
|
?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 4),
|
|
// Address
|
|
Text(
|
|
widget.delivery.deliveryAddress
|
|
?.formattedAddress ??
|
|
'No address',
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.bodySmall,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 4),
|
|
// Order count
|
|
Text(
|
|
'${widget.delivery.orders.length} order${widget.delivery.orders.length != 1 ? 's' : ''}',
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.labelSmall
|
|
?.copyWith(
|
|
fontSize: 10,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// Status badge + Call button
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
// Status badge
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: statusColor,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
_getStatusIcon(widget.delivery),
|
|
color: Colors.white,
|
|
size: 12,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
_getStatusLabel(widget.delivery),
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.labelSmall
|
|
?.copyWith(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 10,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
// Call button (if contact exists)
|
|
if (widget.delivery.orders.isNotEmpty &&
|
|
widget.delivery.orders.first.contact != null)
|
|
GestureDetector(
|
|
onTap: widget.onCall,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Icon(
|
|
Icons.phone,
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
size: 14,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
// Total amount (if present)
|
|
if (widget.delivery.orders.isNotEmpty &&
|
|
widget.delivery.orders.first.totalAmount != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8, left: 16),
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
'Total: \$${widget.delivery.orders.first.totalAmount!.toStringAsFixed(2)}',
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.labelSmall
|
|
?.copyWith(
|
|
color: SvrntyColors.warning,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|