Commit b32c988a authored by Emily Carroll's avatar Emily Carroll

Feat: Add driver and admin login for webpage

parent c2c69eec
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited. # This file should be version controlled and should not be manually edited.
version: version:
revision: "ac4e799d237041cf905519190471f657b657155a" revision: "a402d9a4376add5bc2d6b1e33e53edaae58c07f8"
channel: "stable" channel: "stable"
project_type: app project_type: app
...@@ -13,26 +13,26 @@ project_type: app ...@@ -13,26 +13,26 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: ac4e799d237041cf905519190471f657b657155a create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
base_revision: ac4e799d237041cf905519190471f657b657155a base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
- platform: android - platform: android
create_revision: ac4e799d237041cf905519190471f657b657155a create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
base_revision: ac4e799d237041cf905519190471f657b657155a base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
- platform: ios - platform: ios
create_revision: ac4e799d237041cf905519190471f657b657155a create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
base_revision: ac4e799d237041cf905519190471f657b657155a base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
- platform: linux - platform: linux
create_revision: ac4e799d237041cf905519190471f657b657155a create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
base_revision: ac4e799d237041cf905519190471f657b657155a base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
- platform: macos - platform: macos
create_revision: ac4e799d237041cf905519190471f657b657155a create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
base_revision: ac4e799d237041cf905519190471f657b657155a base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
- platform: web - platform: web
create_revision: ac4e799d237041cf905519190471f657b657155a create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
base_revision: ac4e799d237041cf905519190471f657b657155a base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
- platform: windows - platform: windows
create_revision: ac4e799d237041cf905519190471f657b657155a create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
base_revision: ac4e799d237041cf905519190471f657b657155a base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
# User provided section # User provided section
......
...@@ -79,7 +79,7 @@ class MyApp extends StatelessWidget { ...@@ -79,7 +79,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 +87,7 @@ class MyApp extends StatelessWidget { ...@@ -87,6 +87,7 @@ 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(),
"/adminDashboard": (context) => const AdminDashboardScreen(),
}, },
); );
}, },
......
...@@ -35,6 +35,10 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> { ...@@ -35,6 +35,10 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
setState(() { setState(() {
_user = user; _user = user;
}); });
if (user == null) {
Navigator.of(context).pushReplacementNamed('/login');
}
} }
}); });
} }
......
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,11 +63,45 @@ class _MapScreenState extends State<MapScreen> { ...@@ -57,11 +63,45 @@ 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.getAddresses(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}");
}
} else {
print("Skipping address due to missing fields: ${address.id}");
}
}
if (mounted) {
setState(() => _markers = newMarkers);
} }
}); });
} }
...@@ -76,6 +116,13 @@ class _MapScreenState extends State<MapScreen> { ...@@ -76,6 +116,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,24 +133,18 @@ class _MapScreenState extends State<MapScreen> { ...@@ -86,24 +133,18 @@ 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("Driver's View"),
leading: IconButton( automaticallyImplyLeading: false,
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
actions: [ actions: [
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,
),
], ],
), ),
body: _currentLocation == null body: _currentLocation == null
...@@ -116,14 +157,8 @@ class _MapScreenState extends State<MapScreen> { ...@@ -116,14 +157,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,13 +11,21 @@ class SignupPage extends StatefulWidget { ...@@ -13,13 +11,21 @@ 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.";
...@@ -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');
} }
} 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(
SnackBar(content: Text(errorMessage)),
);
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Signup Failed: ${e.toString()}")), SnackBar(content: Text("An unexpected error occurred: ${e.toString()}")),
); );
} finally { } 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) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Google Sign-Up Failed: ${e.toString()}")),
);
}
} finally {
if(mounted) {
setState(() => _isLoading = false); setState(() => _isLoading = false);
} }
} }
...@@ -87,92 +78,68 @@ class _SignupPageState extends State<SignupPage> { ...@@ -87,92 +78,68 @@ 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(
child: Form( padding: const EdgeInsets.all(32.0),
key: _formKey, child: ConstrainedBox(
child: Column( constraints: const BoxConstraints(maxWidth: 400),
mainAxisSize: MainAxisSize.min, child: Form(
children: [ key: _formKey,
TextFormField( child: Column(
controller: _firstNameController, mainAxisAlignment: MainAxisAlignment.center,
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(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
validator: (value) => value!.isEmpty ? "Enter your email" : null,
),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
suffixIcon: Tooltip(
message: 'Password must be at least 12 characters long and include:\n'
'- 1 uppercase letter\n'
'- 1 number\n'
'- 1 special character (!@#\$%^&*(),.?":{}|<>)',
child: Icon(Icons.help_outline),
),
),
obscureText: true,
validator: _validatePassword,
),
TextFormField(
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: [ children: [
const Expanded(child: Divider()), TextFormField(
Padding( controller: _emailController,
padding: const EdgeInsets.symmetric(horizontal: 16), decoration: const InputDecoration(
child: Text('OR'), labelText: 'Username (Email)',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
validator: _validateEmail,
), ),
const Expanded(child: Divider()), const SizedBox(height: 16),
], TextFormField(
), controller: _passwordController,
const SizedBox(height: 16), decoration: InputDecoration(
_isLoading labelText: 'Password',
? const CircularProgressIndicator() border: const OutlineInputBorder(),
: SizedBox( suffixIcon: Tooltip(
width: double.infinity, message: 'Password must be at least 12 characters long and include:\n'
child: ElevatedButton( '- 1 uppercase letter\n'
onPressed: _signUp, '- 1 number\n'
child: const Text('Sign Up with Email'), '- 1 special character (!@#\$%^&*(),.?":{}|<>)',
child: const Icon(Icons.help_outline),
), ),
), ),
], obscureText: true,
validator: _validatePassword,
),
const SizedBox(height: 30),
_isLoading
? const CircularProgressIndicator()
: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _signUp,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Create Account'),
),
),
],
),
),
), ),
), ),
), ),
......
...@@ -33,6 +33,7 @@ ...@@ -33,6 +33,7 @@
<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>
</head> </head>
<body> <body>
<script src="flutter_bootstrap.js" async></script> <script src="flutter_bootstrap.js" async></script>
......
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