ionic-planb-logistic-app-fl.../lib/components/glassmorphic_route_card.dart
Jean-Philippe Brule 5714fd8443 Implement UI/UX enhancements with collapsible routes sidebar and glassmorphic route cards
Adds new components (CollapsibleRoutesSidebar, GlassmorphicRouteCard) and
internationalization support. Updates deliveries and routes pages with improved
navigation and visual presentation using Material Design 3 principles.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-15 17:55:18 -05:00

331 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import '../models/delivery_route.dart';
import '../theme/spacing_system.dart';
import '../theme/size_system.dart';
import '../theme/animation_system.dart';
import '../theme/color_system.dart';
/// Modern glassmorphic route card with status-based gradient and animated progress
class GlassmorphicRouteCard extends StatefulWidget {
final DeliveryRoute route;
final bool isSelected;
final VoidCallback onTap;
final bool isCollapsed;
const GlassmorphicRouteCard({
super.key,
required this.route,
required this.isSelected,
required this.onTap,
this.isCollapsed = false,
});
@override
State<GlassmorphicRouteCard> createState() => _GlassmorphicRouteCardState();
}
class _GlassmorphicRouteCardState extends State<GlassmorphicRouteCard>
with SingleTickerProviderStateMixin {
late AnimationController _hoverController;
bool _isHovered = false;
@override
void initState() {
super.initState();
_hoverController = AnimationController(
duration: Duration(milliseconds: AppAnimations.durationFast.inMilliseconds),
vsync: this,
);
}
@override
void dispose() {
_hoverController.dispose();
super.dispose();
}
/// Calculate color based on completion percentage
Color _getProgressColor(double progress) {
if (progress < 0.3) {
// Red to orange (0-30%)
return Color.lerp(
SvrntyColors.crimsonRed,
const Color(0xFFFF9800),
(progress / 0.3),
)!;
} else if (progress < 0.7) {
// Orange to yellow (30-70%)
return Color.lerp(
const Color(0xFFFF9800),
const Color(0xFFFFC107),
((progress - 0.3) / 0.4),
)!;
} else {
// Yellow to green (70-100%)
return Color.lerp(
const Color(0xFFFFC107),
const Color(0xFF4CAF50),
((progress - 0.7) / 0.3),
)!;
}
}
void _setHovered(bool hovered) {
setState(() {
_isHovered = hovered;
});
if (hovered) {
_hoverController.forward();
} else {
_hoverController.reverse();
}
}
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final progress = widget.route.deliveredCount / widget.route.deliveriesCount;
final progressColor = _getProgressColor(progress);
if (widget.isCollapsed) {
return _buildCollapsedCard(context, isDarkMode, progress, progressColor);
}
return _buildExpandedCard(context, isDarkMode, progress, progressColor);
}
Widget _buildCollapsedCard(BuildContext context, bool isDarkMode,
double progress, Color progressColor) {
return MouseRegion(
onEnter: (_) => _setHovered(true),
onExit: (_) => _setHovered(false),
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedContainer(
duration: Duration(
milliseconds: AppAnimations.durationFast.inMilliseconds,
),
height: AppSizes.buttonHeightMd,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppSpacing.md),
),
child: Stack(
alignment: Alignment.center,
children: [
// Glassmorphic background
ClipRRect(
borderRadius: BorderRadius.circular(AppSpacing.md),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 8, sigmaY: 8),
child: Container(
decoration: BoxDecoration(
color: (isDarkMode
? SvrntyColors.darkSlate
: Colors.white)
.withValues(alpha: 0.7),
border: Border.all(
color: (isDarkMode ? Colors.white : Colors.white)
.withValues(alpha: 0.2),
width: 1.5,
),
),
),
),
),
// Progress indicator at bottom
Positioned(
bottom: 0,
left: 0,
right: 0,
child: ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(AppSpacing.md - AppSpacing.xs),
bottomRight: Radius.circular(AppSpacing.md - AppSpacing.xs),
),
child: LinearProgressIndicator(
value: progress,
minHeight: 3,
backgroundColor: progressColor.withValues(alpha: 0.2),
valueColor: AlwaysStoppedAnimation<Color>(progressColor),
),
),
),
// Content
Center(
child: Text(
widget.route.name.substring(0, 1).toUpperCase(),
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 16,
color: widget.isSelected ? progressColor : null,
),
),
),
],
),
),
),
);
}
Widget _buildExpandedCard(BuildContext context, bool isDarkMode,
double progress, Color progressColor) {
return MouseRegion(
onEnter: (_) => _setHovered(true),
onExit: (_) => _setHovered(false),
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedBuilder(
animation: _hoverController,
builder: (context, child) {
final hoverValue = _hoverController.value;
final blurSigma = 8 + (hoverValue * 3);
final bgOpacity = 0.7 + (hoverValue * 0.1);
return AnimatedContainer(
duration: Duration(
milliseconds: AppAnimations.durationFast.inMilliseconds,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppSpacing.lg),
boxShadow: [
BoxShadow(
color: progressColor.withValues(alpha: 0.1 + (hoverValue * 0.2)),
blurRadius: 12 + (hoverValue * 8),
offset: Offset(0, 2 + (hoverValue * 2)),
),
],
),
child: Stack(
children: [
// Glassmorphic background
ClipRRect(
borderRadius: BorderRadius.circular(AppSpacing.lg),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: blurSigma, sigmaY: blurSigma),
child: Container(
decoration: BoxDecoration(
color: (isDarkMode
? SvrntyColors.darkSlate
: Colors.white)
.withValues(alpha: bgOpacity),
border: Border.all(
color: (isDarkMode ? Colors.white : Colors.white)
.withValues(alpha: 0.2 + (hoverValue * 0.15)),
width: 1.5,
),
borderRadius: BorderRadius.circular(AppSpacing.lg),
),
),
),
),
// Content
Padding(
padding: EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Route name
Text(
widget.route.name,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: widget.isSelected ? progressColor : null,
fontWeight: FontWeight.bold,
fontSize: 14,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: AppSpacing.xs),
// Delivery count
RichText(
text: TextSpan(
children: [
TextSpan(
text: '${widget.route.deliveredCount}',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: progressColor,
fontWeight: FontWeight.w600,
fontSize: 11,
),
),
TextSpan(
text: '/${widget.route.deliveriesCount}',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
fontSize: 11,
color: (Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black)
.withValues(alpha: 0.6),
),
),
],
),
),
SizedBox(height: AppSpacing.sm),
// Animated gradient progress bar
ClipRRect(
borderRadius: BorderRadius.circular(3),
child: Stack(
children: [
// Background
Container(
height: 6,
decoration: BoxDecoration(
color: progressColor
.withValues(alpha: 0.15),
),
),
// Progress fill with gradient
ClipRRect(
borderRadius: BorderRadius.circular(3),
child: Container(
height: 6,
width: 100 * progress,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
SvrntyColors.crimsonRed,
progressColor,
],
),
borderRadius: BorderRadius.circular(3),
boxShadow: [
BoxShadow(
color: progressColor
.withValues(alpha: 0.4),
blurRadius: 4,
),
],
),
),
),
],
),
),
],
),
),
],
),
);
},
),
),
);
}
}