ios build, connected data (not finished)
This commit is contained in:
+101
-67
@@ -8,6 +8,8 @@ import '../api/openapi_config.dart';
|
||||
import '../models/delivery_commands.dart';
|
||||
import '../utils/breakpoints.dart';
|
||||
import '../utils/responsive.dart';
|
||||
import '../components/map_sidebar_layout.dart';
|
||||
import '../components/delivery_map.dart';
|
||||
|
||||
class DeliveriesPage extends ConsumerStatefulWidget {
|
||||
final int routeFragmentId;
|
||||
@@ -26,6 +28,7 @@ class DeliveriesPage extends ConsumerStatefulWidget {
|
||||
class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
late PageController _pageController;
|
||||
int _currentSegment = 0;
|
||||
Delivery? _selectedDelivery;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -58,57 +61,80 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
.where((d) => d.delivered)
|
||||
.toList();
|
||||
|
||||
return 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,
|
||||
);
|
||||
});
|
||||
},
|
||||
return MapSidebarLayout(
|
||||
mapWidget: DeliveryMap(
|
||||
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,
|
||||
onAction: (delivery, action) =>
|
||||
_handleDeliveryAction(context, delivery, action, token),
|
||||
),
|
||||
DeliveryListView(
|
||||
deliveries: completedDeliveries,
|
||||
onAction: (delivery, action) =>
|
||||
_handleDeliveryAction(context, delivery, action, token),
|
||||
),
|
||||
],
|
||||
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(
|
||||
@@ -200,19 +226,8 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
break;
|
||||
|
||||
case 'map':
|
||||
if (delivery.deliveryAddress != null) {
|
||||
final address = delivery.deliveryAddress!;
|
||||
final Uri mapUri = Uri(
|
||||
scheme: 'https',
|
||||
host: 'maps.google.com',
|
||||
queryParameters: {
|
||||
'q': '${address.latitude},${address.longitude}',
|
||||
},
|
||||
);
|
||||
if (await canLaunchUrl(mapUri)) {
|
||||
await launchUrl(mapUri);
|
||||
}
|
||||
}
|
||||
// Navigation is now handled in-app by the DeliveryMap component
|
||||
// Just ensure the delivery is selected
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -220,11 +235,15 @@ class _DeliveriesPageState extends ConsumerState<DeliveriesPage> {
|
||||
|
||||
class DeliveryListView extends StatelessWidget {
|
||||
final List<Delivery> deliveries;
|
||||
final Delivery? selectedDelivery;
|
||||
final ValueChanged<Delivery> onDeliverySelected;
|
||||
final Function(Delivery, String) onAction;
|
||||
|
||||
const DeliveryListView({
|
||||
super.key,
|
||||
required this.deliveries,
|
||||
this.selectedDelivery,
|
||||
required this.onDeliverySelected,
|
||||
required this.onAction,
|
||||
});
|
||||
|
||||
@@ -246,6 +265,8 @@ class DeliveryListView extends StatelessWidget {
|
||||
final delivery = deliveries[index];
|
||||
return DeliveryCard(
|
||||
delivery: delivery,
|
||||
isSelected: selectedDelivery?.id == delivery.id,
|
||||
onTap: () => onDeliverySelected(delivery),
|
||||
onAction: onAction,
|
||||
);
|
||||
},
|
||||
@@ -256,11 +277,15 @@ class DeliveryListView extends StatelessWidget {
|
||||
|
||||
class DeliveryCard extends StatelessWidget {
|
||||
final Delivery delivery;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
final Function(Delivery, String) onAction;
|
||||
|
||||
const DeliveryCard({
|
||||
super.key,
|
||||
required this.delivery,
|
||||
this.isSelected = false,
|
||||
required this.onTap,
|
||||
required this.onAction,
|
||||
});
|
||||
|
||||
@@ -273,9 +298,14 @@ class DeliveryCard extends StatelessWidget {
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3)
|
||||
: null,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
@@ -350,9 +380,12 @@ class DeliveryCard extends StatelessWidget {
|
||||
),
|
||||
if (delivery.deliveryAddress != null)
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => onAction(delivery, 'map'),
|
||||
onPressed: () {
|
||||
onTap(); // Select the delivery
|
||||
onAction(delivery, 'map');
|
||||
},
|
||||
icon: const Icon(Icons.map),
|
||||
label: const Text('Map'),
|
||||
label: const Text('Navigate'),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _showDeliveryActions(context),
|
||||
@@ -362,6 +395,7 @@ class DeliveryCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+157
-42
@@ -2,55 +2,170 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/providers.dart';
|
||||
|
||||
class LoginPage extends ConsumerWidget {
|
||||
class LoginPage extends ConsumerStatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
bool _obscurePassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_usernameController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleLogin() async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
final authService = ref.read(authServiceProvider);
|
||||
final result = await authService.login(
|
||||
username: _usernameController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
result.when(
|
||||
success: (token) {
|
||||
// ignore: unused_result
|
||||
ref.refresh(isAuthenticatedProvider);
|
||||
},
|
||||
onError: (error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
},
|
||||
cancelled: () {},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Plan B Logistics',
|
||||
style: Theme.of(context).textTheme.displayMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.local_shipping,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Plan B Logistics',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.displayMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Delivery Management System',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
TextFormField(
|
||||
controller: _usernameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Username',
|
||||
hintText: 'Enter your username',
|
||||
prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
enabled: !_isLoading,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Please enter your username';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
hintText: 'Enter your password',
|
||||
prefixIcon: const Icon(Icons.lock),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscurePassword = !_obscurePassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
obscureText: _obscurePassword,
|
||||
textInputAction: TextInputAction.done,
|
||||
enabled: !_isLoading,
|
||||
onFieldSubmitted: (_) => _handleLogin(),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your password';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
onPressed: _isLoading ? null : _handleLogin,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const Text('Login'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Delivery Management System',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final authService = ref.read(authServiceProvider);
|
||||
final result = await authService.login();
|
||||
result.when(
|
||||
success: (token) {
|
||||
if (context.mounted) {
|
||||
// ignore: unused_result
|
||||
ref.refresh(isAuthenticatedProvider);
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Login failed: $error')),
|
||||
);
|
||||
}
|
||||
},
|
||||
cancelled: () {},
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
),
|
||||
child: const Text('Login with Keycloak'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -143,7 +143,7 @@ class RoutesPage extends ConsumerWidget {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DeliveriesPage(
|
||||
routeFragmentId: route.routeFragmentId,
|
||||
routeFragmentId: route.id,
|
||||
routeName: route.name,
|
||||
),
|
||||
),
|
||||
@@ -163,7 +163,7 @@ class RoutesPage extends ConsumerWidget {
|
||||
),
|
||||
SizedBox(height: ResponsiveSpacing.sm(context)),
|
||||
Text(
|
||||
'${route.completedDeliveries}/${route.totalDeliveries} completed',
|
||||
'${route.deliveredCount}/${route.deliveriesCount} completed',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
SizedBox(height: ResponsiveSpacing.md(context)),
|
||||
|
||||
Reference in New Issue
Block a user