ionic-planb-logistic-app-fl.../lib/components/delivery_list_item.dart
Jean-Philippe Brule 9cb5b51f6d Fix Google Navigation initialization timing issues
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>
2025-11-15 20:49:20 -05:00

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,
),
),
),
),
],
),
),
),
),
),
),
);
}
}