ionic-planb-logistic-app-fl.../lib/components/collapsible_routes_sidebar.dart
Jean-Philippe Brule 65f0f4451b Implement collapsible sidebar with badge-only view
Add collapsible sidebar functionality for both deliveries and routes pages:

- DeliveryListItem: Add isCollapsed parameter to show badge-only view when sidebar is collapsed
- RouteListItem: Add isCollapsed parameter with same badge-only behavior
- MapSidebarLayout: Add sidebarBuilder function to pass collapsed state to child widgets
- CollapsibleRoutesSidebar: Pass collapsed state to RouteListItem components
- UnifiedDeliveryListView: Add isCollapsed parameter and pass to DeliveryListItem

Collapsed sidebar:
- Width: 80px (accommodates 60px badge with 10px margins)
- Shows only status-colored order number badges
- Badges remain centered and aligned during animations
- Removed horizontal slide animation in collapsed view to prevent misalignment
- Maintains scale and fade animations for smooth entrance

Expanded sidebar:
- Width: 420px (original full layout)
- Shows badge, vertical accent bar, and delivery/route details
- Full animations including horizontal slide

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

192 lines
5.8 KiB
Dart

import 'package:flutter/material.dart';
import '../models/delivery_route.dart';
import '../theme/spacing_system.dart';
import '../theme/size_system.dart';
import '../theme/animation_system.dart';
import '../theme/color_system.dart';
import '../utils/breakpoints.dart';
import 'route_list_item.dart';
class CollapsibleRoutesSidebar extends StatefulWidget {
final List<DeliveryRoute> routes;
final DeliveryRoute? selectedRoute;
final ValueChanged<DeliveryRoute> onRouteSelected;
const CollapsibleRoutesSidebar({
super.key,
required this.routes,
this.selectedRoute,
required this.onRouteSelected,
});
@override
State<CollapsibleRoutesSidebar> createState() =>
_CollapsibleRoutesSidebarState();
}
class _CollapsibleRoutesSidebarState extends State<CollapsibleRoutesSidebar>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
bool _isExpanded = true;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
if (_isExpanded) {
_animationController.forward();
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _toggleSidebar() {
setState(() {
_isExpanded = !_isExpanded;
});
if (_isExpanded) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
@override
Widget build(BuildContext context) {
final isMobile = context.isMobile;
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
// On mobile, always show as collapsible
if (isMobile) {
return Container(
color: isDarkMode ? SvrntyColors.almostBlack : Colors.white,
child: Column(
children: [
// Header with toggle button
Container(
padding: EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isDarkMode ? SvrntyColors.darkSlate : SvrntyColors.slateGray.withValues(alpha: 0.2),
width: 1,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (_isExpanded)
Text(
'Routes',
style: Theme.of(context).textTheme.titleMedium,
),
IconButton(
icon: AnimatedRotation(
turns: _isExpanded ? 0 : -0.25,
duration: Duration(
milliseconds: AppAnimations.durationFast.inMilliseconds,
),
child: const Icon(Icons.chevron_right),
),
onPressed: _toggleSidebar,
iconSize: AppSizes.iconMd,
),
],
),
),
// Collapsible content
if (_isExpanded)
Expanded(
child: _buildRoutesList(context),
),
],
),
);
}
// On tablet/desktop, show full sidebar with toggle (expanded: 420px, collapsed: 80px for badge)
return Container(
width: _isExpanded ? 420 : 80,
color: isDarkMode ? SvrntyColors.almostBlack : Colors.white,
child: Column(
children: [
// Header with toggle button
Container(
height: kToolbarHeight,
padding: EdgeInsets.symmetric(horizontal: AppSpacing.xs),
decoration: BoxDecoration(
border: Border(
left: BorderSide(
color: isDarkMode ? SvrntyColors.darkSlate : SvrntyColors.slateGray.withValues(alpha: 0.2),
width: 1,
),
),
),
child: Row(
mainAxisAlignment: _isExpanded ? MainAxisAlignment.spaceBetween : MainAxisAlignment.center,
children: [
if (_isExpanded)
Expanded(
child: Text(
'Routes',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: AppSizes.buttonHeightMd,
height: AppSizes.buttonHeightMd,
child: IconButton(
icon: AnimatedRotation(
turns: _isExpanded ? 0 : -0.5,
duration: Duration(
milliseconds: AppAnimations.durationFast.inMilliseconds,
),
child: const Icon(Icons.chevron_right),
),
onPressed: _toggleSidebar,
iconSize: AppSizes.iconMd,
),
),
],
),
),
// Routes list
Expanded(
child: _buildRoutesList(context),
),
],
),
);
}
Widget _buildRoutesList(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.only(top: 4, bottom: 8),
physics: const AlwaysScrollableScrollPhysics(),
itemCount: widget.routes.length,
itemBuilder: (context, index) {
final route = widget.routes[index];
final isSelected = widget.selectedRoute?.id == route.id;
return RouteListItem(
route: route,
isSelected: isSelected,
onTap: () => widget.onRouteSelected(route),
animationIndex: index,
isCollapsed: !_isExpanded,
);
},
);
}
}