Unverified Commit 7bcbe2db authored by Ilham Maulana Pratama's avatar Ilham Maulana Pratama Committed by GitHub

Merge pull request #4 from impfundev/development

Development
parents 6d383880 a80e1707
...@@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart'; ...@@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:library_app/src/screens/profile_edit_screen.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:library_app/src/providers/auth_provider.dart'; import 'package:library_app/src/providers/auth_provider.dart';
import 'package:library_app/src/providers/navigations_provider.dart'; import 'package:library_app/src/providers/navigations_provider.dart';
...@@ -54,7 +55,11 @@ class _LibraryApp extends State<LibraryApp> { ...@@ -54,7 +55,11 @@ class _LibraryApp extends State<LibraryApp> {
GoRoute( GoRoute(
path: "/change-password", path: "/change-password",
builder: (context, state) => const ChangePasswordScreen(), builder: (context, state) => const ChangePasswordScreen(),
) ),
GoRoute(
path: "/profile-edit",
builder: (context, state) => const ProfileEditScreen(),
),
], ],
); );
......
...@@ -17,16 +17,4 @@ class Loan { ...@@ -17,16 +17,4 @@ class Loan {
this.remainingDays, this.remainingDays,
this.isOverdue, this.isOverdue,
); );
factory Loan.fromJson(Map<String, dynamic> data) {
final book = Book.fromJson(data["book_detail"]);
return Loan(
book,
null,
data["loan_date"],
data["due_date"],
data["remaining_loan_time"],
data["is_overdue"],
);
}
} }
class User { class User {
int id; int id;
int accountId;
String username; String username;
String email; String email;
String? firstName; String? firstName;
String? lastName; String? lastName;
bool isStaff; bool isStaff;
User(this.id, this.accountId, this.username, this.email, this.firstName, User(this.id, this.username, this.email, this.firstName, this.lastName,
this.lastName, this.isStaff); this.isStaff);
factory User.fromJson(Map<String, dynamic> data) { factory User.fromJson(Map<String, dynamic> data) {
return User( return User(
data['id'] as int, data['id'] as int,
data['account_id'] as int,
data['username'] as String, data['username'] as String,
data['email'] as String, data['email'] as String,
data['first_name'] as String?, data['first_name'] as String?,
...@@ -36,7 +34,6 @@ class User { ...@@ -36,7 +34,6 @@ class User {
final User initialUser = User( final User initialUser = User(
1, 1,
2,
"test_user", "test_user",
"test@email.com", "test@email.com",
"Test", "Test",
......
...@@ -10,7 +10,9 @@ import 'package:library_app/src/models/user.dart'; ...@@ -10,7 +10,9 @@ import 'package:library_app/src/models/user.dart';
class AuthProvider with ChangeNotifier { class AuthProvider with ChangeNotifier {
final storage = const FlutterSecureStorage(); final storage = const FlutterSecureStorage();
String baseUrl = 'http://localhost:8000/api/v1'; String baseUrl = 'http://localhost:8000/api/v1';
String? message;
User? user; User? user;
bool isAuthenticated = false; bool isAuthenticated = false;
...@@ -26,6 +28,11 @@ class AuthProvider with ChangeNotifier { ...@@ -26,6 +28,11 @@ class AuthProvider with ChangeNotifier {
bool changePasswordSucced = false; bool changePasswordSucced = false;
bool loanBookSuccess = false; bool loanBookSuccess = false;
bool hasNextPage = false;
bool hasPrevPage = false;
int pageNumber = 1;
int? totalPages;
List<dynamic>? loans; List<dynamic>? loans;
List<dynamic>? nearOutstandingLoans; List<dynamic>? nearOutstandingLoans;
List<dynamic>? overduedLoans; List<dynamic>? overduedLoans;
...@@ -47,6 +54,37 @@ class AuthProvider with ChangeNotifier { ...@@ -47,6 +54,37 @@ class AuthProvider with ChangeNotifier {
loanBookSuccess = value; loanBookSuccess = value;
} }
void setInvalidUsernameOrPassword(bool value) {
invalidUsernameOrPassword = value;
}
void resetAllState() {
user = null;
message = null;
memberLoans = null;
loans = null;
nearOutstandingLoans = null;
overduedLoans = null;
totalPages = null;
isAuthenticated = false;
invalidUsernameOrPassword = false;
filterByUpcoming = false;
filterByOverdued = false;
isLoading = false;
resetPasswordTokenSended = false;
resetPasswordSucced = false;
changePasswordSucced = false;
loanBookSuccess = false;
hasNextPage = false;
hasPrevPage = false;
}
void setPage(int value) {
pageNumber = value;
notifyListeners();
}
Future<void> signIn( Future<void> signIn(
BuildContext context, String username, String password) async { BuildContext context, String username, String password) async {
try { try {
...@@ -57,22 +95,20 @@ class AuthProvider with ChangeNotifier { ...@@ -57,22 +95,20 @@ class AuthProvider with ChangeNotifier {
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
if (response.statusCode == 200) { if (response.statusCode == 201) {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
String token = Token.fromJson(data)!.key; String token = Token.fromJson(data)!.key;
await storeAccessToken(token); await storeAccessToken(token);
isAuthenticated = true; isAuthenticated = true;
invalidUsernameOrPassword = false; setInvalidUsernameOrPassword(false);
debugPrint("Login successful $token"); debugPrint("Login successful $token");
} else if (response.statusCode == 401) { } else if (response.statusCode == 400) {
invalidUsernameOrPassword = true; setInvalidUsernameOrPassword(true);
debugPrint("Login failed: ${response.statusCode} ${response.body}"); debugPrint("Login failed: ${response.statusCode} ${response.body}");
} else { } else {
final code = response.statusCode; debugPrint("Login failed: ${response.statusCode} ${response.body}");
debugPrint("Login failed $code");
} }
setLoading(false); setLoading(false);
...@@ -98,19 +134,7 @@ class AuthProvider with ChangeNotifier { ...@@ -98,19 +134,7 @@ class AuthProvider with ChangeNotifier {
if (response.statusCode == 200) { if (response.statusCode == 200) {
await storage.delete(key: 'token'); await storage.delete(key: 'token');
isAuthenticated = false; resetAllState();
user = null;
filterByUpcoming = false;
filterByOverdued = false;
memberLoans = null;
isLoading = false;
resetPasswordTokenSended = false;
resetPasswordSucced = false;
loans = null;
nearOutstandingLoans = null;
overduedLoans = null;
} else { } else {
debugPrint("Logout failed: ${response.statusCode} ${response.body}"); debugPrint("Logout failed: ${response.statusCode} ${response.body}");
} }
...@@ -132,22 +156,25 @@ class AuthProvider with ChangeNotifier { ...@@ -132,22 +156,25 @@ class AuthProvider with ChangeNotifier {
"password": password, "password": password,
}; };
final response = await http.post( final response = await http.post(
Uri.parse('$baseUrl/members/auth/register'), Uri.parse('$baseUrl/auth/register'),
body: jsonEncode(body), body: jsonEncode(body),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
if (response.statusCode == 200) { if (response.statusCode == 201) {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
String token = Token.fromJson(data)!.key; String token = Token.fromJson(data)!.key;
storeAccessToken(token); storeAccessToken(token);
isAuthenticated = true; isAuthenticated = true;
message = null;
if (context.mounted) { if (context.mounted) {
context.go("/"); context.go("/");
} }
debugPrint("Signup successful $token"); debugPrint("Signup successful $token");
} else { } else {
final data = jsonDecode(response.body);
message = data["message"];
debugPrint( debugPrint(
"Error: sign up failed, ${response.statusCode}: ${response.body}"); "Error: sign up failed, ${response.statusCode}: ${response.body}");
} }
...@@ -169,15 +196,17 @@ class AuthProvider with ChangeNotifier { ...@@ -169,15 +196,17 @@ class AuthProvider with ChangeNotifier {
Uri.parse('$baseUrl/user'), Uri.parse('$baseUrl/user'),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer $token' 'Authorization': 'Bearer $token',
}, },
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
user = User.fromJson(data); user = User.fromJson(data);
message = null;
} else { } else {
debugPrint('Error fetching user details: ${response.statusCode}'); debugPrint(
'Error fetching user details: ${response.statusCode} ${response.body}');
} }
setLoading(false); setLoading(false);
...@@ -189,6 +218,7 @@ class AuthProvider with ChangeNotifier { ...@@ -189,6 +218,7 @@ class AuthProvider with ChangeNotifier {
} }
Future<void> updateUserDetail( Future<void> updateUserDetail(
BuildContext context,
int id, int id,
String username, String username,
String email, String email,
...@@ -201,14 +231,15 @@ class AuthProvider with ChangeNotifier { ...@@ -201,14 +231,15 @@ class AuthProvider with ChangeNotifier {
if (token != null) { if (token != null) {
try { try {
final body = jsonEncode({ final data = {
"username": username, "username": username,
"email": email, "email": email,
"first_name": firstName, "first_name": firstName,
"last_name": lastName, "last_name": lastName,
}); };
final body = jsonEncode(data);
final response = await http.put( final response = await http.put(
Uri.parse('$baseUrl/user/$id/'), Uri.parse('$baseUrl/user/update'),
body: body, body: body,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...@@ -217,9 +248,14 @@ class AuthProvider with ChangeNotifier { ...@@ -217,9 +248,14 @@ class AuthProvider with ChangeNotifier {
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = jsonDecode(response.body); await getUserDetail();
user = User.fromJson(data); message = null;
if (context.mounted) {
context.pop();
}
} else { } else {
final data = jsonDecode(response.body);
message = data["message"];
debugPrint( debugPrint(
'Error update user details: ${response.statusCode}, ${response.body}'); 'Error update user details: ${response.statusCode}, ${response.body}');
} }
...@@ -234,19 +270,20 @@ class AuthProvider with ChangeNotifier { ...@@ -234,19 +270,20 @@ class AuthProvider with ChangeNotifier {
Future<void> changePassword( Future<void> changePassword(
BuildContext context, BuildContext context,
int accountId,
String oldPassword, String oldPassword,
String newPassword, String newPassword1,
String newPassword2,
) async { ) async {
final token = await getAccessToken(); final token = await getAccessToken();
try { try {
setLoading(true); setLoading(true);
final response = await http.post( final response = await http.put(
Uri.parse('$baseUrl/members/$accountId/change-password'), Uri.parse('$baseUrl/auth/change-password'),
body: jsonEncode({ body: jsonEncode({
"old_password": oldPassword, "old_password": oldPassword,
"new_password": newPassword, "new_password1": newPassword1,
"new_password2": newPassword2,
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...@@ -255,11 +292,14 @@ class AuthProvider with ChangeNotifier { ...@@ -255,11 +292,14 @@ class AuthProvider with ChangeNotifier {
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
message = null;
if (context.mounted) { if (context.mounted) {
context.go("/"); context.go("/");
} }
changePasswordSucced = true; changePasswordSucced = true;
} else { } else {
final data = jsonDecode(response.body);
message = data["message"];
debugPrint( debugPrint(
'Change password failed: ${response.statusCode}, ${response.body}'); 'Change password failed: ${response.statusCode}, ${response.body}');
} }
...@@ -271,18 +311,24 @@ class AuthProvider with ChangeNotifier { ...@@ -271,18 +311,24 @@ class AuthProvider with ChangeNotifier {
} }
} }
Future<void> resetPassword(String email) async { Future<void> resetPassword(BuildContext context, String email) async {
try { try {
setLoading(true); setLoading(true);
final response = await http.post( final response = await http.post(
Uri.parse('$baseUrl/reset-password/request-token'), Uri.parse('$baseUrl/auth/reset-password'),
body: jsonEncode({"email": email}), body: jsonEncode({"email": email}),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
message = null;
resetPasswordTokenSended = true; resetPasswordTokenSended = true;
if (context.mounted) {
context.go("/confirm-reset-password");
}
} else { } else {
final data = jsonDecode(response.body);
message = data["message"];
debugPrint( debugPrint(
'Error reset user password: ${response.statusCode}, ${response.body}'); 'Error reset user password: ${response.statusCode}, ${response.body}');
} }
...@@ -295,7 +341,11 @@ class AuthProvider with ChangeNotifier { ...@@ -295,7 +341,11 @@ class AuthProvider with ChangeNotifier {
} }
Future<void> confirmResetPassword( Future<void> confirmResetPassword(
int pin, String password1, String password2) async { BuildContext context,
int pin,
String password1,
String password2,
) async {
setLoading(true); setLoading(true);
final body = jsonEncode({ final body = jsonEncode({
"pin": pin, "pin": pin,
...@@ -305,14 +355,20 @@ class AuthProvider with ChangeNotifier { ...@@ -305,14 +355,20 @@ class AuthProvider with ChangeNotifier {
try { try {
final response = await http.post( final response = await http.post(
Uri.parse('$baseUrl/reset-password/confirm'), Uri.parse('$baseUrl/auth/reset-password-confirm'),
body: body, body: body,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
message = null;
resetPasswordSucced = true; resetPasswordSucced = true;
if (context.mounted) {
context.go("/");
}
} else { } else {
final data = jsonDecode(response.body);
message = data["message"];
debugPrint( debugPrint(
'Error confirm reset user password: ${response.statusCode}, ${response.body}'); 'Error confirm reset user password: ${response.statusCode}, ${response.body}');
} }
...@@ -328,13 +384,13 @@ class AuthProvider with ChangeNotifier { ...@@ -328,13 +384,13 @@ class AuthProvider with ChangeNotifier {
setLoading(true); setLoading(true);
final token = await getAccessToken(); final token = await getAccessToken();
String url = '$baseUrl/members/${user?.accountId}/loans/'; String url = '$baseUrl/user/loans';
if (filterByUpcoming) { if (filterByUpcoming) {
url += '?near_outstanding=True'; url += '?near_outstanding=True';
} else if (filterByOverdued) { } else if (filterByOverdued) {
url += '?overdue=True'; url += '?overdue=True';
} else { } else if (pageNumber > 1) {
null; url += "?page=$pageNumber";
} }
try { try {
...@@ -347,9 +403,16 @@ class AuthProvider with ChangeNotifier { ...@@ -347,9 +403,16 @@ class AuthProvider with ChangeNotifier {
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
message = null;
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
memberLoans = data["results"]; memberLoans = data["data"];
hasNextPage = data["has_next"];
hasPrevPage = data["has_prev"];
pageNumber = data["page_number"];
totalPages = data["total_pages"];
} else { } else {
final data = jsonDecode(response.body);
message = data["message"];
debugPrint( debugPrint(
"Failed to get member loan. ${response.statusCode}: ${response.body}"); "Failed to get member loan. ${response.statusCode}: ${response.body}");
} }
...@@ -377,6 +440,7 @@ class AuthProvider with ChangeNotifier { ...@@ -377,6 +440,7 @@ class AuthProvider with ChangeNotifier {
Future<void> createMemberLoan(int memberId, int bookId, int loanDay) async { Future<void> createMemberLoan(int memberId, int bookId, int loanDay) async {
final token = await getAccessToken(); final token = await getAccessToken();
String url = '$baseUrl/user/loans';
final now = DateTime.now(); final now = DateTime.now();
final dueDate = now.add(Duration(days: loanDay)); final dueDate = now.add(Duration(days: loanDay));
...@@ -390,7 +454,7 @@ class AuthProvider with ChangeNotifier { ...@@ -390,7 +454,7 @@ class AuthProvider with ChangeNotifier {
try { try {
setLoading(true); setLoading(true);
final response = await http.post( final response = await http.post(
Uri.parse('$baseUrl/members/$memberId/loans/'), Uri.parse(url),
body: jsonEncode(body), body: jsonEncode(body),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...@@ -399,31 +463,33 @@ class AuthProvider with ChangeNotifier { ...@@ -399,31 +463,33 @@ class AuthProvider with ChangeNotifier {
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = jsonDecode(response.body); message = null;
memberLoans = data["results"]; await getMemberLoan();
} else { } else {
final data = jsonDecode(response.body);
message = data["message"];
debugPrint( debugPrint(
"Failed to create member loan. ${response.statusCode}: ${response.body}"); "Failed to add member loan. ${response.statusCode}: ${response.body}");
} }
loanBookSuccess = true; loanBookSuccess = true;
setLoading(false); setLoading(false);
notifyListeners(); notifyListeners();
} catch (error) { } catch (error) {
debugPrint("Failed to create member loan. $error"); debugPrint("Failed to add member loan. $error");
} }
} }
// for admin or librarian // for admin or librarian
Future<void> getLoans(String? type) async { Future<void> getLoans(String? type) async {
final token = await storage.read(key: 'access_token'); final token = await storage.read(key: 'access_token');
String url = baseUrl; String url = "$baseUrl/book-loans";
if (type == "upcoming") { if (type == "upcoming") {
url += "/upcoming-loans/"; url += '?near_outstanding=True';
} else if (type == "overdue") { } else if (type == "overdue") {
url += "/overdued-loans/"; url += '?overdue=True';
} else { } else if (pageNumber > 1) {
url += "/book-loans/"; url += "?page=$pageNumber";
} }
if (token != null) { if (token != null) {
...@@ -440,12 +506,16 @@ class AuthProvider with ChangeNotifier { ...@@ -440,12 +506,16 @@ class AuthProvider with ChangeNotifier {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
if (type == "upcoming") { if (type == "upcoming") {
nearOutstandingLoans = data["results"]; nearOutstandingLoans = data["data"];
} else if (type == "overdue") { } else if (type == "overdue") {
overduedLoans = data["results"]; overduedLoans = data["data"];
} else { } else {
loans = data["results"]; loans = data["data"];
} }
hasNextPage = data["has_next"];
hasPrevPage = data["has_prev"];
pageNumber = data["page_number"];
totalPages = data["total_pages"];
} else { } else {
final code = response.statusCode; final code = response.statusCode;
debugPrint("Error: Fetch upcoming loans failed, $code"); debugPrint("Error: Fetch upcoming loans failed, $code");
......
...@@ -12,6 +12,11 @@ class BookProvider with ChangeNotifier { ...@@ -12,6 +12,11 @@ class BookProvider with ChangeNotifier {
String? searchKeyword; String? searchKeyword;
String? filterByCategory; String? filterByCategory;
bool hasNextPage = false;
bool hasPrevPage = false;
int pageNumber = 1;
int? totalPages;
bool isLoading = false; bool isLoading = false;
void setLoading(bool value) { void setLoading(bool value) {
...@@ -23,9 +28,11 @@ class BookProvider with ChangeNotifier { ...@@ -23,9 +28,11 @@ class BookProvider with ChangeNotifier {
setLoading(true); setLoading(true);
String url = '$baseUrl/books'; String url = '$baseUrl/books';
if (filterByCategory != null) { if (filterByCategory != null) {
url += '?category__name=$filterByCategory'; url += '?category=$filterByCategory';
} else if (searchKeyword != null) { } else if (searchKeyword != null) {
url += "?search=$searchKeyword"; url += "?search=$searchKeyword";
} else if (pageNumber > 1) {
url += "?page=$pageNumber";
} }
final response = await http.get( final response = await http.get(
...@@ -35,7 +42,11 @@ class BookProvider with ChangeNotifier { ...@@ -35,7 +42,11 @@ class BookProvider with ChangeNotifier {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
books = data["results"]; books = data["data"];
hasNextPage = data["has_next"];
hasPrevPage = data["has_prev"];
pageNumber = data["page_number"];
totalPages = data["total_pages"];
} else { } else {
final code = response.statusCode; final code = response.statusCode;
debugPrint("Error: Fetch books failed, $code"); debugPrint("Error: Fetch books failed, $code");
...@@ -58,8 +69,14 @@ class BookProvider with ChangeNotifier { ...@@ -58,8 +69,14 @@ class BookProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void setPage(int value) {
pageNumber = value;
notifyListeners();
}
Future<void> getCategories() async { Future<void> getCategories() async {
try { try {
setLoading(true);
final response = await http.get( final response = await http.get(
Uri.parse('$baseUrl/categories'), Uri.parse('$baseUrl/categories'),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
...@@ -67,11 +84,12 @@ class BookProvider with ChangeNotifier { ...@@ -67,11 +84,12 @@ class BookProvider with ChangeNotifier {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
categories = data["results"]; categories = data;
} else { } else {
debugPrint("Error: Fetch books failed, ${response.statusCode}"); debugPrint("Error: Fetch books failed, ${response.statusCode}");
} }
setLoading(false);
notifyListeners(); notifyListeners();
} catch (error) { } catch (error) {
debugPrint("Error: Fetch books failed, $error"); debugPrint("Error: Fetch books failed, $error");
......
...@@ -41,7 +41,9 @@ class _FormScreen extends State<FormScreen> { ...@@ -41,7 +41,9 @@ class _FormScreen extends State<FormScreen> {
title: Text(title), title: Text(title),
leading: withBack leading: withBack
? BackButton( ? BackButton(
onPressed: () => context.push(backRoute ?? ""), onPressed: () => backRoute != null
? context.push(backRoute!)
: context.pop(),
) )
: null, : null,
actions: action, actions: action,
...@@ -99,7 +101,7 @@ class ChangePasswordScreen extends StatelessWidget { ...@@ -99,7 +101,7 @@ class ChangePasswordScreen extends StatelessWidget {
return FormScreen( return FormScreen(
title: title, title: title,
backRoute: "/", backRoute: null,
body: const ChangePasswordForm(), body: const ChangePasswordForm(),
); );
} }
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:library_app/src/providers/auth_provider.dart'; import 'package:library_app/src/providers/auth_provider.dart';
import 'package:library_app/src/providers/navigations_provider.dart';
import 'package:library_app/src/screens/list/admin_list_screen.dart'; import 'package:library_app/src/screens/list/admin_list_screen.dart';
import 'package:library_app/src/screens/list/member_list_screen.dart'; import 'package:library_app/src/screens/list/member_list_screen.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
...@@ -12,6 +13,17 @@ class ListScreen extends StatefulWidget { ...@@ -12,6 +13,17 @@ class ListScreen extends StatefulWidget {
} }
class _ListScreen extends State<ListScreen> { class _ListScreen extends State<ListScreen> {
@override
void initState() {
Future.delayed(
Duration.zero,
() {
Provider.of<NavigationsProvider>(context, listen: false).navigate(0);
},
);
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = Provider.of<AuthProvider>(context).user; final user = Provider.of<AuthProvider>(context).user;
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:library_app/src/models/category.dart'; import 'package:library_app/src/models/category.dart';
import 'package:library_app/src/providers/auth_provider.dart'; import 'package:library_app/src/providers/auth_provider.dart';
import 'package:library_app/src/providers/book_provider.dart'; import 'package:library_app/src/providers/book_provider.dart';
...@@ -73,31 +74,53 @@ class _MemberListScreen extends State<MemberListScreen> { ...@@ -73,31 +74,53 @@ class _MemberListScreen extends State<MemberListScreen> {
const BookList(), const BookList(),
// Loans // Loans
LoanList( LoanList(
memberId: authProvider.user?.accountId ?? 0, memberId: authProvider.user?.id ?? 0,
), ),
// Profile // Profile
const Profile(), const Profile(),
][navProvider.currentPageIndex], ][navProvider.currentPageIndex],
drawer: Drawer( drawer: Drawer(
child: ListView( child: Column(
padding: EdgeInsets.zero, children: [
children: List.generate( Padding(
category != null ? category.length : 0, padding: const EdgeInsets.symmetric(
(index) { horizontal: 10.0, vertical: 20.0),
if (category != null) { child: Row(
return ListTile( mainAxisAlignment: MainAxisAlignment.start,
title: Text(category.elementAt(index).name), children: [
onTap: () { ElevatedButton(
bookProvider.filterBookByCategory( child: const Row(
category!.elementAt(index).name); children: [Icon(Icons.arrow_back), Text("Back")],
bookProvider.getBooks(); ),
Navigator.pop(context); onPressed: () {
}, bookProvider.filterBookByCategory(null);
); bookProvider.getBooks();
} context.pop();
return Container(); },
}, ),
), ],
),
),
Expanded(
child: ListView.builder(
itemCount: category != null ? category.length : 0,
itemBuilder: (context, index) {
if (category != null) {
return ListTile(
title: Text(category.elementAt(index).name),
onTap: () {
bookProvider.filterBookByCategory(
category!.elementAt(index).name);
bookProvider.getBooks();
Navigator.pop(context);
},
);
}
return Container();
},
),
),
],
), ),
), ),
); );
......
...@@ -21,60 +21,111 @@ class _BookList extends State<BookList> { ...@@ -21,60 +21,111 @@ class _BookList extends State<BookList> {
Provider.of<BookProvider>(context, listen: false).getBooks(); Provider.of<BookProvider>(context, listen: false).getBooks();
} }
@override
void dispose() {
super.dispose();
}
ScrollController listScrollController = ScrollController();
void scrollToTop() {
if (listScrollController.hasClients) {
final position = listScrollController.position.minScrollExtent;
listScrollController.jumpTo(position);
}
}
Future<void> nextPage() async {
if (Provider.of<BookProvider>(context, listen: false).hasNextPage) {
Provider.of<BookProvider>(context, listen: false).setPage(
Provider.of<BookProvider>(context, listen: false).pageNumber + 1,
);
} else {
Provider.of<BookProvider>(context, listen: false).setPage(
Provider.of<BookProvider>(context, listen: false).totalPages!);
}
Provider.of<BookProvider>(context, listen: false).getBooks();
scrollToTop();
}
Future<void> prevPage() async {
if (Provider.of<BookProvider>(context, listen: false).hasPrevPage) {
Provider.of<BookProvider>(context, listen: false).setPage(
Provider.of<BookProvider>(context, listen: false).pageNumber - 1,
);
} else {
Provider.of<BookProvider>(context, listen: false).setPage(1);
}
Provider.of<BookProvider>(context, listen: false).getBooks();
scrollToTop();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<BookProvider>( return Consumer<BookProvider>(
builder: (context, bookProvider, child) { builder: (context, bookProvider, child) {
if (!bookProvider.isLoading) { if (bookProvider.books == null) {
final Iterable<Book> books = bookProvider.books!.map((book) { return const Center(
if (book["category_detail"] != null) { child: CircularProgressIndicator(),
final Category category = Category.fromJson( );
book["category_detail"], }
); final Iterable<Book> books = bookProvider.books!.map((book) {
return Book( if (book["category"] != null) {
book["id"], final Category category = Category.fromJson(
book["title"], book["category"],
book["author"], );
book["description"],
book["cover_image"],
category.name,
);
}
return Book( return Book(
book["id"], book["id"],
book["title"], book["title"],
book["author"], book["author"],
book["description"], book["description"],
book["cover_image"], book["cover_image"],
null, category.name,
); );
}); }
return NestedScrollView( return Book(
headerSliverBuilder: book["id"],
(BuildContext context, bool innerBoxIsScrolled) { book["title"],
return [const TopAppBar(title: "Books")]; book["author"],
}, book["description"],
body: ListView( book["cover_image"],
children: List.generate(books.length, (index) { null,
return BookItem(
books.elementAt(index),
);
}),
),
); );
} else { });
return NestedScrollView(
headerSliverBuilder: return NestedScrollView(
(BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [const TopAppBar(title: "Books")]; return [const TopAppBar(title: "Books")];
},
body: ListView.builder(
controller: listScrollController,
itemCount: books.length + 1,
itemBuilder: (context, index) {
if (index < books.length) {
return BookItem(books.elementAt(index));
} else {
return Container(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: prevPage,
child: const Text('Prev'),
),
Text(bookProvider.pageNumber.toString()),
ElevatedButton(
onPressed: nextPage,
child: const Text('Next'),
),
],
),
);
}
}, },
body: const Center( ),
child: CircularProgressIndicator(), );
),
);
}
}, },
); );
} }
...@@ -127,14 +178,9 @@ class _TopAppBar extends State<TopAppBar> { ...@@ -127,14 +178,9 @@ class _TopAppBar extends State<TopAppBar> {
leading: !showWidget leading: !showWidget
? IconButton( ? IconButton(
onPressed: () { onPressed: () {
if (category != null) { Scaffold.of(context).openDrawer();
bookProvider.filterBookByCategory(null);
bookProvider.getBooks();
} else {
Scaffold.of(context).openDrawer();
}
}, },
icon: Icon(category != null ? Icons.arrow_back : Icons.menu), icon: const Icon(Icons.menu),
) )
: null, : null,
elevation: 10.0, elevation: 10.0,
......
...@@ -91,7 +91,7 @@ class _LoanBookForm extends State<LoanBookForm> { ...@@ -91,7 +91,7 @@ class _LoanBookForm extends State<LoanBookForm> {
if (_formKey.currentState!.validate()) {} if (_formKey.currentState!.validate()) {}
authProvider authProvider
.createMemberLoan( .createMemberLoan(
authProvider.user!.accountId, authProvider.user!.id,
widget.bookId, widget.bookId,
int.parse(loanDayController.text), int.parse(loanDayController.text),
) )
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
import 'package:library_app/src/providers/auth_provider.dart'; import 'package:library_app/src/providers/auth_provider.dart';
import 'package:library_app/src/widgets/loading.dart'; import 'package:library_app/src/widgets/loading.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
...@@ -15,7 +14,8 @@ class ChangePasswordForm extends StatefulWidget { ...@@ -15,7 +14,8 @@ class ChangePasswordForm extends StatefulWidget {
class _ChangePasswordForm extends State<ChangePasswordForm> { class _ChangePasswordForm extends State<ChangePasswordForm> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final oldPasswordController = TextEditingController(); final oldPasswordController = TextEditingController();
final newPasswordController = TextEditingController(); final newPasswordController1 = TextEditingController();
final newPasswordController2 = TextEditingController();
bool passwordVisible = false; bool passwordVisible = false;
...@@ -28,7 +28,8 @@ class _ChangePasswordForm extends State<ChangePasswordForm> { ...@@ -28,7 +28,8 @@ class _ChangePasswordForm extends State<ChangePasswordForm> {
@override @override
void dispose() { void dispose() {
oldPasswordController.dispose(); oldPasswordController.dispose();
newPasswordController.dispose(); newPasswordController1.dispose();
newPasswordController2.dispose();
super.dispose(); super.dispose();
} }
...@@ -37,6 +38,7 @@ class _ChangePasswordForm extends State<ChangePasswordForm> { ...@@ -37,6 +38,7 @@ class _ChangePasswordForm extends State<ChangePasswordForm> {
Size screenSize = MediaQuery.of(context).size; Size screenSize = MediaQuery.of(context).size;
return Consumer<AuthProvider>(builder: (context, authProvider, child) { return Consumer<AuthProvider>(builder: (context, authProvider, child) {
final message = authProvider.message;
return Column( return Column(
children: [ children: [
Padding( Padding(
...@@ -79,7 +81,7 @@ class _ChangePasswordForm extends State<ChangePasswordForm> { ...@@ -79,7 +81,7 @@ class _ChangePasswordForm extends State<ChangePasswordForm> {
), ),
), ),
validator: (String? value) { validator: (String? value) {
if (value == null || value.isEmpty) { if (value == null) {
return "Please enter your old password"; return "Please enter your old password";
} else { } else {
return null; return null;
...@@ -88,7 +90,7 @@ class _ChangePasswordForm extends State<ChangePasswordForm> { ...@@ -88,7 +90,7 @@ class _ChangePasswordForm extends State<ChangePasswordForm> {
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
), ),
TextFormField( TextFormField(
controller: newPasswordController, controller: newPasswordController1,
obscureText: passwordVisible, obscureText: passwordVisible,
decoration: InputDecoration( decoration: InputDecoration(
hintText: "Enter your New Password", hintText: "Enter your New Password",
...@@ -107,7 +109,7 @@ class _ChangePasswordForm extends State<ChangePasswordForm> { ...@@ -107,7 +109,7 @@ class _ChangePasswordForm extends State<ChangePasswordForm> {
), ),
), ),
validator: (String? value) { validator: (String? value) {
if (value == null || value.isEmpty) { if (value == null) {
return "Please enter your new password"; return "Please enter your new password";
} else { } else {
return null; return null;
...@@ -115,36 +117,43 @@ class _ChangePasswordForm extends State<ChangePasswordForm> { ...@@ -115,36 +117,43 @@ class _ChangePasswordForm extends State<ChangePasswordForm> {
}, },
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
), ),
TextFormField(
controller: newPasswordController2,
decoration: const InputDecoration(
hintText: "Confirm your New Password",
labelText: "Confirm New Password",
),
validator: (String? value) {
if (value == null) {
return "Please enter your confirm new password";
} else {
return null;
}
},
),
Text(
message ?? "",
style: const TextStyle(color: Colors.red),
),
const SizedBox( const SizedBox(
height: 20.0, height: 20.0,
), ),
Column( SizedBox(
children: [ width: double.infinity,
SizedBox( child: FilledButton(
width: double.infinity, onPressed: () async {
child: FilledButton( if (_formKey.currentState!.validate()) {}
onPressed: () async { authProvider.changePassword(
if (_formKey.currentState!.validate()) {} context,
authProvider.changePassword( oldPasswordController.text,
context, newPasswordController1.text,
authProvider.user!.accountId, newPasswordController2.text,
oldPasswordController.text, );
newPasswordController.text, },
); child: authProvider.isLoading
}, ? const Loading()
child: authProvider.isLoading : const Text("Submit"),
? const Loading() ),
: const Text("Submit"),
),
),
SizedBox(
width: double.infinity,
child: TextButton(
child: const Text("Cancel"),
onPressed: () => context.pop("/"),
),
),
],
), ),
], ],
), ),
......
...@@ -39,7 +39,8 @@ class _LoginForm extends State<LoginForm> { ...@@ -39,7 +39,8 @@ class _LoginForm extends State<LoginForm> {
const String formText = "Log In to continue"; const String formText = "Log In to continue";
return Consumer<AuthProvider>(builder: (context, authProvider, child) { return Consumer<AuthProvider>(builder: (context, authProvider, child) {
final isInvalid = authProvider.invalidUsernameOrPassword; final isInvalidUsernameOrPassword =
authProvider.invalidUsernameOrPassword;
return Column( return Column(
children: [ children: [
...@@ -106,28 +107,21 @@ class _LoginForm extends State<LoginForm> { ...@@ -106,28 +107,21 @@ class _LoginForm extends State<LoginForm> {
}, },
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
), ),
Visibility( Padding(
visible: isInvalid, padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Container( child: Text(
decoration: BoxDecoration( isInvalidUsernameOrPassword
borderRadius: BorderRadius.circular(10), ? "Invalid username or password"
color: Theme.of(context).highlightColor, : "",
), style: const TextStyle(color: Colors.red),
margin: const EdgeInsets.symmetric(vertical: 20.0),
padding: const EdgeInsets.symmetric(
horizontal: 10.0, vertical: 8.0),
child: const Text("Invalid username or password"),
), ),
), ),
const SizedBox(
height: 20.0,
),
Column( Column(
children: [ children: [
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: FilledButton(
onPressed: () { onPressed: () async {
if (_formKey.currentState!.validate()) {} if (_formKey.currentState!.validate()) {}
authProvider.signIn( authProvider.signIn(
context, context,
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:library_app/src/providers/auth_provider.dart'; import 'package:library_app/src/providers/auth_provider.dart';
import 'package:library_app/src/models/user.dart'; import 'package:library_app/src/models/user.dart';
...@@ -43,6 +42,7 @@ class _ProfileEditForm extends State<ProfileEditForm> { ...@@ -43,6 +42,7 @@ class _ProfileEditForm extends State<ProfileEditForm> {
lastNameControler.text = user?.lastName ?? ""; lastNameControler.text = user?.lastName ?? "";
return Consumer<AuthProvider>(builder: (context, authProvider, child) { return Consumer<AuthProvider>(builder: (context, authProvider, child) {
final message = authProvider.message;
return Column( return Column(
children: [ children: [
Form( Form(
...@@ -62,6 +62,7 @@ class _ProfileEditForm extends State<ProfileEditForm> { ...@@ -62,6 +62,7 @@ class _ProfileEditForm extends State<ProfileEditForm> {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return "Please enter your username"; return "Please enter your username";
} }
return null; return null;
}, },
), ),
...@@ -75,6 +76,7 @@ class _ProfileEditForm extends State<ProfileEditForm> { ...@@ -75,6 +76,7 @@ class _ProfileEditForm extends State<ProfileEditForm> {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return "Please enter your email"; return "Please enter your email";
} }
return null; return null;
}, },
), ),
...@@ -92,6 +94,10 @@ class _ProfileEditForm extends State<ProfileEditForm> { ...@@ -92,6 +94,10 @@ class _ProfileEditForm extends State<ProfileEditForm> {
labelText: "Last Name", labelText: "Last Name",
), ),
), ),
Text(
message ?? "",
style: const TextStyle(color: Colors.red),
),
Padding( Padding(
padding: const EdgeInsets.only(top: 40.0), padding: const EdgeInsets.only(top: 40.0),
child: SizedBox( child: SizedBox(
...@@ -99,33 +105,20 @@ class _ProfileEditForm extends State<ProfileEditForm> { ...@@ -99,33 +105,20 @@ class _ProfileEditForm extends State<ProfileEditForm> {
child: FilledButton( child: FilledButton(
onPressed: () { onPressed: () {
if (_formKey.currentState!.validate()) {} if (_formKey.currentState!.validate()) {}
authProvider authProvider.updateUserDetail(
.updateUserDetail( context,
authProvider.user!.id, authProvider.user!.id,
usernameControler.text, usernameControler.text,
emailControler.text, emailControler.text,
firstNameControler.text, firstNameControler.text,
lastNameControler.text, lastNameControler.text,
authProvider.user!.isStaff, authProvider.user!.isStaff,
) );
.then(
(res) => Navigator.pop(context),
);
}, },
child: const Text("Submit"), child: const Text("Submit"),
), ),
), ),
), ),
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => context.go("/change-password"),
child: const Text("Change Password"),
),
),
)
], ],
), ),
), ),
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:library_app/src/providers/auth_provider.dart'; import 'package:library_app/src/providers/auth_provider.dart';
import 'package:library_app/src/screens/form_screen.dart';
import 'package:library_app/src/widgets/loading.dart'; import 'package:library_app/src/widgets/loading.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
...@@ -22,6 +22,8 @@ class _ResetPasswordForm extends State<ResetPasswordForm> { ...@@ -22,6 +22,8 @@ class _ResetPasswordForm extends State<ResetPasswordForm> {
const String formText = "Confirm your email to continue reset password"; const String formText = "Confirm your email to continue reset password";
return Consumer<AuthProvider>(builder: (context, authProvider, child) { return Consumer<AuthProvider>(builder: (context, authProvider, child) {
final message = authProvider.message;
return Column( return Column(
children: [ children: [
Container( Container(
...@@ -67,13 +69,14 @@ class _ResetPasswordForm extends State<ResetPasswordForm> { ...@@ -67,13 +69,14 @@ class _ResetPasswordForm extends State<ResetPasswordForm> {
validator: (String? value) { validator: (String? value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return "Please enter your email"; return "Please enter your email";
} else if (!value.contains("@")) {
return "Email should include '@'";
} }
return null; return null;
}, },
), ),
// Flutter, iwant to go to ConfirmResetPasswordScreen after authProvider.resetPassword succeed with response 200 Text(
message ?? "",
style: const TextStyle(color: Colors.red),
),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 20.0, vertical: 20.0,
...@@ -85,22 +88,8 @@ class _ResetPasswordForm extends State<ResetPasswordForm> { ...@@ -85,22 +88,8 @@ class _ResetPasswordForm extends State<ResetPasswordForm> {
child: FilledButton( child: FilledButton(
onPressed: () { onPressed: () {
if (_formKey.currentState!.validate()) {} if (_formKey.currentState!.validate()) {}
authProvider authProvider.resetPassword(
.resetPassword(emailController.text) context, emailController.text);
.then(
(response) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
const ConfirmResetPasswordScreen(),
),
);
},
).catchError(
(error) {
debugPrint('Exception: $error');
},
);
}, },
child: authProvider.isLoading child: authProvider.isLoading
? const Loading() ? const Loading()
...@@ -142,12 +131,7 @@ class _ConfirmResetPasswordForm extends State<ConfirmResetPasswordForm> { ...@@ -142,12 +131,7 @@ class _ConfirmResetPasswordForm extends State<ConfirmResetPasswordForm> {
const String formText = "Enter the pin that we just sent to your email"; const String formText = "Enter the pin that we just sent to your email";
return Consumer<AuthProvider>(builder: (context, authProvider, child) { return Consumer<AuthProvider>(builder: (context, authProvider, child) {
if (authProvider.isLoading) { final message = authProvider.message;
return const Center(
child: CircularProgressIndicator(),
);
}
return Column( return Column(
children: [ children: [
Container( Container(
...@@ -190,6 +174,17 @@ class _ConfirmResetPasswordForm extends State<ConfirmResetPasswordForm> { ...@@ -190,6 +174,17 @@ class _ConfirmResetPasswordForm extends State<ConfirmResetPasswordForm> {
labelText: "confirmation pin", labelText: "confirmation pin",
suffixIcon: Icon(Icons.password), suffixIcon: Icon(Icons.password),
), ),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
validator: (String? value) {
if (value == null) {
return "Please enter your pin";
} else if (value is int) {
return "Please enter pin in number";
}
return null;
},
), ),
TextFormField( TextFormField(
controller: password1Controller, controller: password1Controller,
...@@ -241,12 +236,16 @@ class _ConfirmResetPasswordForm extends State<ConfirmResetPasswordForm> { ...@@ -241,12 +236,16 @@ class _ConfirmResetPasswordForm extends State<ConfirmResetPasswordForm> {
validator: (String? value) { validator: (String? value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return "Please enter your password"; return "Please enter your password";
} else {
return null;
} }
return null;
}, },
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
), ),
Text(
message ?? "",
style: const TextStyle(color: Colors.red),
),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 20.0, vertical: 20.0,
...@@ -258,28 +257,16 @@ class _ConfirmResetPasswordForm extends State<ConfirmResetPasswordForm> { ...@@ -258,28 +257,16 @@ class _ConfirmResetPasswordForm extends State<ConfirmResetPasswordForm> {
child: FilledButton( child: FilledButton(
onPressed: () { onPressed: () {
if (_formKey.currentState!.validate()) {} if (_formKey.currentState!.validate()) {}
authProvider authProvider.confirmResetPassword(
.confirmResetPassword( context,
int.parse(pinController.text), int.parse(pinController.text),
password1Controller.text, password1Controller.text,
password2Controller.text, password2Controller.text,
)
.then(
(response) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
const LoginScreen(),
),
);
},
).catchError(
(error) {
debugPrint('Exception: $error');
},
); );
}, },
child: const Text("Submit"), child: authProvider.isLoading
? const Loading()
: const Text("Submit"),
), ),
), ),
], ],
......
...@@ -24,17 +24,19 @@ class _SearchForm extends State<SearchForm> { ...@@ -24,17 +24,19 @@ class _SearchForm extends State<SearchForm> {
width: queryData.size.width * 0.8, width: queryData.size.width * 0.8,
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: SearchBar( child: SearchBar(
hintText: "Enter keywords...", hintText: "Enter book title...",
elevation: WidgetStateProperty.all(0), elevation: WidgetStateProperty.all(0),
onChanged: (value) { onChanged: (value) {
Future.delayed( if (value.length >= 3) {
Duration.zero, Future.delayed(
() { Duration.zero,
Provider.of<BookProvider>(context, listen: false) () {
.setSearchKeyword(value); Provider.of<BookProvider>(context, listen: false)
Provider.of<BookProvider>(context, listen: false).getBooks(); .setSearchKeyword(value);
}, Provider.of<BookProvider>(context, listen: false).getBooks();
); },
);
}
}, },
leading: const Icon(Icons.search), leading: const Icon(Icons.search),
), ),
......
...@@ -40,6 +40,8 @@ class _SignUpForm extends State<SignUpForm> { ...@@ -40,6 +40,8 @@ class _SignUpForm extends State<SignUpForm> {
const String formText = "Sign In to get started"; const String formText = "Sign In to get started";
return Consumer<AuthProvider>(builder: (context, authProvider, child) { return Consumer<AuthProvider>(builder: (context, authProvider, child) {
final message = authProvider.message;
return Column( return Column(
children: [ children: [
Padding( Padding(
...@@ -120,8 +122,10 @@ class _SignUpForm extends State<SignUpForm> { ...@@ -120,8 +122,10 @@ class _SignUpForm extends State<SignUpForm> {
}, },
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
), ),
const SizedBox( Padding(
height: 20.0, padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Text(message ?? "",
style: const TextStyle(color: Colors.red)),
), ),
Column( Column(
children: [ children: [
......
...@@ -36,6 +36,40 @@ class _AdminLoanList extends State<AdminLoanList> { ...@@ -36,6 +36,40 @@ class _AdminLoanList extends State<AdminLoanList> {
super.initState(); super.initState();
} }
ScrollController listScrollController = ScrollController();
void scrollToTop() {
if (listScrollController.hasClients) {
final position = listScrollController.position.minScrollExtent;
listScrollController.jumpTo(position);
}
}
Future<void> nextPage() async {
if (Provider.of<AuthProvider>(context, listen: false).hasNextPage) {
Provider.of<AuthProvider>(context, listen: false).setPage(
Provider.of<AuthProvider>(context, listen: false).pageNumber + 1,
);
} else {
Provider.of<AuthProvider>(context, listen: false).setPage(
Provider.of<AuthProvider>(context, listen: false).totalPages!);
}
Provider.of<AuthProvider>(context, listen: false).getMemberLoan();
scrollToTop();
}
Future<void> prevPage() async {
if (Provider.of<AuthProvider>(context, listen: false).hasPrevPage) {
Provider.of<AuthProvider>(context, listen: false).setPage(
Provider.of<AuthProvider>(context, listen: false).pageNumber - 1,
);
} else {
Provider.of<AuthProvider>(context, listen: false).setPage(1);
}
Provider.of<AuthProvider>(context, listen: false).getMemberLoan();
scrollToTop();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<AuthProvider>(builder: (context, loanProvider, child) { return Consumer<AuthProvider>(builder: (context, loanProvider, child) {
...@@ -45,18 +79,8 @@ class _AdminLoanList extends State<AdminLoanList> { ...@@ -45,18 +79,8 @@ class _AdminLoanList extends State<AdminLoanList> {
if (getLoans != null) { if (getLoans != null) {
var loans = getLoans.map( var loans = getLoans.map(
(loan) { (loan) {
var book = Book.fromJson(loan["book_detail"]); var book = Book.fromJson(loan["book"]);
var memberData = loan["member_detail"]; var user = User.fromJson(loan["user"]);
var userData = memberData["user"];
var user = User(
userData["id"],
memberData["id"],
userData["username"],
userData["email"],
userData["first_name"],
userData["last_name"],
userData["is_staff"],
);
return Loan( return Loan(
book, book,
...@@ -73,13 +97,34 @@ class _AdminLoanList extends State<AdminLoanList> { ...@@ -73,13 +97,34 @@ class _AdminLoanList extends State<AdminLoanList> {
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [TopAppBar(title: title)]; return [TopAppBar(title: title)];
}, },
body: ListView( body: ListView.builder(
children: List.generate(loans.length, (index) { controller: listScrollController,
return LoanItem( itemCount: loans.length + 1,
loans.elementAt(index), itemBuilder: (context, index) {
user: loans.elementAt(index).user, if (index < loans.length) {
); return LoanItem(
}), loans.elementAt(index),
);
} else {
return Container(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: prevPage,
child: const Text('Prev'),
),
Text(loanProvider.pageNumber.toString()),
ElevatedButton(
onPressed: nextPage,
child: const Text('Next'),
),
],
),
);
}
},
), ),
); );
} else { } else {
......
...@@ -26,13 +26,47 @@ class _LoanList extends State<LoanList> { ...@@ -26,13 +26,47 @@ class _LoanList extends State<LoanList> {
super.initState(); super.initState();
} }
ScrollController listScrollController = ScrollController();
void scrollToTop() {
if (listScrollController.hasClients) {
final position = listScrollController.position.minScrollExtent;
listScrollController.jumpTo(position);
}
}
Future<void> nextPage() async {
if (Provider.of<AuthProvider>(context, listen: false).hasNextPage) {
Provider.of<AuthProvider>(context, listen: false).setPage(
Provider.of<AuthProvider>(context, listen: false).pageNumber + 1,
);
} else {
Provider.of<AuthProvider>(context, listen: false).setPage(
Provider.of<AuthProvider>(context, listen: false).totalPages!);
}
Provider.of<AuthProvider>(context, listen: false).getMemberLoan();
scrollToTop();
}
Future<void> prevPage() async {
if (Provider.of<AuthProvider>(context, listen: false).hasPrevPage) {
Provider.of<AuthProvider>(context, listen: false).setPage(
Provider.of<AuthProvider>(context, listen: false).pageNumber - 1,
);
} else {
Provider.of<AuthProvider>(context, listen: false).setPage(1);
}
Provider.of<AuthProvider>(context, listen: false).getMemberLoan();
scrollToTop();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<AuthProvider>(builder: (context, authProvider, child) { return Consumer<AuthProvider>(builder: (context, authProvider, child) {
if (authProvider.memberLoans != null) { if (authProvider.memberLoans != null) {
var loans = authProvider.memberLoans!.map( var loans = authProvider.memberLoans!.map(
(loan) { (loan) {
var book = Book.fromJson(loan["book_detail"]); var book = Book.fromJson(loan["book"]);
return Loan( return Loan(
book, book,
null, null,
...@@ -48,10 +82,35 @@ class _LoanList extends State<LoanList> { ...@@ -48,10 +82,35 @@ class _LoanList extends State<LoanList> {
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [const TopAppBar(title: "Book Loans")]; return [const TopAppBar(title: "Book Loans")];
}, },
body: ListView( body: ListView.builder(
children: List.generate(loans.length, (index) { controller: listScrollController,
return LoanItem(loans.elementAt(index)); itemCount: loans.length + 1,
}), itemBuilder: (context, index) {
if (index < loans.length) {
return LoanItem(
loans.elementAt(index),
user: loans.elementAt(index).user,
);
} else {
return Container(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: prevPage,
child: const Text('Prev'),
),
Text(authProvider.pageNumber.toString()),
ElevatedButton(
onPressed: nextPage,
child: const Text('Next'),
),
],
),
);
}
},
), ),
); );
} else { } else {
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:library_app/src/providers/auth_provider.dart'; import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:library_app/src/screens/profile_edit_screen.dart'; import 'package:library_app/src/providers/auth_provider.dart';
import 'package:library_app/src/widgets/navigations.dart'; import 'package:library_app/src/widgets/navigations.dart';
import 'package:provider/provider.dart';
class Profile extends StatefulWidget { class Profile extends StatefulWidget {
const Profile({super.key}); const Profile({super.key});
...@@ -78,19 +78,25 @@ class _Profile extends State<Profile> { ...@@ -78,19 +78,25 @@ class _Profile extends State<Profile> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
const SizedBox(
height: 10.0,
),
FilledButton( FilledButton(
child: const Text("Edit Profile"), child: const Text("Edit Profile"),
onPressed: () { onPressed: () {
Navigator.of(context).push( context.push("/profile-edit");
MaterialPageRoute(
builder: (context) => const ProfileEditScreen(),
),
);
}, },
), ),
const SizedBox( const SizedBox(
height: 10.0, height: 10.0,
), ),
ElevatedButton(
onPressed: () => context.push("/change-password"),
child: const Text("Change Password"),
),
const SizedBox(
height: 10.0,
),
OutlinedButton( OutlinedButton(
child: const Text("Log Out"), child: const Text("Log Out"),
onPressed: () { onPressed: () {
......
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