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>
This commit is contained in:
parent
6e6d279d77
commit
5714fd8443
200
lib/components/collapsible_routes_sidebar.dart
Normal file
200
lib/components/collapsible_routes_sidebar.dart
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
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 'glassmorphic_route_card.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
|
||||||
|
return Container(
|
||||||
|
width: _isExpanded ? 280 : 64,
|
||||||
|
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: EdgeInsets.all(AppSpacing.sm),
|
||||||
|
itemCount: widget.routes.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final route = widget.routes[index];
|
||||||
|
final isSelected = widget.selectedRoute?.id == route.id;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: AppSpacing.sm),
|
||||||
|
child: _buildRouteButton(context, route, isSelected),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRouteButton(
|
||||||
|
BuildContext context,
|
||||||
|
DeliveryRoute route,
|
||||||
|
bool isSelected,
|
||||||
|
) {
|
||||||
|
return GlassmorphicRouteCard(
|
||||||
|
route: route,
|
||||||
|
isSelected: isSelected,
|
||||||
|
isCollapsed: !_isExpanded,
|
||||||
|
onTap: () => widget.onRouteSelected(route),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
330
lib/components/glassmorphic_route_card.dart
Normal file
330
lib/components/glassmorphic_route_card.dart
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
368
lib/l10n/app_localizations.dart
Normal file
368
lib/l10n/app_localizations.dart
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:intl/intl.dart' as intl;
|
||||||
|
|
||||||
|
import 'app_localizations_en.dart';
|
||||||
|
import 'app_localizations_fr.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
|
||||||
|
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||||
|
/// returned by `AppLocalizations.of(context)`.
|
||||||
|
///
|
||||||
|
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||||
|
/// `localizationDelegates` list, and the locales they support in the app's
|
||||||
|
/// `supportedLocales` list. For example:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// import 'l10n/app_localizations.dart';
|
||||||
|
///
|
||||||
|
/// return MaterialApp(
|
||||||
|
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
/// home: MyApplicationHome(),
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Update pubspec.yaml
|
||||||
|
///
|
||||||
|
/// Please make sure to update your pubspec.yaml to include the following
|
||||||
|
/// packages:
|
||||||
|
///
|
||||||
|
/// ```yaml
|
||||||
|
/// dependencies:
|
||||||
|
/// # Internationalization support.
|
||||||
|
/// flutter_localizations:
|
||||||
|
/// sdk: flutter
|
||||||
|
/// intl: any # Use the pinned version from flutter_localizations
|
||||||
|
///
|
||||||
|
/// # Rest of dependencies
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## iOS Applications
|
||||||
|
///
|
||||||
|
/// iOS applications define key application metadata, including supported
|
||||||
|
/// locales, in an Info.plist file that is built into the application bundle.
|
||||||
|
/// To configure the locales supported by your app, you’ll need to edit this
|
||||||
|
/// file.
|
||||||
|
///
|
||||||
|
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||||
|
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||||
|
/// project’s Runner folder.
|
||||||
|
///
|
||||||
|
/// Next, select the Information Property List item, select Add Item from the
|
||||||
|
/// Editor menu, then select Localizations from the pop-up menu.
|
||||||
|
///
|
||||||
|
/// Select and expand the newly-created Localizations item then, for each
|
||||||
|
/// locale your application supports, add a new item and select the locale
|
||||||
|
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||||
|
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||||
|
/// property.
|
||||||
|
abstract class AppLocalizations {
|
||||||
|
AppLocalizations(String locale)
|
||||||
|
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||||
|
|
||||||
|
final String localeName;
|
||||||
|
|
||||||
|
static AppLocalizations of(BuildContext context) {
|
||||||
|
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const LocalizationsDelegate<AppLocalizations> delegate =
|
||||||
|
_AppLocalizationsDelegate();
|
||||||
|
|
||||||
|
/// A list of this localizations delegate along with the default localizations
|
||||||
|
/// delegates.
|
||||||
|
///
|
||||||
|
/// Returns a list of localizations delegates containing this delegate along with
|
||||||
|
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||||
|
/// and GlobalWidgetsLocalizations.delegate.
|
||||||
|
///
|
||||||
|
/// Additional delegates can be added by appending to this list in
|
||||||
|
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||||
|
/// of delegates is preferred or required.
|
||||||
|
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||||
|
<LocalizationsDelegate<dynamic>>[
|
||||||
|
delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A list of this localizations delegate's supported locales.
|
||||||
|
static const List<Locale> supportedLocales = <Locale>[
|
||||||
|
Locale('en'),
|
||||||
|
Locale('fr'),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// No description provided for @appTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Plan B Logistics'**
|
||||||
|
String get appTitle;
|
||||||
|
|
||||||
|
/// No description provided for @appDescription.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Delivery Management System'**
|
||||||
|
String get appDescription;
|
||||||
|
|
||||||
|
/// No description provided for @loginWithKeycloak.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Login with Keycloak'**
|
||||||
|
String get loginWithKeycloak;
|
||||||
|
|
||||||
|
/// No description provided for @deliveryRoutes.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Delivery Routes'**
|
||||||
|
String get deliveryRoutes;
|
||||||
|
|
||||||
|
/// No description provided for @routes.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Routes'**
|
||||||
|
String get routes;
|
||||||
|
|
||||||
|
/// No description provided for @deliveries.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Deliveries'**
|
||||||
|
String get deliveries;
|
||||||
|
|
||||||
|
/// No description provided for @settings.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Settings'**
|
||||||
|
String get settings;
|
||||||
|
|
||||||
|
/// No description provided for @profile.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Profile'**
|
||||||
|
String get profile;
|
||||||
|
|
||||||
|
/// No description provided for @logout.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Logout'**
|
||||||
|
String get logout;
|
||||||
|
|
||||||
|
/// No description provided for @completed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Completed'**
|
||||||
|
String get completed;
|
||||||
|
|
||||||
|
/// No description provided for @pending.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Pending'**
|
||||||
|
String get pending;
|
||||||
|
|
||||||
|
/// No description provided for @todo.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'To Do'**
|
||||||
|
String get todo;
|
||||||
|
|
||||||
|
/// No description provided for @delivered.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Delivered'**
|
||||||
|
String get delivered;
|
||||||
|
|
||||||
|
/// No description provided for @newCustomer.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'New Customer'**
|
||||||
|
String get newCustomer;
|
||||||
|
|
||||||
|
/// No description provided for @items.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{count} items'**
|
||||||
|
String items(int count);
|
||||||
|
|
||||||
|
/// No description provided for @moneyCurrency.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{amount} MAD'**
|
||||||
|
String moneyCurrency(double amount);
|
||||||
|
|
||||||
|
/// No description provided for @call.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Call'**
|
||||||
|
String get call;
|
||||||
|
|
||||||
|
/// No description provided for @map.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Map'**
|
||||||
|
String get map;
|
||||||
|
|
||||||
|
/// No description provided for @more.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'More'**
|
||||||
|
String get more;
|
||||||
|
|
||||||
|
/// No description provided for @markAsCompleted.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Mark as Completed'**
|
||||||
|
String get markAsCompleted;
|
||||||
|
|
||||||
|
/// No description provided for @markAsUncompleted.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Mark as Uncompleted'**
|
||||||
|
String get markAsUncompleted;
|
||||||
|
|
||||||
|
/// No description provided for @uploadPhoto.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Upload Photo'**
|
||||||
|
String get uploadPhoto;
|
||||||
|
|
||||||
|
/// No description provided for @viewDetails.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'View Details'**
|
||||||
|
String get viewDetails;
|
||||||
|
|
||||||
|
/// No description provided for @deliverySuccessful.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Delivery marked as completed'**
|
||||||
|
String get deliverySuccessful;
|
||||||
|
|
||||||
|
/// No description provided for @deliveryFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to mark delivery'**
|
||||||
|
String get deliveryFailed;
|
||||||
|
|
||||||
|
/// No description provided for @noDeliveries.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No deliveries'**
|
||||||
|
String get noDeliveries;
|
||||||
|
|
||||||
|
/// No description provided for @noRoutes.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No routes available'**
|
||||||
|
String get noRoutes;
|
||||||
|
|
||||||
|
/// No description provided for @error.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Error: {message}'**
|
||||||
|
String error(String message);
|
||||||
|
|
||||||
|
/// No description provided for @retry.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Retry'**
|
||||||
|
String get retry;
|
||||||
|
|
||||||
|
/// No description provided for @authenticationRequired.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Authentication required'**
|
||||||
|
String get authenticationRequired;
|
||||||
|
|
||||||
|
/// No description provided for @phoneCall.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Call customer'**
|
||||||
|
String get phoneCall;
|
||||||
|
|
||||||
|
/// No description provided for @navigateToAddress.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show on map'**
|
||||||
|
String get navigateToAddress;
|
||||||
|
|
||||||
|
/// No description provided for @language.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Language'**
|
||||||
|
String get language;
|
||||||
|
|
||||||
|
/// No description provided for @english.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'English'**
|
||||||
|
String get english;
|
||||||
|
|
||||||
|
/// No description provided for @french.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'French'**
|
||||||
|
String get french;
|
||||||
|
|
||||||
|
/// No description provided for @appVersion.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'App Version'**
|
||||||
|
String get appVersion;
|
||||||
|
|
||||||
|
/// No description provided for @about.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'About'**
|
||||||
|
String get about;
|
||||||
|
|
||||||
|
/// No description provided for @fullName.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{firstName} {lastName}'**
|
||||||
|
String fullName(String firstName, String lastName);
|
||||||
|
|
||||||
|
/// No description provided for @completedDeliveries.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{completed}/{total} completed'**
|
||||||
|
String completedDeliveries(int completed, int total);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppLocalizationsDelegate
|
||||||
|
extends LocalizationsDelegate<AppLocalizations> {
|
||||||
|
const _AppLocalizationsDelegate();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AppLocalizations> load(Locale locale) {
|
||||||
|
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isSupported(Locale locale) =>
|
||||||
|
<String>['en', 'fr'].contains(locale.languageCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||||
|
// Lookup logic when only language code is specified.
|
||||||
|
switch (locale.languageCode) {
|
||||||
|
case 'en':
|
||||||
|
return AppLocalizationsEn();
|
||||||
|
case 'fr':
|
||||||
|
return AppLocalizationsFr();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw FlutterError(
|
||||||
|
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||||
|
'an issue with the localizations generation tool. Please file an issue '
|
||||||
|
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||||
|
'that was used.',
|
||||||
|
);
|
||||||
|
}
|
||||||
137
lib/l10n/app_localizations_en.dart
Normal file
137
lib/l10n/app_localizations_en.dart
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// ignore: unused_import
|
||||||
|
import 'package:intl/intl.dart' as intl;
|
||||||
|
import 'app_localizations.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
|
||||||
|
/// The translations for English (`en`).
|
||||||
|
class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appTitle => 'Plan B Logistics';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appDescription => 'Delivery Management System';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loginWithKeycloak => 'Login with Keycloak';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deliveryRoutes => 'Delivery Routes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get routes => 'Routes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deliveries => 'Deliveries';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings => 'Settings';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profile => 'Profile';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logout => 'Logout';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get completed => 'Completed';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pending => 'Pending';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get todo => 'To Do';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get delivered => 'Delivered';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get newCustomer => 'New Customer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String items(int count) {
|
||||||
|
return '$count items';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String moneyCurrency(double amount) {
|
||||||
|
return '$amount MAD';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get call => 'Call';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map => 'Map';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get more => 'More';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get markAsCompleted => 'Mark as Completed';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get markAsUncompleted => 'Mark as Uncompleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uploadPhoto => 'Upload Photo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => 'View Details';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deliverySuccessful => 'Delivery marked as completed';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deliveryFailed => 'Failed to mark delivery';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noDeliveries => 'No deliveries';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRoutes => 'No routes available';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String error(String message) {
|
||||||
|
return 'Error: $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get retry => 'Retry';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get authenticationRequired => 'Authentication required';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get phoneCall => 'Call customer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get navigateToAddress => 'Show on map';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get language => 'Language';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get english => 'English';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get french => 'French';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appVersion => 'App Version';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get about => 'About';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String fullName(String firstName, String lastName) {
|
||||||
|
return '$firstName $lastName';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String completedDeliveries(int completed, int total) {
|
||||||
|
return '$completed/$total completed';
|
||||||
|
}
|
||||||
|
}
|
||||||
137
lib/l10n/app_localizations_fr.dart
Normal file
137
lib/l10n/app_localizations_fr.dart
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// ignore: unused_import
|
||||||
|
import 'package:intl/intl.dart' as intl;
|
||||||
|
import 'app_localizations.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
|
||||||
|
/// The translations for French (`fr`).
|
||||||
|
class AppLocalizationsFr extends AppLocalizations {
|
||||||
|
AppLocalizationsFr([String locale = 'fr']) : super(locale);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appTitle => 'Plan B Logistique';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appDescription => 'Systme de Gestion des Livraisons';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loginWithKeycloak => 'Connexion avec Keycloak';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deliveryRoutes => 'Itinraires de Livraison';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get routes => 'Itinraires';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deliveries => 'Livraisons';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings => 'Paramtres';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profile => 'Profil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logout => 'Dconnexion';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get completed => 'Livr';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pending => 'En attente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get todo => 'livrer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get delivered => 'Livr';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get newCustomer => 'Nouveau Client';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String items(int count) {
|
||||||
|
return '$count articles';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String moneyCurrency(double amount) {
|
||||||
|
return '$amount MAD';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get call => 'Appeler';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map => 'Carte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get more => 'Plus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get markAsCompleted => 'Marquer comme livr';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get markAsUncompleted => 'Marquer comme livrer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uploadPhoto => 'Tlcharger une photo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => 'Voir les dtails';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deliverySuccessful => 'Livraison marque comme complte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deliveryFailed => 'chec du marquage de la livraison';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noDeliveries => 'Aucune livraison';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRoutes => 'Aucun itinraire disponible';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String error(String message) {
|
||||||
|
return 'Erreur: $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get retry => 'Ressayer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get authenticationRequired => 'Authentification requise';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get phoneCall => 'Appeler le client';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get navigateToAddress => 'Afficher sur la carte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get language => 'Langue';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get english => 'English';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get french => 'Franais';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appVersion => 'Version de l\'application';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get about => ' propos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String fullName(String firstName, String lastName) {
|
||||||
|
return '$firstName $lastName';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String completedDeliveries(int completed, int total) {
|
||||||
|
return '$completed/$total livrs';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,15 +2,17 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../models/delivery.dart';
|
import '../models/delivery.dart';
|
||||||
|
import '../models/delivery_route.dart';
|
||||||
import '../providers/providers.dart';
|
import '../providers/providers.dart';
|
||||||
import '../api/client.dart';
|
import '../api/client.dart';
|
||||||
import '../api/openapi_config.dart';
|
import '../api/openapi_config.dart';
|
||||||
import '../models/delivery_commands.dart';
|
import '../models/delivery_commands.dart';
|
||||||
import '../utils/breakpoints.dart';
|
import '../utils/breakpoints.dart';
|
||||||
import '../utils/responsive.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/delivery_list_item.dart';
|
||||||
|
import '../components/collapsible_routes_sidebar.dart'
|
||||||
|
show CollapsibleRoutesSidebar;
|
||||||
|
|
||||||
class DeliveriesPage extends ConsumerStatefulWidget {
|
class DeliveriesPage extends ConsumerStatefulWidget {
|
||||||
final int routeFragmentId;
|
final int routeFragmentId;
|
||||||
@ -46,6 +48,7 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
|
final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId));
|
||||||
|
final routesData = ref.watch(deliveryRoutesProvider);
|
||||||
final token = ref.watch(authTokenProvider).valueOrNull;
|
final token = ref.watch(authTokenProvider).valueOrNull;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -62,7 +65,38 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
.where((d) => d.delivered)
|
.where((d) => d.delivered)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return MapSidebarLayout(
|
return routesData.when(
|
||||||
|
data: (routes) {
|
||||||
|
DeliveryRoute? currentRoute;
|
||||||
|
try {
|
||||||
|
currentRoute = routes.firstWhere(
|
||||||
|
(r) => r.id == widget.routeFragmentId,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
currentRoute = routes.isNotEmpty ? routes.first : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (context.isDesktop && routes.isNotEmpty)
|
||||||
|
CollapsibleRoutesSidebar(
|
||||||
|
routes: routes,
|
||||||
|
selectedRoute: currentRoute,
|
||||||
|
onRouteSelected: (route) {
|
||||||
|
if (route.id != widget.routeFragmentId) {
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => DeliveriesPage(
|
||||||
|
routeFragmentId: route.id,
|
||||||
|
routeName: route.name,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: MapSidebarLayout(
|
||||||
mapWidget: DarkModeMapComponent(
|
mapWidget: DarkModeMapComponent(
|
||||||
deliveries: deliveries,
|
deliveries: deliveries,
|
||||||
selectedDelivery: _selectedDelivery,
|
selectedDelivery: _selectedDelivery,
|
||||||
@ -136,6 +170,89 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
error: (error, stackTrace) => MapSidebarLayout(
|
||||||
|
mapWidget: DarkModeMapComponent(
|
||||||
|
deliveries: deliveries,
|
||||||
|
selectedDelivery: _selectedDelivery,
|
||||||
|
onDeliverySelected: (delivery) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDelivery = delivery;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
sidebarWidget: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: SegmentedButton<int>(
|
||||||
|
segments: const [
|
||||||
|
ButtonSegment(
|
||||||
|
value: 0,
|
||||||
|
label: Text('To Do'),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: 1,
|
||||||
|
label: Text('Delivered'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: <int>{_currentSegment},
|
||||||
|
onSelectionChanged: (Set<int> newSelection) {
|
||||||
|
setState(() {
|
||||||
|
_currentSegment = newSelection.first;
|
||||||
|
_pageController.animateToPage(
|
||||||
|
_currentSegment,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: PageView(
|
||||||
|
controller: _pageController,
|
||||||
|
onPageChanged: (index) {
|
||||||
|
setState(() {
|
||||||
|
_currentSegment = index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
DeliveryListView(
|
||||||
|
deliveries: todoDeliveries,
|
||||||
|
selectedDelivery: _selectedDelivery,
|
||||||
|
onDeliverySelected: (delivery) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDelivery = delivery;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onAction: (delivery, action) =>
|
||||||
|
_handleDeliveryAction(context, delivery, action, token),
|
||||||
|
),
|
||||||
|
DeliveryListView(
|
||||||
|
deliveries: completedDeliveries,
|
||||||
|
selectedDelivery: _selectedDelivery,
|
||||||
|
onDeliverySelected: (delivery) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDelivery = delivery;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onAction: (delivery, action) =>
|
||||||
|
_handleDeliveryAction(context, delivery, action, token),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
|
|||||||
@ -3,17 +3,29 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../models/delivery_route.dart';
|
import '../models/delivery_route.dart';
|
||||||
import '../providers/providers.dart';
|
import '../providers/providers.dart';
|
||||||
import '../utils/breakpoints.dart';
|
import '../utils/breakpoints.dart';
|
||||||
import '../utils/responsive.dart';
|
import '../components/collapsible_routes_sidebar.dart';
|
||||||
import '../components/premium_route_card.dart';
|
import '../components/dark_mode_map.dart';
|
||||||
import 'deliveries_page.dart';
|
import 'deliveries_page.dart';
|
||||||
import 'settings_page.dart';
|
import 'settings_page.dart';
|
||||||
|
|
||||||
class RoutesPage extends ConsumerWidget {
|
class RoutesPage extends ConsumerWidget {
|
||||||
const RoutesPage({super.key});
|
const RoutesPage({super.key});
|
||||||
|
|
||||||
|
void _navigateToDeliveries(BuildContext context, DeliveryRoute route) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => DeliveriesPage(
|
||||||
|
routeFragmentId: route.id,
|
||||||
|
routeName: route.name,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final routesData = ref.watch(deliveryRoutesProvider);
|
final routesData = ref.watch(deliveryRoutesProvider);
|
||||||
|
final allDeliveriesData = ref.watch(allDeliveriesProvider);
|
||||||
final userProfile = ref.watch(userProfileProvider);
|
final userProfile = ref.watch(userProfileProvider);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -73,14 +85,70 @@ class RoutesPage extends ConsumerWidget {
|
|||||||
child: Text('No routes available'),
|
child: Text('No routes available'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return allDeliveriesData.when(
|
||||||
|
data: (allDeliveries) {
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
// ignore: unused_result
|
// ignore: unused_result
|
||||||
ref.refresh(deliveryRoutesProvider);
|
ref.refresh(deliveryRoutesProvider);
|
||||||
|
// ignore: unused_result
|
||||||
|
ref.refresh(allDeliveriesProvider);
|
||||||
},
|
},
|
||||||
child: context.isDesktop
|
child: context.isDesktop
|
||||||
? _buildDesktopGrid(context, routes)
|
? Row(
|
||||||
: _buildMobileList(context, routes),
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: DarkModeMapComponent(
|
||||||
|
deliveries: allDeliveries,
|
||||||
|
selectedDelivery: null,
|
||||||
|
onDeliverySelected: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CollapsibleRoutesSidebar(
|
||||||
|
routes: routes,
|
||||||
|
selectedRoute: null,
|
||||||
|
onRouteSelected: (route) {
|
||||||
|
_navigateToDeliveries(context, route);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: DarkModeMapComponent(
|
||||||
|
deliveries: allDeliveries,
|
||||||
|
selectedDelivery: null,
|
||||||
|
onDeliverySelected: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CollapsibleRoutesSidebar(
|
||||||
|
routes: routes,
|
||||||
|
selectedRoute: null,
|
||||||
|
onRouteSelected: (route) {
|
||||||
|
_navigateToDeliveries(context, route);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
error: (error, stackTrace) => Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text('Error loading deliveries: $error'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => ref.refresh(allDeliveriesProvider),
|
||||||
|
child: const Text('Retry'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
@ -103,53 +171,4 @@ class RoutesPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMobileList(BuildContext context, List<DeliveryRoute> routes) {
|
|
||||||
final spacing = ResponsiveSpacing.md(context);
|
|
||||||
return ListView.builder(
|
|
||||||
padding: EdgeInsets.all(ResponsiveSpacing.md(context)),
|
|
||||||
itemCount: routes.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final route = routes[index];
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: spacing),
|
|
||||||
child: _buildRouteCard(context, route),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDesktopGrid(BuildContext context, List<DeliveryRoute> routes) {
|
|
||||||
final spacing = ResponsiveSpacing.lg(context);
|
|
||||||
final columns = context.isTablet ? 2 : 3;
|
|
||||||
return GridView.builder(
|
|
||||||
padding: EdgeInsets.all(spacing),
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: columns,
|
|
||||||
crossAxisSpacing: spacing,
|
|
||||||
mainAxisSpacing: spacing,
|
|
||||||
childAspectRatio: 1.2,
|
|
||||||
),
|
|
||||||
itemCount: routes.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final route = routes[index];
|
|
||||||
return _buildRouteCard(context, route);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRouteCard(BuildContext context, DeliveryRoute route) {
|
|
||||||
return PremiumRouteCard(
|
|
||||||
route: route,
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => DeliveriesPage(
|
|
||||||
routeFragmentId: route.id,
|
|
||||||
routeName: route.name,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,6 +100,32 @@ final deliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, rout
|
|||||||
return result.whenSuccess((deliveries) => deliveries) ?? [];
|
return result.whenSuccess((deliveries) => deliveries) ?? [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Provider to get all deliveries from all routes
|
||||||
|
final allDeliveriesProvider = FutureProvider<List<Delivery>>((ref) async {
|
||||||
|
final routes = ref.watch(deliveryRoutesProvider).valueOrNull ?? [];
|
||||||
|
|
||||||
|
if (routes.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch deliveries for all routes
|
||||||
|
final deliveriesFutures = routes.map((route) {
|
||||||
|
return ref.watch(deliveriesProvider(route.id)).when(
|
||||||
|
data: (deliveries) => deliveries,
|
||||||
|
loading: () => <Delivery>[],
|
||||||
|
error: (_, __) => <Delivery>[],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combine all deliveries
|
||||||
|
final allDeliveries = <Delivery>[];
|
||||||
|
for (final deliveries in deliveriesFutures) {
|
||||||
|
allDeliveries.addAll(deliveries);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allDeliveries;
|
||||||
|
});
|
||||||
|
|
||||||
final languageProvider = StateProvider<String>((ref) {
|
final languageProvider = StateProvider<String>((ref) {
|
||||||
return 'fr';
|
return 'fr';
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user