Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
M
Mapping-Software-Efficient-Routing-Algorithm
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
h703249754
Mapping-Software-Efficient-Routing-Algorithm
Commits
a2d2df27
Commit
a2d2df27
authored
Oct 07, 2025
by
aleenaasghar
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Web Page for Admin and Driver Users
parent
c2c69eec
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
1357 additions
and
513 deletions
+1357
-513
main.dart
lib/main.dart
+6
-1
delivery_address.dart
lib/models/delivery_address.dart
+12
-0
user_model.dart
lib/models/user_model.dart
+25
-0
admin_dashboard_screen.dart
lib/screens/admin_dashboard_screen.dart
+154
-225
assigned_addresses_screen.dart
lib/screens/assigned_addresses_screen.dart
+89
-0
driver_assignments_screen.dart
lib/screens/driver_assignments_screen.dart
+88
-0
login.dart
lib/screens/login.dart
+168
-107
map_screen.dart
lib/screens/map_screen.dart
+79
-39
signup.dart
lib/screens/signup.dart
+75
-108
firestore_service.dart
lib/services/firestore_service.dart
+138
-5
address_list.dart
lib/widgets/address_list.dart
+108
-28
assign_address_dialog.dart
lib/widgets/assign_address_dialog.dart
+82
-0
assign_drivers_dialog.dart
lib/widgets/assign_drivers_dialog.dart
+101
-0
drivers_list.dart
lib/widgets/drivers_list.dart
+66
-0
select_address_dialog.dart
lib/widgets/select_address_dialog.dart
+81
-0
users_list.dart
lib/widgets/users_list.dart
+64
-0
index.html
web/index.html
+21
-0
No files found.
lib/main.dart
View file @
a2d2df27
...
...
@@ -20,6 +20,8 @@ import 'screens/login.dart';
import
'screens/signup.dart'
;
import
'screens/forgot_password.dart'
;
import
'screens/admin_dashboard_screen.dart'
;
import
'screens/assigned_addresses_screen.dart'
;
import
'screens/driver_assignments_screen.dart'
;
Future
<
void
>
main
()
async
{
WidgetsFlutterBinding
.
ensureInitialized
();
...
...
@@ -79,7 +81,7 @@ class MyApp extends StatelessWidget {
),
useMaterial3:
true
,
),
home:
kIsWeb
?
const
AdminDashboardScreen
()
:
const
HomeScreen
(),
home:
kIsWeb
?
const
LoginPage
()
:
const
HomeScreen
(),
routes:
{
"/login"
:
(
context
)
=>
const
LoginPage
(),
"/signup"
:
(
context
)
=>
const
SignupPage
(),
...
...
@@ -87,6 +89,9 @@ class MyApp extends StatelessWidget {
"/map"
:
(
context
)
=>
const
MapScreen
(),
"/settings"
:
(
context
)
=>
const
SettingsScreen
(),
"/profile"
:
(
context
)
=>
const
ProfileScreen
(),
"/admin-dashboard"
:
(
context
)
=>
const
AdminDashboardScreen
(),
"/assigned-addresses"
:
(
context
)
=>
AssignedAddressesScreen
(),
"/driver-assignments"
:
(
context
)
=>
const
DriverAssignmentsScreen
(),
},
);
},
...
...
lib/models/delivery_address.dart
View file @
a2d2df27
...
...
@@ -11,6 +11,8 @@ class DeliveryAddress {
final
double
?
longitude
;
final
String
?
notes
;
final
DateTime
createdAt
;
final
String
?
driverId
;
final
String
status
;
DeliveryAddress
({
String
?
id
,
...
...
@@ -23,6 +25,8 @@ class DeliveryAddress {
this
.
longitude
,
this
.
notes
,
DateTime
?
createdAt
,
this
.
driverId
,
this
.
status
=
'pending'
,
})
:
id
=
id
??
const
Uuid
().
v4
(),
createdAt
=
createdAt
??
DateTime
.
now
();
...
...
@@ -41,6 +45,8 @@ class DeliveryAddress {
'longitude'
:
longitude
,
'notes'
:
notes
,
'createdAt'
:
createdAt
.
toIso8601String
(),
'driverId'
:
driverId
,
'status'
:
status
,
};
factory
DeliveryAddress
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
DeliveryAddress
(
...
...
@@ -54,6 +60,8 @@ class DeliveryAddress {
longitude:
json
[
'longitude'
]?.
toDouble
(),
notes:
json
[
'notes'
],
createdAt:
DateTime
.
parse
(
json
[
'createdAt'
]),
driverId:
json
[
'driverId'
],
status:
json
[
'status'
]
??
'pending'
,
);
DeliveryAddress
copyWith
({
...
...
@@ -65,6 +73,8 @@ class DeliveryAddress {
double
?
latitude
,
double
?
longitude
,
String
?
notes
,
String
?
driverId
,
String
?
status
,
})
=>
DeliveryAddress
(
id:
id
,
userId:
userId
??
this
.
userId
,
...
...
@@ -76,5 +86,7 @@ class DeliveryAddress {
longitude:
longitude
??
this
.
longitude
,
notes:
notes
??
this
.
notes
,
createdAt:
createdAt
,
driverId:
driverId
??
this
.
driverId
,
status:
status
??
this
.
status
,
);
}
lib/models/user_model.dart
0 → 100644
View file @
a2d2df27
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'
],
);
}
}
lib/screens/admin_dashboard_screen.dart
View file @
a2d2df27
...
...
@@ -3,13 +3,15 @@ import 'package:csv/csv.dart';
import
'package:file_picker/file_picker.dart'
;
import
'package:firebase_auth/firebase_auth.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_svg/flutter_svg.dart'
;
import
'../models/delivery_address.dart'
;
import
'../
services/google_auth_service
.dart'
;
import
'../
models/user_model
.dart'
;
import
'../services/firestore_service.dart'
;
import
'../widgets/address_list.dart'
;
import
'../widgets/add_edit_address_dialog.dart'
;
import
'../widgets/assign_drivers_dialog.dart'
;
import
'../widgets/drivers_list.dart'
;
import
'../widgets/users_list.dart'
;
class
AdminDashboardScreen
extends
StatefulWidget
{
const
AdminDashboardScreen
({
super
.
key
});
...
...
@@ -20,10 +22,7 @@ class AdminDashboardScreen extends StatefulWidget {
class
_AdminDashboardScreenState
extends
State
<
AdminDashboardScreen
>
{
final
FirestoreService
_firestoreService
=
FirestoreService
();
final
TextEditingController
_emailController
=
TextEditingController
();
final
TextEditingController
_passwordController
=
TextEditingController
();
final
_formKey
=
GlobalKey
<
FormState
>();
bool
_isLoading
=
false
;
Set
<
String
>
_selectedAddressIds
=
{};
User
?
_user
;
@override
...
...
@@ -35,10 +34,20 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
setState
(()
{
_user
=
user
;
});
if
(
user
==
null
)
{
Navigator
.
of
(
context
).
pushReplacementNamed
(
'/login'
);
}
}
});
}
void
_onSelectionChanged
(
Set
<
String
>
selectedIds
)
{
setState
(()
{
_selectedAddressIds
=
selectedIds
;
});
}
void
_showAddEditAddressDialog
({
DeliveryAddress
?
address
})
{
if
(
_user
==
null
)
return
;
showDialog
(
...
...
@@ -72,26 +81,30 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
final
list
=
const
CsvToListConverter
().
convert
(
content
);
if
(
list
.
isNotEmpty
)
{
list
.
removeAt
(
0
);
// remove header
list
.
removeAt
(
0
);
}
final
addresses
=
list
.
map
((
row
)
{
try
{
return
DeliveryAddress
(
userId:
_user
!.
uid
,
streetAddress:
row
[
0
].
toString
(),
city:
row
[
1
].
toString
(),
state:
row
[
2
].
toString
(),
zipCode:
row
[
3
].
toString
(),
notes:
row
.
length
>
4
?
row
[
4
].
toString
()
:
null
,
);
}
catch
(
e
)
{
print
(
'Error parsing row:
$row
, error:
$e
'
);
return
null
;
}
}).
where
((
address
)
=>
address
!=
null
).
cast
<
DeliveryAddress
>().
toList
();
final
addresses
=
list
.
map
((
row
)
{
try
{
return
DeliveryAddress
(
userId:
_user
!.
uid
,
streetAddress:
row
[
0
].
toString
(),
city:
row
[
1
].
toString
(),
state:
row
[
2
].
toString
(),
zipCode:
row
[
3
].
toString
(),
notes:
row
.
length
>
4
?
row
[
4
].
toString
()
:
null
,
);
}
catch
(
e
)
{
print
(
'Error parsing row:
$row
, error:
$e
'
);
return
null
;
}
})
.
where
((
address
)
=>
address
!=
null
)
.
cast
<
DeliveryAddress
>()
.
toList
();
if
(!
mounted
)
return
;
// Check if the widget is still in the tree
if
(!
mounted
)
return
;
if
(
addresses
.
isNotEmpty
)
{
try
{
...
...
@@ -126,49 +139,48 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
void
_deleteAddress
(
String
addressId
)
{
_firestoreService
.
deleteAddress
(
addressId
);
setState
(()
{
_selectedAddressIds
.
remove
(
addressId
);
});
}
Future
<
void
>
_loginWithEmail
()
async
{
if
(!
_formKey
.
currentState
!.
validate
())
return
;
void
_reassignAddress
(
String
addressId
)
{
_firestoreService
.
reassignAddress
(
addressId
);
}
setState
(()
=>
_isLoading
=
true
);
void
_logout
()
async
{
await
FirebaseAuth
.
instance
.
signOut
();
}
try
{
await
FirebaseAuth
.
instance
.
signInWithEmailAndPassword
(
email:
_emailController
.
text
.
trim
(),
password:
_passwordController
.
text
.
trim
(),
);
}
on
FirebaseAuthException
catch
(
e
)
{
if
(!
mounted
)
return
;
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
"Login Failed:
${e.message}
"
)),
);
}
finally
{
if
(
mounted
)
{
setState
(()
=>
_isLoading
=
false
);
}
}
void
_assignDriverRole
(
String
uid
)
{
_firestoreService
.
assignDriverRole
(
uid
);
}
Future
<
void
>
_loginWithGoogle
()
async
{
setState
(()
=>
_isLoading
=
true
);
try
{
await
GoogleAuthService
.
signInWithGoogle
();
}
catch
(
e
)
{
if
(
mounted
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
"Google Login Failed:
${e.toString()}
"
)),
);
}
}
finally
{
if
(
mounted
)
{
setState
(()
=>
_isLoading
=
false
);
}
}
void
_removeDriverRole
(
String
uid
)
{
_firestoreService
.
removeDriverRole
(
uid
);
}
void
_logout
()
async
{
await
FirebaseAuth
.
instance
.
signOut
();
void
_showAssignDriversDialog
()
async
{
if
(
_user
==
null
||
_selectedAddressIds
.
isEmpty
)
return
;
final
drivers
=
await
_firestoreService
.
getDrivers
().
first
;
if
(!
mounted
)
return
;
showDialog
(
context:
context
,
builder:
(
context
)
=>
AssignDriversDialog
(
drivers:
drivers
,
onAssign:
(
selectedDriverIds
)
{
_firestoreService
.
assignAddressesToDrivers
(
_selectedAddressIds
.
toList
(),
selectedDriverIds
,
);
setState
(()
{
_selectedAddressIds
.
clear
();
});
},
),
);
}
@override
...
...
@@ -188,15 +200,13 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
child:
Center
(
child:
Text
(
_user
!.
email
??
''
,
style:
const
TextStyle
(
color:
Colors
.
white
))),
),
if
(
_user
!=
null
)
TextButton
.
ic
on
(
IconButt
on
(
onPressed:
_logout
,
icon:
const
Icon
(
Icons
.
logout
,
color:
Colors
.
white
,
size:
18
),
label:
const
Text
(
'Logout'
,
style:
TextStyle
(
color:
Colors
.
white
)),
style:
TextButton
.
styleFrom
(
foregroundColor:
Colors
.
white
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16.0
)),
),
],
),
body:
_user
==
null
?
_buildLoginForm
(
context
)
:
_buildLoggedInView
(
context
),
body:
_user
==
null
?
const
Center
(
child:
CircularProgressIndicator
()
)
:
_buildLoggedInView
(
context
),
);
}
...
...
@@ -206,179 +216,98 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
}
return
Padding
(
padding:
const
EdgeInsets
.
all
(
16.0
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
child:
Row
(
children:
[
Row
(
children:
[
ElevatedButton
.
icon
(
onPressed:
()
=>
_showAddEditAddressDialog
(),
icon:
const
Icon
(
Icons
.
add
),
label:
const
Text
(
'Add Address'
),
style:
ElevatedButton
.
styleFrom
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
24
,
vertical:
16
)
),
),
const
SizedBox
(
width:
16
),
OutlinedButton
.
icon
(
onPressed:
_showUploadCsvDialog
,
icon:
const
Icon
(
Icons
.
upload_file
),
label:
const
Text
(
'Upload CSV'
),
style:
OutlinedButton
.
styleFrom
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
24
,
vertical:
16
)
),
),
],
),
const
SizedBox
(
height:
24
),
Text
(
'List of Addresses'
,
style:
Theme
.
of
(
context
).
textTheme
.
headlineSmall
,
),
Expanded
(
child:
AddressList
(
onEdit:
(
address
)
=>
_showAddEditAddressDialog
(
address:
address
),
onDelete:
_deleteAddress
,
addressesStream:
_firestoreService
.
getAddresses
(
_user
!.
uid
),
),
),
const
SizedBox
(
height:
16
),
SizedBox
(
width:
double
.
infinity
,
child:
ElevatedButton
(
onPressed:
()
{},
style:
ElevatedButton
.
styleFrom
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
20
),
backgroundColor:
Colors
.
green
,
foregroundColor:
Colors
.
white
),
child:
const
Text
(
'Release to Drivers'
),
),
),
],
),
);
}
Widget
_buildLoginForm
(
BuildContext
context
)
{
final
ThemeData
currentTheme
=
Theme
.
of
(
context
);
final
bool
darkMode
=
currentTheme
.
brightness
==
Brightness
.
dark
;
final
Color
welcomeTextColor
=
darkMode
?
Colors
.
white
:
Colors
.
black87
;
final
Color
sloganTextColor
=
darkMode
?
Colors
.
grey
[
300
]!
:
Colors
.
black54
;
final
Color
iconColor
=
darkMode
?
Colors
.
white
:
const
Color
(
0xFF0D2B0D
);
return
Center
(
child:
SingleChildScrollView
(
padding:
const
EdgeInsets
.
all
(
32.0
),
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
<
Widget
>[
Icon
(
Icons
.
account_tree
,
size:
100
,
color:
iconColor
,
),
const
SizedBox
(
height:
20
),
Text
(
'Welcome to GraphGo'
,
style:
currentTheme
.
textTheme
.
headlineMedium
?.
copyWith
(
fontSize:
(
currentTheme
.
textTheme
.
headlineMedium
?.
fontSize
??
28
)
*
1.15
,
fontWeight:
FontWeight
.
bold
,
color:
welcomeTextColor
,
),
),
const
SizedBox
(
height:
10
),
Text
(
'Admin Panel Access'
,
style:
currentTheme
.
textTheme
.
bodyLarge
?.
copyWith
(
fontSize:
(
currentTheme
.
textTheme
.
bodyLarge
?.
fontSize
??
16
)
*
1.1
,
color:
sloganTextColor
,
),
textAlign:
TextAlign
.
center
,
),
const
SizedBox
(
height:
50
),
ConstrainedBox
(
constraints:
const
BoxConstraints
(
maxWidth:
400
),
child:
Column
(
children:
[
SizedBox
(
width:
double
.
infinity
,
child:
OutlinedButton
.
icon
(
onPressed:
_isLoading
?
null
:
_loginWithGoogle
,
icon:
SvgPicture
.
asset
(
'assets/icons/google_icon.svg'
,
width:
20
,
height:
20
),
label:
const
Text
(
'Sign in with Google'
),
flex:
3
,
// 75% of the space
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Wrap
(
spacing:
16.0
,
runSpacing:
8.0
,
children:
[
ElevatedButton
.
icon
(
onPressed:
()
=>
_showAddEditAddressDialog
(),
icon:
const
Icon
(
Icons
.
add
),
label:
const
Text
(
'Add Address'
),
style:
ElevatedButton
.
styleFrom
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
24
,
vertical:
16
),
),
),
OutlinedButton
.
icon
(
onPressed:
_showUploadCsvDialog
,
icon:
const
Icon
(
Icons
.
upload_file
),
label:
const
Text
(
'Upload CSV'
),
style:
OutlinedButton
.
styleFrom
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
12
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
24
,
vertical:
16
),
),
),
),
const
SizedBox
(
height:
20
),
const
Row
(
children:
[
Expanded
(
child:
Divider
()),
Padding
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
16
)
,
child:
Text
(
'OR'
)
,
ElevatedButton
.
icon
(
onPressed:
()
=>
Navigator
.
of
(
context
).
pushNamed
(
'/assigned-addresses'
),
icon:
const
Icon
(
Icons
.
assignment_turned_in
),
label:
const
Text
(
'View Assigned Addresses'
),
style:
ElevatedButton
.
styleFrom
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
24
,
vertical:
16
),
backgroundColor:
Colors
.
indigo
,
foregroundColor:
Colors
.
white
,
),
Expanded
(
child:
Divider
()),
],
),
const
SizedBox
(
height:
20
),
Form
(
key:
_formKey
,
child:
Column
(
children:
[
TextFormField
(
controller:
_emailController
,
decoration:
const
InputDecoration
(
labelText:
'Email'
,
border:
OutlineInputBorder
()),
keyboardType:
TextInputType
.
emailAddress
,
validator:
(
value
)
=>
value
!.
isEmpty
?
"Enter your email"
:
null
,
),
const
SizedBox
(
height:
16
),
TextFormField
(
controller:
_passwordController
,
decoration:
const
InputDecoration
(
labelText:
'Password'
,
border:
OutlineInputBorder
()),
obscureText:
true
,
validator:
(
value
)
=>
value
!.
isEmpty
?
"Enter your password"
:
null
,
),
Align
(
alignment:
Alignment
.
centerRight
,
child:
TextButton
(
onPressed:
()
{
Navigator
.
of
(
context
).
pushNamed
(
'/forgot'
);
},
child:
const
Text
(
'Forgot Password?'
),
),
),
const
SizedBox
(
height:
10
),
_isLoading
?
const
CircularProgressIndicator
()
:
SizedBox
(
width:
double
.
infinity
,
child:
ElevatedButton
(
onPressed:
_loginWithEmail
,
style:
ElevatedButton
.
styleFrom
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
20
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
8
)),
),
child:
const
Text
(
'Login with Email'
),
),
),
],
),
if
(
_selectedAddressIds
.
isNotEmpty
)
ElevatedButton
.
icon
(
onPressed:
_showAssignDriversDialog
,
icon:
const
Icon
(
Icons
.
assignment_ind
),
label:
Text
(
'Assign Selected (
${_selectedAddressIds.length}
)'
),
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
Colors
.
green
,
foregroundColor:
Colors
.
white
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
24
,
vertical:
16
),
),
),
],
),
const
SizedBox
(
height:
24
),
Text
(
'List of Addresses'
,
style:
Theme
.
of
(
context
).
textTheme
.
headlineSmall
,
),
Expanded
(
child:
AddressList
(
onEdit:
(
address
)
=>
_showAddEditAddressDialog
(
address:
address
),
onDelete:
_deleteAddress
,
onReassign:
_reassignAddress
,
addressesStream:
_firestoreService
.
getAddresses
(
_user
!.
uid
),
onSelectionChanged:
_onSelectionChanged
,
),
const
SizedBox
(
height:
20
),
TextButton
(
onPressed:
()
{
Navigator
.
of
(
context
).
pushNamed
(
'/signup'
);
},
child:
const
Text
(
"Don't have an account? Sign Up"
),
),
],
),
),
const
VerticalDivider
(
width:
32
),
Expanded
(
flex:
1
,
// 25% of the space
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'Users'
,
style:
Theme
.
of
(
context
).
textTheme
.
headlineSmall
),
Expanded
(
child:
UsersList
(
usersStream:
_firestoreService
.
getUsers
(),
onAssignDriver:
_assignDriverRole
,
),
),
const
SizedBox
(
height:
16
),
Text
(
'Drivers'
,
style:
Theme
.
of
(
context
).
textTheme
.
headlineSmall
),
Expanded
(
child:
DriversList
(
driversStream:
_firestoreService
.
getDrivers
(),
onRemoveDriver:
_removeDriverRole
,
),
]
,
)
,
)
,
]
,
),
]
,
)
,
)
,
]
,
),
);
}
...
...
lib/screens/assigned_addresses_screen.dart
0 → 100644
View file @
a2d2df27
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
'
),
],
);
},
),
),
);
},
);
},
),
);
}
}
lib/screens/driver_assignments_screen.dart
0 → 100644
View file @
a2d2df27
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'
),
),
],
),
),
);
},
);
},
),
);
}
}
lib/screens/login.dart
View file @
a2d2df27
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:firebase_auth/firebase_auth.dart'
;
import
'package:flutter_svg/flutter_svg.dart'
;
import
'package:provider/provider.dart'
;
import
'../providers/settings_provider.dart'
;
import
'../services/google_auth_service.dart'
;
import
'../colors.dart'
;
class
LoginPage
extends
StatefulWidget
{
const
LoginPage
({
super
.
key
});
...
...
@@ -18,160 +19,220 @@ class _LoginPageState extends State<LoginPage> {
final
TextEditingController
_passwordController
=
TextEditingController
();
final
_formKey
=
GlobalKey
<
FormState
>();
bool
_isLoading
=
false
;
bool
_rememberMe
=
false
;
String
_selectedRole
=
'Admin'
;
// Default role
void
_navigateBasedOnRole
(
String
role
)
{
if
(!
mounted
)
return
;
switch
(
role
)
{
case
'Admin'
:
Navigator
.
of
(
context
).
pushReplacementNamed
(
'/admin-dashboard'
);
break
;
case
'Driver'
:
default
:
Navigator
.
of
(
context
).
pushReplacementNamed
(
'/map'
);
break
;
}
}
Future
<
void
>
_login
()
async
{
if
(!
_formKey
.
currentState
!.
validate
())
return
;
setState
(()
=>
_isLoading
=
true
);
final
settingsProvider
=
Provider
.
of
<
SettingsProvider
>(
context
,
listen:
false
);
try
{
await
FirebaseAuth
.
instance
.
signInWithEmailAndPassword
(
email:
_emailController
.
text
.
trim
(),
password:
_passwordController
.
text
.
trim
(),
);
if
(
_rememberMe
)
{
settingsProvider
.
login
();
}
if
(
mounted
)
{
Navigator
.
of
(
context
).
pushReplacementNamed
(
'/map'
);
}
_navigateBasedOnRole
(
_selectedRole
);
}
on
FirebaseAuthException
catch
(
e
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
"Login Failed:
${e.message}
"
)),
);
}
catch
(
e
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
"Login Failed:
${e.toString()}
"
)),
);
}
finally
{
if
(
mounted
)
{
setState
(()
=>
_isLoading
=
false
);
}
if
(
mounted
)
setState
(()
=>
_isLoading
=
false
);
}
}
Future
<
void
>
_loginWithGoogle
()
async
{
setState
(()
=>
_isLoading
=
true
);
final
settingsProvider
=
Provider
.
of
<
SettingsProvider
>(
context
,
listen:
false
);
try
{
final
UserCredential
?
userCredential
=
await
GoogleAuthService
.
signInWithGoogle
();
if
(
FirebaseAuth
.
instance
.
currentUser
!=
null
)
{
settingsProvider
.
login
();
if
(
mounted
)
{
Navigator
.
of
(
context
).
pushReplacementNamed
(
'/map'
);
}
_navigateBasedOnRole
(
_selectedRole
);
}
else
if
(
userCredential
==
null
&&
mounted
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
"Google Sign-In was cancelled or failed."
)),
);
}
}
catch
(
e
)
{
if
(
mounted
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
"Google Login Failed:
${e.toString()}
"
)),
);
}
}
finally
{
if
(
mounted
)
{
setState
(()
=>
_isLoading
=
false
);
if
(
mounted
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
"Google Login Failed:
${e.toString()}
"
)),
);
}
}
finally
{
if
(
mounted
)
setState
(()
=>
_isLoading
=
false
);
}
}
@override
@override
Widget
build
(
BuildContext
context
)
{
final
ThemeData
currentTheme
=
Theme
.
of
(
context
);
final
bool
darkMode
=
currentTheme
.
brightness
==
Brightness
.
dark
;
final
Color
welcomeTextColor
=
darkMode
?
Colors
.
white
:
Colors
.
black87
;
final
Color
sloganTextColor
=
darkMode
?
Colors
.
grey
[
300
]!
:
Colors
.
black54
;
final
Color
iconColor
=
darkMode
?
Colors
.
white
:
const
Color
(
0xFF0D2B0D
);
return
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'GraphGo Login'
),
leading:
IconButton
(
icon:
const
Icon
(
Icons
.
arrow_back
),
onPressed:
()
=>
Navigator
.
of
(
context
).
pop
(),
tooltip:
'Back to Home'
,
icon:
const
Icon
(
Icons
.
settings
,
color:
Colors
.
white
),
onPressed:
()
=>
Navigator
.
of
(
context
).
pushNamed
(
'/settings'
),
),
title:
const
Text
(
'GraphGo'
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
color:
Colors
.
white
)),
centerTitle:
true
,
),
body:
SingleChildScrollView
(
padding:
const
EdgeInsets
.
all
(
16.0
),
child:
Form
(
key:
_formKey
,
child:
Column
(
children:
[
TextFormField
(
controller:
_emailController
,
decoration:
const
InputDecoration
(
labelText:
'Email'
),
keyboardType:
TextInputType
.
emailAddress
,
validator:
(
value
)
=>
value
!.
isEmpty
?
"Enter your email"
:
null
,
),
TextFormField
(
controller:
_passwordController
,
decoration:
const
InputDecoration
(
labelText:
'Password'
),
obscureText:
true
,
validator:
(
value
)
=>
value
!.
isEmpty
?
"Enter your password"
:
null
,
),
Align
(
alignment:
Alignment
.
centerLeft
,
child:
Row
(
children:
[
Checkbox
(
value:
_rememberMe
,
onChanged:
(
val
)
{
setState
(()
=>
_rememberMe
=
val
??
false
);
},
),
const
Text
(
"Remember Me"
),
],
),
),
TextButton
(
onPressed:
()
{
Navigator
.
of
(
context
).
pushNamed
(
'/forgot'
);
},
child:
const
Text
(
"Forgot Password?"
),
),
const
SizedBox
(
height:
20
),
SizedBox
(
width:
double
.
infinity
,
child:
OutlinedButton
.
icon
(
onPressed:
_isLoading
?
null
:
_loginWithGoogle
,
icon:
const
Icon
(
Icons
.
login
,
size:
20
,
color:
Colors
.
blue
),
label:
const
Text
(
'Sign in with Google'
),
),
),
const
SizedBox
(
height:
16
),
Row
(
body:
Center
(
child:
SingleChildScrollView
(
padding:
const
EdgeInsets
.
all
(
32.0
),
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
<
Widget
>[
Icon
(
Icons
.
account_tree
,
size:
100
,
color:
iconColor
,
),
const
SizedBox
(
height:
20
),
Text
(
'Welcome to GraphGo'
,
style:
currentTheme
.
textTheme
.
headlineMedium
?.
copyWith
(
fontSize:
(
currentTheme
.
textTheme
.
headlineMedium
?.
fontSize
??
28
)
*
1.15
,
fontWeight:
FontWeight
.
bold
,
color:
welcomeTextColor
,
),
),
const
SizedBox
(
height:
10
),
Text
(
'Log in as Admin or Driver to start exploring'
,
style:
currentTheme
.
textTheme
.
bodyLarge
?.
copyWith
(
fontSize:
16
,
color:
sloganTextColor
,
),
textAlign:
TextAlign
.
center
,
),
const
SizedBox
(
height:
50
),
ConstrainedBox
(
constraints:
const
BoxConstraints
(
maxWidth:
400
),
child:
Column
(
children:
[
const
Expanded
(
child:
Divider
()),
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
child:
Text
(
'OR'
),
SizedBox
(
width:
double
.
infinity
,
child:
OutlinedButton
.
icon
(
onPressed:
_isLoading
?
null
:
_loginWithGoogle
,
icon:
SvgPicture
.
asset
(
'assets/icons/google_icon.svg'
,
width:
20
,
height:
20
),
label:
const
Text
(
'Sign in with Google'
),
style:
OutlinedButton
.
styleFrom
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
12
),
),
),
),
const
SizedBox
(
height:
20
),
const
Row
(
children:
[
Expanded
(
child:
Divider
()),
Padding
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
16
),
child:
Text
(
'OR'
),
),
Expanded
(
child:
Divider
()),
],
),
const
SizedBox
(
height:
20
),
Form
(
key:
_formKey
,
child:
Column
(
children:
[
DropdownButtonFormField
<
String
>(
value:
_selectedRole
,
decoration:
const
InputDecoration
(
labelText:
'Role'
,
border:
OutlineInputBorder
(),
),
items:
[
'Admin'
,
'Driver'
]
.
map
<
DropdownMenuItem
<
String
>>((
String
value
)
{
return
DropdownMenuItem
<
String
>(
value:
value
,
child:
Text
(
value
),
);
}).
toList
(),
onChanged:
(
String
?
newValue
)
{
if
(
newValue
!=
null
)
{
setState
(()
=>
_selectedRole
=
newValue
);
}
},
),
const
SizedBox
(
height:
16
),
TextFormField
(
controller:
_emailController
,
decoration:
const
InputDecoration
(
labelText:
'Email'
,
border:
OutlineInputBorder
()),
keyboardType:
TextInputType
.
emailAddress
,
validator:
(
value
)
=>
value
!.
isEmpty
?
"Enter your email"
:
null
,
),
const
SizedBox
(
height:
16
),
TextFormField
(
controller:
_passwordController
,
decoration:
const
InputDecoration
(
labelText:
'Password'
,
border:
OutlineInputBorder
()),
obscureText:
true
,
validator:
(
value
)
=>
value
!.
isEmpty
?
"Enter your password"
:
null
,
),
Align
(
alignment:
Alignment
.
centerRight
,
child:
TextButton
(
onPressed:
()
{
Navigator
.
of
(
context
).
pushNamed
(
'/forgot'
);
},
child:
const
Text
(
'Forgot Password?'
),
),
),
const
SizedBox
(
height:
10
),
_isLoading
?
const
CircularProgressIndicator
()
:
SizedBox
(
width:
double
.
infinity
,
child:
ElevatedButton
(
onPressed:
_login
,
style:
ElevatedButton
.
styleFrom
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
20
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
8
)),
),
child:
const
Text
(
'Login with Email'
),
),
),
],
),
),
const
SizedBox
(
height:
20
),
TextButton
(
onPressed:
()
{
Navigator
.
of
(
context
).
pushNamed
(
'/signup'
);
},
child:
const
Text
(
"Don't have an account? Sign Up"
),
),
const
Expanded
(
child:
Divider
()),
],
),
const
SizedBox
(
height:
16
),
_isLoading
?
const
CircularProgressIndicator
()
:
SizedBox
(
width:
double
.
infinity
,
child:
ElevatedButton
(
onPressed:
_login
,
child:
const
Text
(
'Login with Email'
),
),
),
TextButton
(
onPressed:
()
=>
Navigator
.
of
(
context
).
pushNamed
(
'/signup'
),
child:
const
Text
(
"Don't have an account? Sign Up"
),
),
// The debug override button has been removed.
],
),
),
],
),
),
),
);
}
}
lib/screens/map_screen.dart
View file @
a2d2df27
...
...
@@ -3,6 +3,11 @@ import 'package:flutter/material.dart';
import
'package:google_maps_flutter/google_maps_flutter.dart'
;
import
'package:location/location.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
{
const
MapScreen
({
super
.
key
});
...
...
@@ -13,8 +18,10 @@ class MapScreen extends StatefulWidget {
class
_MapScreenState
extends
State
<
MapScreen
>
{
final
Completer
<
GoogleMapController
>
_controller
=
Completer
();
final
FirestoreService
_firestoreService
=
FirestoreService
();
LocationData
?
_currentLocation
;
StreamSubscription
<
LocationData
>?
_locationSubscription
;
Set
<
Marker
>
_markers
=
{};
final
User
?
user
=
FirebaseAuth
.
instance
.
currentUser
;
...
...
@@ -26,29 +33,28 @@ class _MapScreenState extends State<MapScreen> {
@override
void
initState
()
{
super
.
initState
();
_initializeLocation
();
_initializeLocationAndMarkers
();
}
Future
<
void
>
_initializeLocationAndMarkers
()
async
{
await
_initializeLocation
();
if
(
user
!=
null
)
{
_loadAddressMarkers
(
user
!.
uid
);
}
}
Future
<
void
>
_initializeLocation
()
async
{
Location
location
=
Location
();
bool
serviceEnabled
;
PermissionStatus
permissionGranted
;
serviceEnabled
=
await
location
.
serviceEnabled
();
bool
serviceEnabled
=
await
location
.
serviceEnabled
();
if
(!
serviceEnabled
)
{
serviceEnabled
=
await
location
.
requestService
();
if
(!
serviceEnabled
)
{
return
;
}
if
(!
serviceEnabled
)
return
;
}
permissionGranted
=
await
location
.
hasPermission
();
PermissionStatus
permissionGranted
=
await
location
.
hasPermission
();
if
(
permissionGranted
==
PermissionStatus
.
denied
)
{
permissionGranted
=
await
location
.
requestPermission
();
if
(
permissionGranted
!=
PermissionStatus
.
granted
)
{
return
;
}
if
(
permissionGranted
!=
PermissionStatus
.
granted
)
return
;
}
_currentLocation
=
await
location
.
getLocation
();
...
...
@@ -57,11 +63,43 @@ class _MapScreenState extends State<MapScreen> {
}
_locationSubscription
=
location
.
onLocationChanged
.
listen
((
LocationData
newLocation
)
{
if
(
mounted
)
{
setState
(()
{
_currentLocation
=
newLocation
;
});
_moveCameraToLocation
(
newLocation
);
if
(
mounted
)
{
setState
(()
=>
_currentLocation
=
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
);
}
});
}
...
...
@@ -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
void
dispose
()
{
_locationSubscription
?.
cancel
();
...
...
@@ -86,24 +131,25 @@ class _MapScreenState extends State<MapScreen> {
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'Your Location'
),
leading:
IconButton
(
icon:
const
Icon
(
Icons
.
arrow_back
),
onPressed:
()
=>
Navigator
.
of
(
context
).
pop
(),
),
title:
const
Text
(
"GraphGo Driver"
),
automaticallyImplyLeading:
false
,
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
)
Padding
(
padding:
const
EdgeInsets
.
only
(
right:
16.0
),
child:
Center
(
child:
Text
(
user
!.
email
!,
style:
const
TextStyle
(
fontSize:
12
,
),
),
),
child:
Center
(
child:
Text
(
user
!.
email
!,
style:
const
TextStyle
(
fontSize:
12
))),
),
IconButton
(
icon:
const
Icon
(
Icons
.
logout
),
onPressed:
_logout
,
),
],
),
body:
_currentLocation
==
null
...
...
@@ -116,14 +162,8 @@ class _MapScreenState extends State<MapScreen> {
},
myLocationEnabled:
true
,
myLocationButtonEnabled:
true
,
markers:
{
if
(
_currentLocation
!=
null
)
Marker
(
markerId:
const
MarkerId
(
'currentLocation'
),
position:
LatLng
(
_currentLocation
!.
latitude
!,
_currentLocation
!.
longitude
!),
infoWindow:
const
InfoWindow
(
title:
'My Location'
),
),
},
markers:
_markers
,
),
);
}
...
...
lib/screens/signup.dart
View file @
a2d2df27
import
'package:flutter/material.dart'
;
import
'package:firebase_auth/firebase_auth.dart'
;
import
'package:cloud_firestore/cloud_firestore.dart'
;
import
'../services/google_auth_service.dart'
;
import
'../colors.dart'
;
class
SignupPage
extends
StatefulWidget
{
const
SignupPage
({
super
.
key
});
...
...
@@ -13,19 +11,27 @@ class SignupPage extends StatefulWidget {
class
_SignupPageState
extends
State
<
SignupPage
>
{
final
_formKey
=
GlobalKey
<
FormState
>();
final
TextEditingController
_firstNameController
=
TextEditingController
();
final
TextEditingController
_lastNameController
=
TextEditingController
();
final
TextEditingController
_emailController
=
TextEditingController
();
final
TextEditingController
_passwordController
=
TextEditingController
();
final
TextEditingController
_confirmPasswordController
=
TextEditingController
();
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
)
{
if
(
value
==
null
||
value
.
isEmpty
)
return
"Password is required."
;
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'\d'
).
hasMatch
(
value
))
return
"Must contain 1 number."
;
if
(!
RegExp
(
r'[!@#\
$%^&*(),.?\
":{}|<>]'
).
hasMatch
(
value
))
return
"Must contain 1 special character."
;
if
(!
RegExp
(
r'\
\
d'
).
hasMatch
(
value
))
return
"Must contain 1 number."
;
if
(!
RegExp
(
r'[!@#\
\\$%^&*(),.?
":{}|<>]'
).
hasMatch
(
value
))
return
"Must contain 1 special character."
;
return
null
;
}
...
...
@@ -41,45 +47,30 @@ class _SignupPageState extends State<SignupPage> {
);
await
FirebaseFirestore
.
instance
.
collection
(
'users'
).
doc
(
userCredential
.
user
!.
uid
).
set
({
'first_name'
:
_firstNameController
.
text
.
trim
(),
'last_name'
:
_lastNameController
.
text
.
trim
(),
'email'
:
_emailController
.
text
.
trim
(),
'provider'
:
'email'
,
'created_at'
:
Timestamp
.
now
(),
});
if
(
mounted
)
{
// After signing up, go to the map screen
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
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
"
Signup Failed:
${e.toString()}
"
)),
SnackBar
(
content:
Text
(
"
An unexpected error occurred:
\
${e.toString()}
"
)),
);
}
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
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
"Google Sign-Up Failed:
${e.toString()}
"
)),
);
}
}
finally
{
if
(
mounted
)
{
setState
(()
=>
_isLoading
=
false
);
}
}
...
...
@@ -87,92 +78,68 @@ class _SignupPageState extends State<SignupPage> {
@override
Widget
build
(
BuildContext
context
)
{
final
bool
isDarkMode
=
Theme
.
of
(
context
).
brightness
==
Brightness
.
dark
;
return
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'GraphGo Sign Up'
),
// The back button is now handled correctly by the Navigator
title:
const
Text
(
'Create Account'
),
leading:
IconButton
(
icon:
const
Icon
(
Icons
.
arrow_back
),
onPressed:
()
=>
Navigator
.
of
(
context
).
pop
(),
// Corrected line
onPressed:
()
=>
Navigator
.
of
(
context
).
pop
(),
tooltip:
'Back to Login'
,
),
),
body:
SingleChildScrollView
(
padding:
const
EdgeInsets
.
all
(
16.0
),
child:
Form
(
key:
_formKey
,
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
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
(
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
(
body:
Center
(
child:
SingleChildScrollView
(
padding:
const
EdgeInsets
.
all
(
32.0
),
child:
ConstrainedBox
(
constraints:
const
BoxConstraints
(
maxWidth:
400
),
child:
Form
(
key:
_formKey
,
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
const
Expanded
(
child:
Divider
()),
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
child:
Text
(
'OR'
),
TextFormField
(
controller:
_emailController
,
decoration:
const
InputDecoration
(
labelText:
'Username (Email)'
,
border:
OutlineInputBorder
(),
),
keyboardType:
TextInputType
.
emailAddress
,
validator:
_validateEmail
,
),
const
Expanded
(
child:
Divider
()),
],
),
const
SizedBox
(
height:
16
),
_isLoading
?
const
CircularProgressIndicator
()
:
SizedBox
(
width:
double
.
infinity
,
child:
ElevatedButton
(
onPressed:
_signUp
,
child:
const
Text
(
'Sign Up with Email'
),
const
SizedBox
(
height:
16
),
TextFormField
(
controller:
_passwordController
,
decoration:
InputDecoration
(
labelText:
'Password'
,
border:
const
OutlineInputBorder
(),
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:
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'
),
),
),
],
),
),
),
),
),
...
...
lib/services/firestore_service.dart
View file @
a2d2df27
import
'package:cloud_firestore/cloud_firestore.dart'
;
import
'../models/delivery_address.dart'
;
import
'../models/user_model.dart'
;
class
FirestoreService
{
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
Stream
<
List
<
DeliveryAddress
>>
getAddresses
(
String
userId
)
{
return
_db
.
collection
(
_
c
ollectionPath
)
.
collection
(
_
addressesC
ollectionPath
)
.
where
(
'userId'
,
isEqualTo:
userId
)
.
snapshots
()
.
map
((
snapshot
)
=>
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
Future
<
void
>
saveAddress
(
DeliveryAddress
address
)
{
return
_db
.
collection
(
_
c
ollectionPath
).
doc
(
address
.
id
).
set
(
address
.
toJson
());
return
_db
.
collection
(
_
addressesC
ollectionPath
).
doc
(
address
.
id
).
set
(
address
.
toJson
());
}
// Save a list of addresses from a CSV
Future
<
void
>
saveAddressesFromCsv
(
List
<
DeliveryAddress
>
addresses
)
async
{
final
batch
=
_db
.
batch
();
for
(
final
address
in
addresses
)
{
final
docRef
=
_db
.
collection
(
_
c
ollectionPath
).
doc
(
address
.
id
);
final
docRef
=
_db
.
collection
(
_
addressesC
ollectionPath
).
doc
(
address
.
id
);
batch
.
set
(
docRef
,
address
.
toJson
());
}
await
batch
.
commit
();
...
...
@@ -32,6 +46,125 @@ class FirestoreService {
// Delete an address
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'
,
});
}
}
lib/widgets/address_list.dart
View file @
a2d2df27
import
'package:flutter/material.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
Function
(
DeliveryAddress
)
onEdit
;
final
Function
(
String
)
onDelete
;
final
Function
(
String
)
onReassign
;
final
SelectionChangedCallback
onSelectionChanged
;
const
AddressList
({
super
.
key
,
required
this
.
addressesStream
,
required
this
.
onEdit
,
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
Widget
build
(
BuildContext
context
)
{
return
StreamBuilder
<
List
<
DeliveryAddress
>>(
stream:
addressesStream
,
stream:
widget
.
addressesStream
,
builder:
(
context
,
snapshot
)
{
if
(
snapshot
.
hasError
)
{
return
Center
(
child:
Text
(
'Error:
${snapshot.error}
'
));
...
...
@@ -28,34 +69,73 @@ class AddressList extends StatelessWidget {
return
const
Center
(
child:
Text
(
'No addresses found.'
));
}
final
addresses
=
snapshot
.
data
!;
return
ListView
.
builder
(
itemCount:
addresses
.
length
,
itemBuilder:
(
context
,
index
)
{
final
address
=
addresses
[
index
];
return
Card
(
margin:
const
EdgeInsets
.
symmetric
(
vertical:
8.0
,
horizontal:
4.0
),
child:
ListTile
(
leading:
CircleAvatar
(
child:
Text
(
'
${index + 1}
'
)),
title:
Text
(
address
.
fullAddress
),
subtitle:
const
Text
(
'Status: Pending'
),
trailing:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
IconButton
(
icon:
const
Icon
(
Icons
.
edit
,
color:
Colors
.
blue
),
onPressed:
()
=>
onEdit
(
address
),
),
IconButton
(
icon:
const
Icon
(
Icons
.
delete
,
color:
Colors
.
red
),
onPressed:
()
=>
onDelete
(
address
.
id
),
_currentAddresses
=
snapshot
.
data
!;
final
currentIds
=
_currentAddresses
.
map
((
e
)
=>
e
.
id
).
toSet
();
_selectedAddressIds
.
removeWhere
((
id
)
=>
!
currentIds
.
contains
(
id
));
return
Column
(
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
)
{
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
(
margin:
const
EdgeInsets
.
symmetric
(
vertical:
4.0
,
horizontal:
4.0
),
child:
ListTile
(
leading:
Checkbox
(
value:
isSelected
,
onChanged:
(
bool
?
value
)
{
if
(
value
!=
null
)
{
_handleAddressSelection
(
address
.
id
,
value
);
}
},
),
title:
Text
(
address
.
fullAddress
),
subtitle:
Text
(
'Status:
$capitalizedStatus
'
),
trailing:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
if
(
address
.
status
==
'denied'
)
TextButton
(
onPressed:
()
=>
widget
.
onReassign
(
address
.
id
),
child:
const
Text
(
'Reassign'
,
style:
TextStyle
(
color:
Colors
.
orange
)),
),
IconButton
(
icon:
const
Icon
(
Icons
.
edit
,
color:
Colors
.
blue
),
onPressed:
()
=>
widget
.
onEdit
(
address
),
),
IconButton
(
icon:
const
Icon
(
Icons
.
delete
,
color:
Colors
.
red
),
onPressed:
()
=>
widget
.
onDelete
(
address
.
id
),
),
],
),
),
],
)
,
);
}
,
),
)
;
}
,
)
,
]
,
);
},
);
...
...
lib/widgets/assign_address_dialog.dart
0 → 100644
View file @
a2d2df27
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'
),
),
],
);
}
}
lib/widgets/assign_drivers_dialog.dart
0 → 100644
View file @
a2d2df27
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'
),
),
],
);
}
}
lib/widgets/drivers_list.dart
0 → 100644
View file @
a2d2df27
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
),
),
),
);
},
);
},
);
}
}
lib/widgets/select_address_dialog.dart
0 → 100644
View file @
a2d2df27
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'
),
),
],
);
}
}
lib/widgets/users_list.dart
0 → 100644
View file @
a2d2df27
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
,
),
);
},
);
},
);
}
}
web/index.html
View file @
a2d2df27
...
...
@@ -33,8 +33,29 @@
<title>
graph_go
</title>
<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>
<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>
</body>
</html>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment