Commit a2d2df27 authored by aleenaasghar's avatar aleenaasghar

Web Page for Admin and Driver Users

parent c2c69eec
...@@ -20,6 +20,8 @@ import 'screens/login.dart'; ...@@ -20,6 +20,8 @@ import 'screens/login.dart';
import 'screens/signup.dart'; import 'screens/signup.dart';
import 'screens/forgot_password.dart'; import 'screens/forgot_password.dart';
import 'screens/admin_dashboard_screen.dart'; import 'screens/admin_dashboard_screen.dart';
import 'screens/assigned_addresses_screen.dart';
import 'screens/driver_assignments_screen.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
...@@ -79,7 +81,7 @@ class MyApp extends StatelessWidget { ...@@ -79,7 +81,7 @@ class MyApp extends StatelessWidget {
), ),
useMaterial3: true, useMaterial3: true,
), ),
home: kIsWeb ? const AdminDashboardScreen() : const HomeScreen(), home: kIsWeb ? const LoginPage() : const HomeScreen(),
routes: { routes: {
"/login": (context) => const LoginPage(), "/login": (context) => const LoginPage(),
"/signup": (context) => const SignupPage(), "/signup": (context) => const SignupPage(),
...@@ -87,6 +89,9 @@ class MyApp extends StatelessWidget { ...@@ -87,6 +89,9 @@ class MyApp extends StatelessWidget {
"/map": (context) => const MapScreen(), "/map": (context) => const MapScreen(),
"/settings": (context) => const SettingsScreen(), "/settings": (context) => const SettingsScreen(),
"/profile": (context) => const ProfileScreen(), "/profile": (context) => const ProfileScreen(),
"/admin-dashboard": (context) => const AdminDashboardScreen(),
"/assigned-addresses": (context) => AssignedAddressesScreen(),
"/driver-assignments": (context) => const DriverAssignmentsScreen(),
}, },
); );
}, },
......
...@@ -11,6 +11,8 @@ class DeliveryAddress { ...@@ -11,6 +11,8 @@ class DeliveryAddress {
final double? longitude; final double? longitude;
final String? notes; final String? notes;
final DateTime createdAt; final DateTime createdAt;
final String? driverId;
final String status;
DeliveryAddress({ DeliveryAddress({
String? id, String? id,
...@@ -23,6 +25,8 @@ class DeliveryAddress { ...@@ -23,6 +25,8 @@ class DeliveryAddress {
this.longitude, this.longitude,
this.notes, this.notes,
DateTime? createdAt, DateTime? createdAt,
this.driverId,
this.status = 'pending',
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? DateTime.now(); createdAt = createdAt ?? DateTime.now();
...@@ -41,6 +45,8 @@ class DeliveryAddress { ...@@ -41,6 +45,8 @@ class DeliveryAddress {
'longitude': longitude, 'longitude': longitude,
'notes': notes, 'notes': notes,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),
'driverId': driverId,
'status': status,
}; };
factory DeliveryAddress.fromJson(Map<String, dynamic> json) => DeliveryAddress( factory DeliveryAddress.fromJson(Map<String, dynamic> json) => DeliveryAddress(
...@@ -54,6 +60,8 @@ class DeliveryAddress { ...@@ -54,6 +60,8 @@ class DeliveryAddress {
longitude: json['longitude']?.toDouble(), longitude: json['longitude']?.toDouble(),
notes: json['notes'], notes: json['notes'],
createdAt: DateTime.parse(json['createdAt']), createdAt: DateTime.parse(json['createdAt']),
driverId: json['driverId'],
status: json['status'] ?? 'pending',
); );
DeliveryAddress copyWith({ DeliveryAddress copyWith({
...@@ -65,6 +73,8 @@ class DeliveryAddress { ...@@ -65,6 +73,8 @@ class DeliveryAddress {
double? latitude, double? latitude,
double? longitude, double? longitude,
String? notes, String? notes,
String? driverId,
String? status,
}) => DeliveryAddress( }) => DeliveryAddress(
id: id, id: id,
userId: userId ?? this.userId, userId: userId ?? this.userId,
...@@ -76,5 +86,7 @@ class DeliveryAddress { ...@@ -76,5 +86,7 @@ class DeliveryAddress {
longitude: longitude ?? this.longitude, longitude: longitude ?? this.longitude,
notes: notes ?? this.notes, notes: notes ?? this.notes,
createdAt: createdAt, createdAt: createdAt,
driverId: driverId ?? this.driverId,
status: status ?? this.status,
); );
} }
import 'package:cloud_firestore/cloud_firestore.dart';
class UserModel {
final String uid;
final String? email;
final String? displayName;
final String? role;
UserModel({
required this.uid,
this.email,
this.displayName,
this.role,
});
factory UserModel.fromFirestore(DocumentSnapshot doc) {
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
return UserModel(
uid: doc.id,
email: data['email'],
displayName: data['displayName'],
role: data['role'],
);
}
}
This diff is collapsed.
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/delivery_address.dart';
import '../models/user_model.dart';
import '../services/firestore_service.dart';
class AssignedAddressesScreen extends StatelessWidget {
final FirestoreService _firestoreService = FirestoreService();
AssignedAddressesScreen({super.key});
Future<UserModel?> _getDriver(String driverId) async {
return await _firestoreService.getUserById(driverId);
}
@override
Widget build(BuildContext context) {
final user = FirebaseAuth.instance.currentUser;
return Scaffold(
appBar: AppBar(
title: const Text('Assigned Addresses'),
),
body: user == null
? const Center(child: Text('Please log in to see your assigned addresses.'))
: StreamBuilder<List<DeliveryAddress>>(
stream: _firestoreService.getAssignedAddresses(user.uid),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No addresses have been assigned yet.'));
}
final assignedAddresses = snapshot.data!;
return ListView.builder(
itemCount: assignedAddresses.length,
itemBuilder: (context, index) {
final address = assignedAddresses[index];
if (address.driverId == null) {
return Card(
margin: const EdgeInsets.all(8.0),
child: ListTile(
title: Text(address.fullAddress),
subtitle: const Text('Error: Driver ID is missing.'),
),
);
}
return Card(
margin: const EdgeInsets.all(8.0),
child: ListTile(
title: Text(address.fullAddress),
subtitle: FutureBuilder<UserModel?>(
future: _getDriver(address.driverId!),
builder: (context, driverSnapshot) {
if (driverSnapshot.connectionState == ConnectionState.waiting) {
return const Text('Loading driver...');
}
if (driverSnapshot.hasError || driverSnapshot.data == null) {
return const Text('Driver not found');
}
final driver = driverSnapshot.data!;
final capitalizedStatus = address.status.isEmpty
? ''
: '${address.status[0].toUpperCase()}${address.status.substring(1)}';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Assigned to: ${driver.displayName ?? driver.email ?? 'Unknown Driver'}'),
Text('Status: $capitalizedStatus'),
],
);
},
),
),
);
},
);
},
),
);
}
}
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/delivery_address.dart';
import '../services/firestore_service.dart';
class DriverAssignmentsScreen extends StatefulWidget {
const DriverAssignmentsScreen({super.key});
@override
State<DriverAssignmentsScreen> createState() => _DriverAssignmentsScreenState();
}
class _DriverAssignmentsScreenState extends State<DriverAssignmentsScreen> {
final FirestoreService _firestoreService = FirestoreService();
final User? currentUser = FirebaseAuth.instance.currentUser;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Assignments'),
),
body: currentUser == null
? const Center(child: Text('Please log in to see your assignments.'))
: StreamBuilder<List<DeliveryAddress>>(
stream: _firestoreService.getDriverDeliveries(currentUser!.uid),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No Assignments Yet'));
}
final addresses = snapshot.data!;
return ListView.builder(
itemCount: addresses.length,
itemBuilder: (context, index) {
final address = addresses[index];
final capitalizedStatus = address.status.isEmpty
? ''
: '${address.status[0].toUpperCase()}${address.status.substring(1)}';
return Card(
margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: ListTile(
title: Text(address.fullAddress),
subtitle: Text('Status: $capitalizedStatus'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () {
_firestoreService.updateDeliveryStatus(address.id, 'accepted');
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
),
child: const Text('Accept'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
_firestoreService.denyAssignment(address.id);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey.shade700,
foregroundColor: Colors.white,
),
child: const Text('Deny'),
),
],
),
),
);
},
);
},
),
);
}
}
This diff is collapsed.
...@@ -3,6 +3,11 @@ import 'package:flutter/material.dart'; ...@@ -3,6 +3,11 @@ import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:location/location.dart'; import 'package:location/location.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:geocoding/geocoding.dart' as geocoding;
import '../models/delivery_address.dart';
import '../services/firestore_service.dart';
class MapScreen extends StatefulWidget { class MapScreen extends StatefulWidget {
const MapScreen({super.key}); const MapScreen({super.key});
...@@ -13,8 +18,10 @@ class MapScreen extends StatefulWidget { ...@@ -13,8 +18,10 @@ class MapScreen extends StatefulWidget {
class _MapScreenState extends State<MapScreen> { class _MapScreenState extends State<MapScreen> {
final Completer<GoogleMapController> _controller = Completer(); final Completer<GoogleMapController> _controller = Completer();
final FirestoreService _firestoreService = FirestoreService();
LocationData? _currentLocation; LocationData? _currentLocation;
StreamSubscription<LocationData>? _locationSubscription; StreamSubscription<LocationData>? _locationSubscription;
Set<Marker> _markers = {};
final User? user = FirebaseAuth.instance.currentUser; final User? user = FirebaseAuth.instance.currentUser;
...@@ -26,29 +33,28 @@ class _MapScreenState extends State<MapScreen> { ...@@ -26,29 +33,28 @@ class _MapScreenState extends State<MapScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializeLocation(); _initializeLocationAndMarkers();
}
Future<void> _initializeLocationAndMarkers() async {
await _initializeLocation();
if (user != null) {
_loadAddressMarkers(user!.uid);
}
} }
Future<void> _initializeLocation() async { Future<void> _initializeLocation() async {
Location location = Location(); Location location = Location();
bool serviceEnabled = await location.serviceEnabled();
bool serviceEnabled;
PermissionStatus permissionGranted;
serviceEnabled = await location.serviceEnabled();
if (!serviceEnabled) { if (!serviceEnabled) {
serviceEnabled = await location.requestService(); serviceEnabled = await location.requestService();
if (!serviceEnabled) { if (!serviceEnabled) return;
return;
}
} }
permissionGranted = await location.hasPermission(); PermissionStatus permissionGranted = await location.hasPermission();
if (permissionGranted == PermissionStatus.denied) { if (permissionGranted == PermissionStatus.denied) {
permissionGranted = await location.requestPermission(); permissionGranted = await location.requestPermission();
if (permissionGranted != PermissionStatus.granted) { if (permissionGranted != PermissionStatus.granted) return;
return;
}
} }
_currentLocation = await location.getLocation(); _currentLocation = await location.getLocation();
...@@ -57,15 +63,47 @@ class _MapScreenState extends State<MapScreen> { ...@@ -57,15 +63,47 @@ class _MapScreenState extends State<MapScreen> {
} }
_locationSubscription = location.onLocationChanged.listen((LocationData newLocation) { _locationSubscription = location.onLocationChanged.listen((LocationData newLocation) {
if(mounted) { if (mounted) {
setState(() { setState(() => _currentLocation = newLocation);
_currentLocation = newLocation;
});
_moveCameraToLocation(newLocation); _moveCameraToLocation(newLocation);
} }
}); });
} }
Future<void> _loadAddressMarkers(String userId) async {
_firestoreService.getDriverDeliveries(userId).listen((addresses) async {
Set<Marker> newMarkers = {};
for (var address in addresses) {
// Check for null or empty required fields before geocoding
if (address.streetAddress.isNotEmpty &&
address.city.isNotEmpty &&
address.state.isNotEmpty &&
address.zipCode.isNotEmpty) {
try {
List<geocoding.Location> locations = await geocoding.locationFromAddress(
'${address.streetAddress}, ${address.city}, ${address.state} ${address.zipCode}'
);
if (locations.isNotEmpty) {
final loc = locations.first;
newMarkers.add(
Marker(
markerId: MarkerId(address.id),
position: LatLng(loc.latitude, loc.longitude),
infoWindow: InfoWindow(title: address.streetAddress, snippet: address.notes),
),
);
}
} catch (e) {
print("Error geocoding address: ${e}");
}
}
}
if (mounted) {
setState(() => _markers = newMarkers);
}
});
}
Future<void> _moveCameraToLocation(LocationData locationData) async { Future<void> _moveCameraToLocation(LocationData locationData) async {
final GoogleMapController controller = await _controller.future; final GoogleMapController controller = await _controller.future;
controller.animateCamera(CameraUpdate.newCameraPosition( controller.animateCamera(CameraUpdate.newCameraPosition(
...@@ -76,6 +114,13 @@ class _MapScreenState extends State<MapScreen> { ...@@ -76,6 +114,13 @@ class _MapScreenState extends State<MapScreen> {
)); ));
} }
Future<void> _logout() async {
await FirebaseAuth.instance.signOut();
if (mounted) {
Navigator.of(context).pushNamedAndRemoveUntil('/', (Route<dynamic> route) => false);
}
}
@override @override
void dispose() { void dispose() {
_locationSubscription?.cancel(); _locationSubscription?.cancel();
...@@ -86,23 +131,24 @@ class _MapScreenState extends State<MapScreen> { ...@@ -86,23 +131,24 @@ class _MapScreenState extends State<MapScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Your Location'), title: const Text("GraphGo Driver"),
leading: IconButton( automaticallyImplyLeading: false,
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
actions: [ actions: [
TextButton.icon(
onPressed: () {
Navigator.of(context).pushNamed('/driver-assignments');
},
icon: const Icon(Icons.assignment, color: Colors.white),
label: const Text('View Assignments', style: TextStyle(color: Colors.white)),
),
if (user?.email != null) if (user?.email != null)
Padding( Padding(
padding: const EdgeInsets.only(right: 16.0), padding: const EdgeInsets.only(right: 16.0),
child: Center( child: Center(child: Text(user!.email!, style: const TextStyle(fontSize: 12))),
child: Text(
user!.email!,
style: const TextStyle(
fontSize: 12,
),
),
), ),
IconButton(
icon: const Icon(Icons.logout),
onPressed: _logout,
), ),
], ],
), ),
...@@ -116,14 +162,8 @@ class _MapScreenState extends State<MapScreen> { ...@@ -116,14 +162,8 @@ class _MapScreenState extends State<MapScreen> {
}, },
myLocationEnabled: true, myLocationEnabled: true,
myLocationButtonEnabled: true, myLocationButtonEnabled: true,
markers: {
if (_currentLocation != null) markers: _markers,
Marker(
markerId: const MarkerId('currentLocation'),
position: LatLng(_currentLocation!.latitude!, _currentLocation!.longitude!),
infoWindow: const InfoWindow(title: 'My Location'),
),
},
), ),
); );
} }
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import '../services/google_auth_service.dart';
import '../colors.dart';
class SignupPage extends StatefulWidget { class SignupPage extends StatefulWidget {
const SignupPage({super.key}); const SignupPage({super.key});
...@@ -13,19 +11,27 @@ class SignupPage extends StatefulWidget { ...@@ -13,19 +11,27 @@ class SignupPage extends StatefulWidget {
class _SignupPageState extends State<SignupPage> { class _SignupPageState extends State<SignupPage> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final TextEditingController _firstNameController = TextEditingController();
final TextEditingController _lastNameController = TextEditingController();
final TextEditingController _emailController = TextEditingController(); final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController(); final TextEditingController _passwordController = TextEditingController();
final TextEditingController _confirmPasswordController = TextEditingController();
bool _isLoading = false; bool _isLoading = false;
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return "Email is required.";
}
final emailRegex = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\$');
if (!emailRegex.hasMatch(value)) {
return "Please enter a valid email address.";
}
return null;
}
String? _validatePassword(String? value) { String? _validatePassword(String? value) {
if (value == null || value.isEmpty) return "Password is required."; if (value == null || value.isEmpty) return "Password is required.";
if (value.length < 12) return "Password must be at least 12 characters."; if (value.length < 12) return "Password must be at least 12 characters.";
if (!RegExp(r'[A-Z]').hasMatch(value)) return "Must contain 1 uppercase letter."; if (!RegExp(r'[A-Z]').hasMatch(value)) return "Must contain 1 uppercase letter.";
if (!RegExp(r'\d').hasMatch(value)) return "Must contain 1 number."; if (!RegExp(r'\\d').hasMatch(value)) return "Must contain 1 number.";
if (!RegExp(r'[!@#\$%^&*(),.?\":{}|<>]').hasMatch(value)) return "Must contain 1 special character."; if (!RegExp(r'[!@#\\\$%^&*(),.?":{}|<>]').hasMatch(value)) return "Must contain 1 special character.";
return null; return null;
} }
...@@ -41,45 +47,30 @@ class _SignupPageState extends State<SignupPage> { ...@@ -41,45 +47,30 @@ class _SignupPageState extends State<SignupPage> {
); );
await FirebaseFirestore.instance.collection('users').doc(userCredential.user!.uid).set({ await FirebaseFirestore.instance.collection('users').doc(userCredential.user!.uid).set({
'first_name': _firstNameController.text.trim(),
'last_name': _lastNameController.text.trim(),
'email': _emailController.text.trim(), 'email': _emailController.text.trim(),
'provider': 'email', 'provider': 'email',
'created_at': Timestamp.now(), 'created_at': Timestamp.now(),
}); });
if (mounted) { if (mounted) {
// After signing up, go to the map screen
Navigator.of(context).pushReplacementNamed('/map'); Navigator.of(context).pushReplacementNamed('/map');
} }
} catch (e) { } on FirebaseAuthException catch (e) {
String errorMessage;
if (e.code == 'email-already-in-use') {
errorMessage = "This email is already registered. Please log in or use a different email.";
} else {
errorMessage = "Signup Failed: \${e.message}";
}
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Signup Failed: ${e.toString()}")), SnackBar(content: Text(errorMessage)),
); );
} finally {
if(mounted) {
setState(() => _isLoading = false);
}
}
}
Future<void> _signUpWithGoogle() async {
setState(() => _isLoading = true);
try {
await GoogleAuthService.signInWithGoogle();
final user = FirebaseAuth.instance.currentUser;
if (user != null && mounted) {
Navigator.of(context).pushReplacementNamed('/map');
}
} catch (e) { } catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Google Sign-Up Failed: ${e.toString()}")), SnackBar(content: Text("An unexpected error occurred: \${e.toString()}")),
); );
}
} finally { } finally {
if(mounted) { if (mounted) {
setState(() => _isLoading = false); setState(() => _isLoading = false);
} }
} }
...@@ -87,95 +78,71 @@ class _SignupPageState extends State<SignupPage> { ...@@ -87,95 +78,71 @@ class _SignupPageState extends State<SignupPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('GraphGo Sign Up'), title: const Text('Create Account'),
// The back button is now handled correctly by the Navigator
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(), // Corrected line onPressed: () => Navigator.of(context).pop(),
tooltip: 'Back to Login', tooltip: 'Back to Login',
), ),
), ),
body: SingleChildScrollView( body: Center(
padding: const EdgeInsets.all(16.0), child: SingleChildScrollView(
padding: const EdgeInsets.all(32.0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Form( child: Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
TextFormField(
controller: _firstNameController,
decoration: const InputDecoration(labelText: 'First Name'),
validator: (value) => value!.isEmpty ? "Enter your first name" : null,
),
TextFormField(
controller: _lastNameController,
decoration: const InputDecoration(labelText: 'Last Name'),
validator: (value) => value!.isEmpty ? "Enter your last name" : null,
),
TextFormField( TextFormField(
controller: _emailController, controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'), decoration: const InputDecoration(
labelText: 'Username (Email)',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
validator: (value) => value!.isEmpty ? "Enter your email" : null, validator: _validateEmail,
), ),
const SizedBox(height: 16),
TextFormField( TextFormField(
controller: _passwordController, controller: _passwordController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Password', labelText: 'Password',
border: const OutlineInputBorder(),
suffixIcon: Tooltip( suffixIcon: Tooltip(
message: 'Password must be at least 12 characters long and include:\n' message: 'Password must be at least 12 characters long and include:\\n'
'- 1 uppercase letter\n' '- 1 uppercase letter\\n'
'- 1 number\n' '- 1 number\\n'
'- 1 special character (!@#\$%^&*(),.?":{}|<>)', '- 1 special character (!@#\\\$%^&*(),.?":{}|<>)',
child: Icon(Icons.help_outline), child: const Icon(Icons.help_outline),
), ),
), ),
obscureText: true, obscureText: true,
validator: _validatePassword, validator: _validatePassword,
), ),
TextFormField( const SizedBox(height: 30),
controller: _confirmPasswordController,
decoration: const InputDecoration(labelText: 'Confirm Password'),
obscureText: true,
validator: (value) => value != _passwordController.text ? "Passwords do not match" : null,
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _isLoading ? null : _signUpWithGoogle,
icon: const Icon(Icons.login, size: 20, color: Colors.blue),
label: const Text('Sign up with Google'),
),
),
const SizedBox(height: 16),
Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text('OR'),
),
const Expanded(child: Divider()),
],
),
const SizedBox(height: 16),
_isLoading _isLoading
? const CircularProgressIndicator() ? const CircularProgressIndicator()
: SizedBox( : SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: _signUp, onPressed: _signUp,
child: const Text('Sign Up with Email'), style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Create Account'),
), ),
), ),
], ],
), ),
), ),
), ),
),
),
); );
} }
} }
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/delivery_address.dart'; import '../models/delivery_address.dart';
import '../models/user_model.dart';
class FirestoreService { class FirestoreService {
final FirebaseFirestore _db = FirebaseFirestore.instance; final FirebaseFirestore _db = FirebaseFirestore.instance;
final String _collectionPath = 'addresses'; final String _addressesCollectionPath = 'addresses';
final String _usersCollectionPath = 'users';
// Get a stream of addresses for a specific user // Get a stream of addresses for a specific user
Stream<List<DeliveryAddress>> getAddresses(String userId) { Stream<List<DeliveryAddress>> getAddresses(String userId) {
return _db return _db
.collection(_collectionPath) .collection(_addressesCollectionPath)
.where('userId', isEqualTo: userId) .where('userId', isEqualTo: userId)
.snapshots() .snapshots()
.map((snapshot) => .map((snapshot) =>
snapshot.docs.map((doc) => DeliveryAddress.fromJson(doc.data())).toList()); snapshot.docs.map((doc) => DeliveryAddress.fromJson(doc.data())).toList());
} }
// Get a stream of unassigned addresses
Stream<List<DeliveryAddress>> getUnassignedAddresses(String userId) {
return _db
.collection(_addressesCollectionPath)
.where('userId', isEqualTo: userId)
.where('status', isEqualTo: 'pending')
.snapshots()
.map((snapshot) =>
snapshot.docs.map((doc) => DeliveryAddress.fromJson(doc.data())).toList());
}
// Add or update an address // Add or update an address
Future<void> saveAddress(DeliveryAddress address) { Future<void> saveAddress(DeliveryAddress address) {
return _db.collection(_collectionPath).doc(address.id).set(address.toJson()); return _db.collection(_addressesCollectionPath).doc(address.id).set(address.toJson());
} }
// Save a list of addresses from a CSV // Save a list of addresses from a CSV
Future<void> saveAddressesFromCsv(List<DeliveryAddress> addresses) async { Future<void> saveAddressesFromCsv(List<DeliveryAddress> addresses) async {
final batch = _db.batch(); final batch = _db.batch();
for (final address in addresses) { for (final address in addresses) {
final docRef = _db.collection(_collectionPath).doc(address.id); final docRef = _db.collection(_addressesCollectionPath).doc(address.id);
batch.set(docRef, address.toJson()); batch.set(docRef, address.toJson());
} }
await batch.commit(); await batch.commit();
...@@ -32,6 +46,125 @@ class FirestoreService { ...@@ -32,6 +46,125 @@ class FirestoreService {
// Delete an address // Delete an address
Future<void> deleteAddress(String addressId) { Future<void> deleteAddress(String addressId) {
return _db.collection(_collectionPath).doc(addressId).delete(); return _db.collection(_addressesCollectionPath).doc(addressId).delete();
}
// Get a stream of addresses for a specific driver
Stream<List<DeliveryAddress>> getDriverDeliveries(String driverId) {
return _db
.collection(_addressesCollectionPath)
.where('driverId', isEqualTo: driverId)
.snapshots()
.map((snapshot) =>
snapshot.docs.map((doc) => DeliveryAddress.fromJson(doc.data())).toList());
}
// Update delivery status to accepted
Future<void> updateDeliveryStatus(String addressId, String status) {
return _db.collection(_addressesCollectionPath).doc(addressId).update({
'status': status,
});
}
// Deny an assignment
Future<void> denyAssignment(String addressId) {
return _db.collection(_addressesCollectionPath).doc(addressId).update({
'status': 'denied',
'driverId': FieldValue.delete(),
});
}
// Get all users
Stream<List<UserModel>> getUsers() {
return _db.collection(_usersCollectionPath).snapshots().map((snapshot) =>
snapshot.docs.map((doc) => UserModel.fromFirestore(doc)).toList());
}
// Get all drivers
Stream<List<UserModel>> getDrivers() {
return _db
.collection(_usersCollectionPath)
.where('role', isEqualTo: 'driver')
.snapshots()
.map((snapshot) =>
snapshot.docs.map((doc) => UserModel.fromFirestore(doc)).toList());
}
// Get a stream of assigned addresses for a specific user
Stream<List<DeliveryAddress>> getAssignedAddresses(String userId) {
return _db
.collection(_addressesCollectionPath)
.where('userId', isEqualTo: userId)
.where('status', whereIn: ['assigned', 'accepted'])
.snapshots()
.map((snapshot) =>
snapshot.docs.map((doc) => DeliveryAddress.fromJson(doc.data())).toList());
}
// Get a user by their ID
Future<UserModel?> getUserById(String uid) async {
final doc = await _db.collection(_usersCollectionPath).doc(uid).get();
if (doc.exists) {
return UserModel.fromFirestore(doc);
}
return null;
}
// Assign a list of addresses to a list of drivers in a round-robin fashion
Future<void> assignAddressesToDrivers(
List<String> addressIds, List<String> driverIds) async {
if (addressIds.isEmpty || driverIds.isEmpty) return;
final batch = _db.batch();
int driverIndex = 0;
for (final addressId in addressIds) {
final driverId = driverIds[driverIndex];
final docRef = _db.collection(_addressesCollectionPath).doc(addressId);
batch.update(docRef, {'driverId': driverId, 'status': 'assigned'});
driverIndex = (driverIndex + 1) % driverIds.length;
}
await batch.commit();
}
// Unassign all addresses for a specific user
Future<void> unassignAllAddresses(String userId) async {
final addresses = await getAssignedAddresses(userId).first;
final batch = _db.batch();
for (final address in addresses) {
final docRef = _db.collection(_addressesCollectionPath).doc(address.id);
batch.update(docRef, {'driverId': FieldValue.delete(), 'status': 'pending'});
}
await batch.commit();
}
// Reassign a denied address
Future<void> reassignAddress(String addressId) {
return _db.collection(_addressesCollectionPath).doc(addressId).update({
'status': 'pending',
'driverId': FieldValue.delete(),
});
}
// Assign a user the driver role
Future<void> assignDriverRole(String uid) {
return _db.collection(_usersCollectionPath).doc(uid).update({'role': 'driver'});
}
// Remove the driver role from a user
Future<void> removeDriverRole(String uid) {
return _db.collection(_usersCollectionPath).doc(uid).update({'role': FieldValue.delete()});
}
// Assign an address to a driver
Future<void> assignAddressToDriver(String addressId, String driverId) {
return _db.collection(_addressesCollectionPath).doc(addressId).update({
'driverId': driverId,
'status': 'assigned',
});
} }
} }
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/delivery_address.dart'; import '../models/delivery_address.dart';
class AddressList extends StatelessWidget { typedef SelectionChangedCallback = void Function(Set<String> selectedIds);
class AddressList extends StatefulWidget {
final Stream<List<DeliveryAddress>> addressesStream; final Stream<List<DeliveryAddress>> addressesStream;
final Function(DeliveryAddress) onEdit; final Function(DeliveryAddress) onEdit;
final Function(String) onDelete; final Function(String) onDelete;
final Function(String) onReassign;
final SelectionChangedCallback onSelectionChanged;
const AddressList({ const AddressList({
super.key, super.key,
required this.addressesStream, required this.addressesStream,
required this.onEdit, required this.onEdit,
required this.onDelete, required this.onDelete,
required this.onReassign,
required this.onSelectionChanged,
});
@override
_AddressListState createState() => _AddressListState();
}
class _AddressListState extends State<AddressList> {
Set<String> _selectedAddressIds = {};
List<DeliveryAddress> _currentAddresses = [];
bool _isSelectAll = false;
void _handleAddressSelection(String addressId, bool isSelected) {
setState(() {
if (isSelected) {
_selectedAddressIds.add(addressId);
} else {
_selectedAddressIds.remove(addressId);
}
_isSelectAll = _currentAddresses.isNotEmpty &&
_selectedAddressIds.length == _currentAddresses.length;
}); });
widget.onSelectionChanged(_selectedAddressIds);
}
void _toggleSelectAll() {
setState(() {
if (_isSelectAll) {
_selectedAddressIds.clear();
_isSelectAll = false;
} else {
_selectedAddressIds = _currentAddresses.map((addr) => addr.id).toSet();
_isSelectAll = true;
}
});
widget.onSelectionChanged(_selectedAddressIds);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<List<DeliveryAddress>>( return StreamBuilder<List<DeliveryAddress>>(
stream: addressesStream, stream: widget.addressesStream,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}')); return Center(child: Text('Error: ${snapshot.error}'));
...@@ -28,34 +69,73 @@ class AddressList extends StatelessWidget { ...@@ -28,34 +69,73 @@ class AddressList extends StatelessWidget {
return const Center(child: Text('No addresses found.')); return const Center(child: Text('No addresses found.'));
} }
final addresses = snapshot.data!; _currentAddresses = snapshot.data!;
final currentIds = _currentAddresses.map((e) => e.id).toSet();
_selectedAddressIds.removeWhere((id) => !currentIds.contains(id));
return ListView.builder( return Column(
itemCount: addresses.length, children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
Checkbox(
value: _isSelectAll,
onChanged: (bool? value) {
_toggleSelectAll();
},
),
const Text('Select All'),
],
),
),
Expanded(
child: ListView.builder(
itemCount: _currentAddresses.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final address = addresses[index]; final address = _currentAddresses[index];
final isSelected = _selectedAddressIds.contains(address.id);
final capitalizedStatus = address.status.isEmpty
? ''
: '${address.status[0].toUpperCase()}${address.status.substring(1)}';
return Card( return Card(
margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 4.0),
child: ListTile( child: ListTile(
leading: CircleAvatar(child: Text('${index + 1}')), leading: Checkbox(
value: isSelected,
onChanged: (bool? value) {
if (value != null) {
_handleAddressSelection(address.id, value);
}
},
),
title: Text(address.fullAddress), title: Text(address.fullAddress),
subtitle: const Text('Status: Pending'), subtitle: Text('Status: $capitalizedStatus'),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (address.status == 'denied')
TextButton(
onPressed: () => widget.onReassign(address.id),
child: const Text('Reassign', style: TextStyle(color: Colors.orange)),
),
IconButton( IconButton(
icon: const Icon(Icons.edit, color: Colors.blue), icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () => onEdit(address), onPressed: () => widget.onEdit(address),
), ),
IconButton( IconButton(
icon: const Icon(Icons.delete, color: Colors.red), icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => onDelete(address.id), onPressed: () => widget.onDelete(address.id),
), ),
], ],
), ),
), ),
); );
}, },
),
),
],
); );
}, },
); );
......
import 'package:flutter/material.dart';
import '../models/delivery_address.dart';
import '../models/user_model.dart';
class AssignAddressDialog extends StatefulWidget {
final List<UserModel> drivers;
final List<DeliveryAddress> addresses;
final Function(String, String) onAssign;
const AssignAddressDialog({
super.key,
required this.drivers,
required this.addresses,
required this.onAssign,
});
@override
State<AssignAddressDialog> createState() => _AssignAddressDialogState();
}
class _AssignAddressDialogState extends State<AssignAddressDialog> {
String? _selectedDriverId;
String? _selectedAddressId;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Assign Address to Driver'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButtonFormField<String>(
value: _selectedDriverId,
hint: const Text('Select a driver'),
onChanged: (value) {
setState(() {
_selectedDriverId = value;
});
},
items: widget.drivers.map((driver) {
return DropdownMenuItem(
value: driver.uid,
child: Text(driver.displayName ?? driver.email ?? 'N/A'),
);
}).toList(),
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: _selectedAddressId,
hint: const Text('Select an address'),
onChanged: (value) {
setState(() {
_selectedAddressId = value;
});
},
items: widget.addresses.where((address) => address.driverId == null).map((address) {
return DropdownMenuItem(
value: address.id,
child: Text(address.fullAddress),
);
}).toList(),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: (_selectedDriverId != null && _selectedAddressId != null)
? () {
widget.onAssign(_selectedAddressId!, _selectedDriverId!);
Navigator.of(context).pop();
}
: null,
child: const Text('Assign'),
),
],
);
}
}
import 'package:flutter/material.dart';
import '../models/user_model.dart';
typedef AssignCallback = void Function(List<String> driverIds);
class AssignDriversDialog extends StatefulWidget {
final List<UserModel> drivers;
final AssignCallback onAssign;
const AssignDriversDialog({
super.key,
required this.drivers,
required this.onAssign,
});
@override
_AssignDriversDialogState createState() => _AssignDriversDialogState();
}
class _AssignDriversDialogState extends State<AssignDriversDialog> {
Set<String> _selectedDriverIds = {};
bool _isSelectAll = false;
void _handleDriverSelection(String driverId, bool isSelected) {
setState(() {
if (isSelected) {
_selectedDriverIds.add(driverId);
} else {
_selectedDriverIds.remove(driverId);
}
_isSelectAll = widget.drivers.isNotEmpty &&
_selectedDriverIds.length == widget.drivers.length;
});
}
void _toggleSelectAll() {
setState(() {
if (_isSelectAll) {
_selectedDriverIds.clear();
} else {
_selectedDriverIds = widget.drivers.map((d) => d.uid).toSet();
}
_isSelectAll = !_isSelectAll;
});
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Assign to Drivers'),
content: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.drivers.isNotEmpty)
CheckboxListTile(
title: const Text('Select All Drivers'),
value: _isSelectAll,
onChanged: (value) => _toggleSelectAll(),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: widget.drivers.length,
itemBuilder: (context, index) {
final driver = widget.drivers[index];
final isSelected = _selectedDriverIds.contains(driver.uid);
return CheckboxListTile(
title: Text(driver.displayName ?? driver.email ?? driver.uid),
value: isSelected,
onChanged: (bool? value) {
if (value != null) {
_handleDriverSelection(driver.uid, value);
}
},
);
},
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: _selectedDriverIds.isNotEmpty
? () {
widget.onAssign(_selectedDriverIds.toList());
Navigator.of(context).pop();
}
: null,
child: const Text('Assign'),
),
],
);
}
}
import 'package:flutter/material.dart';
import '../models/user_model.dart';
class DriversList extends StatelessWidget {
final Stream<List<UserModel>> driversStream;
final Function(String) onRemoveDriver;
const DriversList({
super.key,
required this.driversStream,
required this.onRemoveDriver,
});
@override
Widget build(BuildContext context) {
return StreamBuilder<List<UserModel>>(
stream: driversStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No drivers found.'));
}
final drivers = snapshot.data!;
return ListView.builder(
itemCount: drivers.length,
itemBuilder: (context, index) {
final driver = drivers[index];
final String displayTitle;
if (driver.displayName != null && driver.displayName!.isNotEmpty) {
displayTitle = driver.displayName!;
} else if (driver.email != null && driver.email!.contains('@')) {
displayTitle = driver.email!.split('@').first;
} else {
displayTitle = driver.email ?? 'N/A';
}
final role = driver.role ?? 'driver';
final capitalizedRole =
role.isEmpty ? '' : '${role[0].toUpperCase()}${role.substring(1)}';
return Card(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 0),
child: ListTile(
title: Text(displayTitle),
subtitle: Text('Role: $capitalizedRole'),
trailing: IconButton(
icon: const Icon(Icons.remove_circle_outline, color: Colors.red),
tooltip: 'Remove Driver Role',
onPressed: () => onRemoveDriver(driver.uid),
),
),
);
},
);
},
);
}
}
import 'package:flutter/material.dart';
import '../models/delivery_address.dart';
class SelectAddressDialog extends StatefulWidget {
final List<DeliveryAddress> addresses;
final Function(List<String>) onSelect;
const SelectAddressDialog({
super.key,
required this.addresses,
required this.onSelect,
});
@override
State<SelectAddressDialog> createState() => _SelectAddressDialogState();
}
class _SelectAddressDialogState extends State<SelectAddressDialog> {
final List<String> _selectedAddressIds = [];
void _addAddress() {
setState(() {
_selectedAddressIds.add("");
});
}
@override
Widget build(BuildContext context) {
final unassignedAddresses = widget.addresses.where((a) => a.driverId == null).toList();
return AlertDialog(
title: const Text('Select Addresses'),
content: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
..._selectedAddressIds.asMap().entries.map((entry) {
int index = entry.key;
return DropdownButtonFormField<String>(
value: _selectedAddressIds[index].isEmpty ? null : _selectedAddressIds[index],
hint: const Text('Choose an address'),
onChanged: (value) {
setState(() {
_selectedAddressIds[index] = value!;
});
},
items: unassignedAddresses.map((address) {
return DropdownMenuItem(
value: address.id,
child: Text(address.fullAddress, overflow: TextOverflow.ellipsis),
);
}).toList(),
);
}).toList(),
const SizedBox(height: 16),
IconButton(
icon: const Icon(Icons.add),
onPressed: _addAddress,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: _selectedAddressIds.isNotEmpty && _selectedAddressIds.every((id) => id.isNotEmpty)
? () {
widget.onSelect(_selectedAddressIds);
Navigator.of(context).pop();
}
: null,
child: const Text('Assign'),
),
],
);
}
}
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/user_model.dart';
class UsersList extends StatelessWidget {
final Stream<List<UserModel>> usersStream;
final Function(String) onAssignDriver;
const UsersList({super.key, required this.usersStream, required this.onAssignDriver});
@override
Widget build(BuildContext context) {
return StreamBuilder<List<UserModel>>(
stream: usersStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No users found.'));
}
final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
final String displayTitle;
if (user.displayName != null && user.displayName!.isNotEmpty) {
displayTitle = user.displayName!;
} else if (user.email != null && user.email!.contains('@')) {
displayTitle = user.email!.split('@').first;
} else {
displayTitle = user.email ?? 'N/A';
}
final role = user.role ?? 'user';
final capitalizedRole =
role.isEmpty ? '' : '${role[0].toUpperCase()}${role.substring(1)}';
return Card(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 0),
child: ListTile(
title: Text(displayTitle),
subtitle: Text('Role: $capitalizedRole'),
trailing: user.role != 'driver'
? ElevatedButton(
onPressed: () => onAssignDriver(user.uid),
child: const Text('Make Driver'),
)
: null,
),
);
},
);
},
);
}
}
...@@ -33,8 +33,29 @@ ...@@ -33,8 +33,29 @@
<title>graph_go</title> <title>graph_go</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCFx_8PW_R6rGq-julkwV4JJGixbzmnP74"></script>
<!-- Firebase SDK -->
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-firestore.js"></script>
</head> </head>
<body> <body>
<script>
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "AIzaSyByWSG8ewS_QX2jLfsmO5YsnbKE7HH8HRE",
authDomain: "graph-go-bd4f0.firebaseapp.com",
projectId: "graph-go-bd4f0",
storageBucket: "graph-go-bd4f0.firebasestorage.app",
messagingSenderId: "627645762372",
appId: "1:627645762372:web:45d648b5ef756be6f2a511",
measurementId: "G-DW8MH83H28"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
</script>
<script src="flutter_bootstrap.js" async></script> <script src="flutter_bootstrap.js" async></script>
</body> </body>
</html> </html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment