Update .gitignore to exclude Firebase configuration files and refactor notification handling in the app. Removed redundant logging and notification data storage in SharedPreferences. Introduced NotificationCheckService and NotificationStateService for improved notification management. Updated view models and UI components to utilize the new services, enhancing notification state tracking and user experience.

This commit is contained in:
amirrezaghabeli
2025-10-06 11:59:54 +03:30
parent 17634744f7
commit 42f2e93a8e
28 changed files with 1499 additions and 595 deletions
+25 -15
View File
@@ -10,7 +10,9 @@ import 'package:tm_app/data/repositories/tenders_repository.dart';
import 'package:tm_app/data/repositories/your_tenders_repository.dart';
import 'package:tm_app/data/services/home_service.dart';
import 'package:tm_app/data/services/liked_tenders_service.dart';
import 'package:tm_app/data/services/notification_check_service.dart';
import 'package:tm_app/data/services/notification_service.dart';
import 'package:tm_app/data/services/notification_state_service.dart';
import 'package:tm_app/data/services/tender_detail_service.dart';
import 'package:tm_app/data/services/your_tenders_service.dart';
import 'package:tm_app/view_models/final_completion_of_documents_view_model.dart';
@@ -30,13 +32,14 @@ import '../../view_models/home_view_model.dart';
import '../../view_models/profile_view_model.dart';
List<SingleChildWidget> get providersRemote {
return [...cores, ...apiClients, ...repositories, ...viewModels, ...others];
return [...cores, ...apiClients, ...repositories, ...services, ...viewModels];
}
List<SingleChildWidget> get cores {
return [
Provider(create: (context) => NetworkManager(context.read())),
ChangeNotifierProvider(create: (context) => ThemeProvider()),
ChangeNotifierProvider(create: (context) => NotificationStateService()),
];
}
@@ -108,20 +111,32 @@ List<SingleChildWidget> get repositories {
];
}
List<SingleChildWidget> get services {
return [
Provider(
create:
(context) => NotificationCheckService(
homeRepository: context.read(),
notificationStateService: context.read(),
),
),
];
}
List<SingleChildWidget> get viewModels {
return [
ChangeNotifierProvider(
create: (context) => AuthViewModel(authRepository: context.read()),
),
ChangeNotifierProvider(
create: (context) => HomeViewModel(homeRepository: context.read()),
create:
(context) => HomeViewModel(
homeRepository: context.read(),
notificationStateService: context.read(),
),
),
ChangeNotifierProvider(
create:
(context) => TendersViewModel(
tendersRepository: context.read(),
homeViewModel: context.read(),
),
create: (context) => TendersViewModel(tendersRepository: context.read()),
),
ChangeNotifierProvider(
create:
@@ -129,7 +144,6 @@ List<SingleChildWidget> get viewModels {
tenderDetailRepository: context.read(),
tendersRepository: context.read(),
tendersViewModel: context.read(),
homeViewModel: context.read(),
),
),
@@ -143,7 +157,6 @@ List<SingleChildWidget> get viewModels {
(context) => LikedTendersViewModel(
likedTendersRepository: context.read(),
tendersRepository: context.read(),
homeViewModel: context.read(),
),
),
ChangeNotifierProvider(
@@ -160,13 +173,10 @@ List<SingleChildWidget> get viewModels {
ChangeNotifierProvider(
create:
(context) => NotificationViewModel(
repositoryViewModel: context.read(),
homeViewModel: context.read(),
notificationsRepository: context.read(),
notificationStateService: context.read(),
notificationCheckService: context.read(),
),
),
];
}
List<SingleChildWidget> get others {
return [];
}
@@ -4,26 +4,41 @@ import 'package:provider/provider.dart';
import 'package:tm_app/core/routes/app_shell/desktop_shell_page.dart';
import 'package:tm_app/core/routes/app_shell/mobile_shell_page.dart';
import 'package:tm_app/core/routes/app_shell/tablet_shell_page.dart';
import 'package:tm_app/data/services/notification_check_service.dart';
import 'package:tm_app/view_models/home_view_model.dart';
import 'package:tm_app/view_models/tenders_view_model.dart';
import 'package:tm_app/views/shared/responsive_builder.dart';
import '../../../view_models/notification_view_model.dart';
class AppShellScreen extends StatelessWidget {
class AppShellScreen extends StatefulWidget {
const AppShellScreen({required this.navigationShell, super.key});
final StatefulNavigationShell navigationShell;
@override
State<AppShellScreen> createState() => _AppShellScreenState();
}
class _AppShellScreenState extends State<AppShellScreen> {
@override
void initState() {
super.initState();
// Check for unread notifications when app shell loads
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<NotificationCheckService>().checkUnreadNotifications();
});
}
void _gotoBranch(int index, BuildContext context) {
if (index == navigationShell.currentIndex) {
if (index == widget.navigationShell.currentIndex) {
return;
}
// Navigate first
navigationShell.goBranch(
widget.navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
initialLocation: index == widget.navigationShell.currentIndex,
);
// Then trigger data loading after navigation is complete
@@ -45,16 +60,16 @@ class AppShellScreen extends StatelessWidget {
return ResponsiveBuilder(
mobile: SafeArea(
child: MobileShellPage(
navigationShell: navigationShell,
navigationShell: widget.navigationShell,
onTap: (value) => _gotoBranch(value, context),
),
),
tablet: TabletShellPage(
navigationShell: navigationShell,
navigationShell: widget.navigationShell,
onTap: (value) => _gotoBranch(value, context),
),
desktop: DesktopShellPage(
navigationShell: navigationShell,
navigationShell: widget.navigationShell,
onTap: (value) => _gotoBranch(value, context),
),
);
@@ -5,9 +5,9 @@ import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/home_view_model.dart';
import 'package:tm_app/views/profile/strings/profile_strings.dart';
import '../../../data/services/notification_state_service.dart';
import '../../../views/home/strings/home_strings.dart';
import '../../../views/tenders/strings/tenders_strings.dart';
@@ -65,7 +65,10 @@ class MobileShellPage extends StatelessWidget {
onTap: () => onTap(3),
iconPath: AssetsManager.notify,
activeIconPath: AssetsManager.notifyActive,
showBadge: context.watch<HomeViewModel>().hasUnreadNotification,
showBadge:
context
.watch<NotificationStateService>()
.hasUnreadNotification,
),
_bottomNavigationItem(
context: context,
@@ -0,0 +1,32 @@
import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/repositories/home_repository.dart';
import 'package:tm_app/data/services/notification_state_service.dart';
/// Service to check for unread notifications and update state
class NotificationCheckService {
final HomeRepository _homeRepository;
final NotificationStateService _notificationStateService;
NotificationCheckService({
required HomeRepository homeRepository,
required NotificationStateService notificationStateService,
}) : _homeRepository = homeRepository,
_notificationStateService = notificationStateService;
Future<void> checkUnreadNotifications() async {
final result = await _homeRepository.checkUnreadNotifications();
switch (result) {
case Ok<Map<String, dynamic>>():
final response = result.value;
final data = response['data'];
_notificationStateService.setHasUnreadNotification(
data is List && data.isNotEmpty,
);
break;
case Error<Map<String, dynamic>>():
_notificationStateService.setHasUnreadNotification(false);
break;
}
}
}
@@ -0,0 +1,17 @@
import 'package:flutter/foundation.dart';
/// Service to manage notification state across the app
class NotificationStateService extends ChangeNotifier {
bool _hasUnreadNotification = false;
bool get hasUnreadNotification => _hasUnreadNotification;
void setHasUnreadNotification(bool value) {
if (_hasUnreadNotification != value) {
_hasUnreadNotification = value;
notifyListeners();
}
}
void clearUnreadNotification() => setHasUnreadNotification(false);
}
-62
View File
@@ -66,45 +66,9 @@ class _MyAppState extends State<MyApp> {
// if (kIsWeb) {
FirebaseMessaging.onMessage.listen((event) {
final overlayCtx = rootNavigatorKey.currentContext;
AppLogger().error('Notification received: ${event.notification?.web}');
AppLogger().error('web link: ${event.notification?.title}');
AppLogger().error('web link: ${event.notification?.web?.link}');
print('Notification received1: ${event.notification?.web.toString()}');
print(
'Notification received1: ${event.notification?.web?.analyticsLabel}',
);
print('web link: ${event.notification?.web?.link}');
print('Notification received: ${event.notification?.title}');
AppLogger().error('notif data: ${event.data}');
AppLogger().error('notif from: ${event.from}');
AppLogger().error('notif type: ${event.messageType}');
AppLogger().error('notif keys: ${event.data.keys}');
AppLogger().info('notif title: ${event.notification?.title}');
AppLogger().info('notif body: ${event.notification?.body}');
AppLogger().info('notif data: ${event.data}');
AppLogger().info('notif from: ${event.from}');
AppLogger().info('notif type: ${event.messageType}');
AppLogger().info('notif keys: ${event.data.keys}');
if (overlayCtx != null) {
if (overlayCtx.mounted) {
AppLogger().error(
'Notification received: ${event.notification?.web}',
);
AppLogger().error(
' link android: ${event.notification?.android?.link}',
);
AppLogger().error(
'android link image: ${event.notification?.android?.imageUrl}',
);
AppLogger().error(
'android link click action: ${event.notification?.android?.clickAction}',
);
AppLogger().error(
'web link image: ${event.notification?.web?.image}',
);
print('Notification received: ${event.notification?.web}');
print('web link: ${event.notification?.web?.link}');
AppToast.notification(
overlayCtx,
event.notification?.title ?? 'New Notification',
@@ -119,32 +83,6 @@ class _MyAppState extends State<MyApp> {
}
void _handleMessage(RemoteMessage message) {
SharedPreferences.getInstance().then((value) {
value.setString('notif_data', message.data.toString());
value.setString('notif_from', message.from.toString());
value.setString('notif_type', message.messageType.toString());
value.setString('notif_keys', message.data.keys.toString());
value.setString('notif_title', message.notification?.title ?? '');
value.setString('notif_body', message.notification?.body ?? '');
value.setString('notif_data', message.contentAvailable.toString());
value.setString('notif_web_link', message.notification?.web?.link ?? '');
value.setString(
'notif_android_link',
message.notification?.android?.link ?? '',
);
value.setString(
'notif_web_image',
message.notification?.web?.image ?? '',
);
value.setString(
'notif_android_image',
message.notification?.android?.imageUrl ?? '',
);
value.setString(
'notif_web_click_action',
message.notification?.web?.link ?? '',
);
});
appRouter.go(
const SplashScreenRoute(navigateToNotification: true).location,
);
+9 -9
View File
@@ -3,11 +3,14 @@ import 'package:tm_app/data/services/model/customer/customer.dart';
import 'package:tm_app/data/services/model/forgot_password_response/forgot_password_response_model.dart';
import 'package:tm_app/data/services/model/verify_otp_response/verify_otp_response_model.dart';
import '../core/utils/error_utils.dart';
import '../core/utils/logger.dart';
import '../core/utils/result.dart';
import '../data/repositories/auth_repository.dart';
import '../data/services/model/login_response/login_response_model.dart';
import '../data/services/model/logout_response/logout_response.dart';
import '../data/services/model/reset_password_response/reset_password_response.dart';
import '../views/forget_password_create/strings/forgot_password_create_strings.dart';
class AuthViewModel with ChangeNotifier {
final AuthRepository _authRepository;
@@ -139,9 +142,6 @@ class AuthViewModel with ChangeNotifier {
}
_isLoading = false;
notifyListeners();
// reset error after notify
_errorMessage = null;
notifyListeners();
}
@@ -151,6 +151,7 @@ class AuthViewModel with ChangeNotifier {
await _authRepository.localLogout();
} catch (e) {
// Handle local logout exceptions gracefully
AppLogger().error('❌ Failed to local logout: $e');
}
_loggedInUser = null;
notifyListeners();
@@ -177,7 +178,7 @@ class AuthViewModel with ChangeNotifier {
notifyListeners();
}
Future<void> forgotPassword(BuildContext context) async {
Future<void> forgotPassword() async {
_isLoadingForgot = true;
_errorMessageForget = null;
successMessage = null;
@@ -214,7 +215,7 @@ class AuthViewModel with ChangeNotifier {
notifyListeners();
}
Future<void> verifyOtp(BuildContext context) async {
Future<void> verifyOtp() async {
_isLoadingOtp = true;
_errorMessageOtp = null;
_verifyOtpSuccess = false;
@@ -273,16 +274,16 @@ class AuthViewModel with ChangeNotifier {
_resetPasswordSuccess = true;
break;
case Error<ResetPasswordResponse>():
_resetPasswordErrorMessage = result.error.toString();
_resetPasswordErrorMessage = ErrorUtils.getErrorMessage(result.error);
_resetPasswordSuccess = false;
break;
}
_isResetPasswordLoading = false;
notifyListeners();
_resetPasswordErrorMessage = null;
notifyListeners();
} else {
_resetPasswordErrorMessage = 'Reset token is required';
_resetPasswordErrorMessage =
ForgotPasswordCreateStrings.passwordResetFailed;
_isResetPasswordLoading = false;
notifyListeners();
}
@@ -325,7 +326,6 @@ class AuthViewModel with ChangeNotifier {
emailController.dispose();
newPasswordController.dispose();
otpController.dispose();
usernameFocus.dispose();
passwordFocus.dispose();
emailFocus.dispose();
+20 -33
View File
@@ -10,15 +10,31 @@ import '../data/repositories/home_repository.dart';
import '../data/services/model/feedback_stats_response/feedback_stat_response.dart';
import '../data/services/model/home/home_response/home_response_model.dart';
import '../data/services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
import '../data/services/notification_state_service.dart';
class HomeViewModel with ChangeNotifier {
final HomeRepository _homeRepository;
final NotificationStateService _notificationStateService;
HomeViewModel({required HomeRepository homeRepository})
: _homeRepository = homeRepository {
HomeViewModel({
required HomeRepository homeRepository,
required NotificationStateService notificationStateService,
}) : _homeRepository = homeRepository,
_notificationStateService = notificationStateService {
_notificationStateService.addListener(_onNotificationStateChanged);
init();
}
void _onNotificationStateChanged() {
notifyListeners();
}
@override
void dispose() {
_notificationStateService.removeListener(_onNotificationStateChanged);
super.dispose();
}
bool _isLoading = false;
String? _errorMessage;
HomeResponseModel? _homeResponse;
@@ -44,8 +60,8 @@ class HomeViewModel with ChangeNotifier {
bool get hasMore => _hasMore;
bool get isRecommendedMode => _isRecommendedMode;
bool _hasUnreadNotification = false;
bool get hasUnreadNotification => _hasUnreadNotification;
bool get hasUnreadNotification =>
_notificationStateService.hasUnreadNotification;
void init() async {
_isLoading = true;
@@ -54,7 +70,6 @@ class HomeViewModel with ChangeNotifier {
await getApprovedStates();
await getYourTenders(reset: true);
await getFeedbackStats();
await checkUnreadNotifications();
if (_errorMessage != null) {
_isLoading = false;
@@ -212,32 +227,4 @@ class HomeViewModel with ChangeNotifier {
double get selfApplyPercent {
return totalCount > 0 ? (selfApplyCount / totalCount) * 100 : 0;
}
Future<void> checkUnreadNotifications() async {
final result = await _homeRepository.checkUnreadNotifications();
switch (result) {
case Ok<Map<String, dynamic>>():
final response = result.value;
final data = response['data'];
if (data is List && data.isNotEmpty) {
_hasUnreadNotification = true;
} else {
_hasUnreadNotification = false;
}
break;
case Error<Map<String, dynamic>>():
_hasUnreadNotification = false;
break;
}
notifyListeners();
}
void setUnreadNotificationFalse() {
_hasUnreadNotification = false;
notifyListeners();
}
}
@@ -4,7 +4,6 @@ import 'package:tm_app/data/repositories/liked_tenders_repository.dart';
import 'package:tm_app/data/repositories/tenders_repository.dart';
import 'package:tm_app/data/services/model/liked_tender_feedback/liked_tender_feedback.dart';
import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_response.dart';
import 'package:tm_app/view_models/home_view_model.dart';
import '../data/services/model/feedback_response/feedback_response.dart';
import '../data/services/model/request_models/tender_feedback_request_model/tender_feedback_request_model.dart';
@@ -12,13 +11,12 @@ import '../data/services/model/request_models/tender_feedback_request_model/tend
class LikedTendersViewModel with ChangeNotifier {
final LikedTendersRepository _likedTendersRepository;
final TendersRepository _tendersRepository;
final HomeViewModel _homeViewModel;
LikedTendersViewModel({
required LikedTendersRepository likedTendersRepository,
required TendersRepository tendersRepository,
required HomeViewModel homeViewModel,
}) : _likedTendersRepository = likedTendersRepository,
_homeViewModel = homeViewModel,
_tendersRepository = tendersRepository;
bool isLoading = false;
@@ -44,10 +42,6 @@ class LikedTendersViewModel with ChangeNotifier {
bool get isSubmitApprovalLoading => _isSubmitApprovalLoading;
bool get isRejectApprovalLoading => _isRejectApprovalLoading;
void updateHome() {
_homeViewModel.init();
}
void clearApprovalErrorMessage() {
_errorMessage = null;
notifyListeners();
+14 -9
View File
@@ -4,18 +4,22 @@ import 'package:tm_app/core/utils/app_toast.dart';
import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/repositories/notification_repository.dart';
import 'package:tm_app/data/services/model/notification__response/notification_response_model.dart';
import 'package:tm_app/view_models/home_view_model.dart';
import 'package:tm_app/data/services/notification_check_service.dart';
import 'package:tm_app/data/services/notification_state_service.dart';
import 'package:tm_app/views/notification/strings/notification_strings.dart';
class NotificationViewModel with ChangeNotifier {
final NotificationsRepository _repository;
final HomeViewModel _homeViewModel;
final NotificationStateService _notificationStateService;
final NotificationCheckService _notificationCheckService;
NotificationViewModel({
required NotificationsRepository repositoryViewModel,
required HomeViewModel homeViewModel,
}) : _repository = repositoryViewModel,
_homeViewModel = homeViewModel {
required NotificationsRepository notificationsRepository,
required NotificationStateService notificationStateService,
required NotificationCheckService notificationCheckService,
}) : _repository = notificationsRepository,
_notificationStateService = notificationStateService,
_notificationCheckService = notificationCheckService {
WidgetsBinding.instance.addPostFrameCallback((_) {
init();
});
@@ -347,7 +351,7 @@ class NotificationViewModel with ChangeNotifier {
}
void setNotificationFalse(BuildContext context) {
_homeViewModel.setUnreadNotificationFalse();
_notificationStateService.clearUnreadNotification();
}
void _checkAndUpdateUnreadState() {
@@ -379,8 +383,9 @@ class NotificationViewModel with ChangeNotifier {
return importantCount > 0;
}
void checkNotifications() {
_homeViewModel.checkUnreadNotifications();
Future<void> checkNotifications() async {
// Re-check unread notifications from server
await _notificationCheckService.checkUnreadNotifications();
}
Future<void> markAsRead({required String notificationId}) async {
-78
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tm_app/data/repositories/auth_repository.dart';
import 'package:tm_app/data/services/model/company_profile_data/company_profile_data.dart';
import 'package:tm_app/data/services/model/company_profile_response/company_profile_response.dart';
@@ -30,65 +29,12 @@ class ProfileViewModel with ChangeNotifier {
CompanyProfileData? _companyProfileData;
bool _loggedOut = false;
// Notification info saved from FCM message in SharedPreferences
String? _notifData;
String? _notifFrom;
String? _notifType;
String? _notifKeys;
String? _notifTitle;
String? _notifBody;
String? _notifWebLink;
String? _notifAndroidLink;
String? _notifWebImage;
String? _notifAndroidImage;
String? _notifWebClickAction;
bool get isLoading => _isLoading;
bool get loggedOut => _loggedOut;
String? get errorMessage => _errorMessage;
ProfileData? get profileData => _profileData;
CompanyProfileData? get companyProfileData => _companyProfileData;
// Expose notification info
String? get notifData => _notifData;
String? get notifFrom => _notifFrom;
String? get notifType => _notifType;
String? get notifKeys => _notifKeys;
String? get notifTitle => _notifTitle;
String? get notifBody => _notifBody;
String? get notifWebLink => _notifWebLink;
String? get notifAndroidLink => _notifAndroidLink;
String? get notifWebImage => _notifWebImage;
String? get notifAndroidImage => _notifAndroidImage;
String? get notifWebClickAction => _notifWebClickAction;
String get notifSummaryText {
final parts = <String>[];
if (_notifTitle != null && _notifTitle!.isNotEmpty)
parts.add('Title: ${_notifTitle!}');
if (_notifBody != null && _notifBody!.isNotEmpty)
parts.add('Body: ${_notifBody!}');
if (_notifType != null && _notifType!.isNotEmpty)
parts.add('Type: ${_notifType!}');
if (_notifFrom != null && _notifFrom!.isNotEmpty)
parts.add('From: ${_notifFrom!}');
if (_notifKeys != null && _notifKeys!.isNotEmpty)
parts.add('Keys: ${_notifKeys!}');
if (_notifWebLink != null && _notifWebLink!.isNotEmpty)
parts.add('Web: ${_notifWebLink!}');
if (_notifAndroidLink != null && _notifAndroidLink!.isNotEmpty)
parts.add('Android: ${_notifAndroidLink!}');
if (_notifWebImage != null && _notifWebImage!.isNotEmpty)
parts.add('WebImg: ${_notifWebImage!}');
if (_notifAndroidImage != null && _notifAndroidImage!.isNotEmpty)
parts.add('AndroidImg: ${_notifAndroidImage!}');
if (_notifWebClickAction != null && _notifWebClickAction!.isNotEmpty)
parts.add('Click: ${_notifWebClickAction!}');
if (_notifData != null && _notifData!.isNotEmpty)
parts.add('Data: ${_notifData!}');
return parts.isEmpty ? '-' : parts.join(' | ');
}
Future<void> getProfile() async {
_isLoading = true;
_errorMessage = null;
@@ -152,28 +98,4 @@ class ProfileViewModel with ChangeNotifier {
_loggedOut = false;
notifyListeners();
}
Future<void> loadNotificationInfoFromPrefs() async {
// Do not toggle global loading; this is a lightweight local load
try {
final prefs = await SharedPreferences.getInstance();
_notifData = prefs.getString('notif_data');
_notifFrom = prefs.getString('notif_from');
_notifType = prefs.getString('notif_type');
_notifKeys = prefs.getString('notif_keys');
_notifTitle = prefs.getString('notif_title');
_notifBody = prefs.getString('notif_body');
_notifWebLink = prefs.getString('notif_web_link');
_notifAndroidLink = prefs.getString('notif_android_link');
_notifWebImage = prefs.getString('notif_web_image');
_notifAndroidImage = prefs.getString('notif_android_image');
_notifWebClickAction = prefs.getString('notif_web_click_action');
} catch (e) {
// Non-fatal; surface as errorMessage so UI can toast if listening
_errorMessage = e.toString();
}
notifyListeners();
_errorMessage = null;
notifyListeners();
}
}
+1 -10
View File
@@ -9,23 +9,18 @@ import '../core/constants/tender_approval_status.dart';
import '../data/services/model/tender_approvals_by_id_response/tender_approvals_by_id_response.dart';
import '../data/services/model/tender_approvals_data/tender_approvals_data.dart';
import '../data/services/model/tender_approvals_toggle_response/tender_approvals_toggle_response.dart';
import 'home_view_model.dart';
import 'tenders_view_model.dart';
class TenderDetailViewModel with ChangeNotifier {
final TenderDetailRepository _tenderDetailRepository;
final TendersRepository _tendersRepository;
final HomeViewModel _homeViewModel;
TenderDetailViewModel({
required TenderDetailRepository tenderDetailRepository,
required TendersRepository tendersRepository,
required TendersViewModel tendersViewModel,
required HomeViewModel homeViewModel,
}) : _tenderDetailRepository = tenderDetailRepository,
_tendersRepository = tendersRepository,
_homeViewModel = homeViewModel;
_tendersRepository = tendersRepository;
bool _isLoading = false;
bool _isSubmitApprovalLoading = false;
@@ -122,10 +117,6 @@ class TenderDetailViewModel with ChangeNotifier {
notifyListeners();
}
void updateHome() async {
_homeViewModel.init();
}
Future<void> getTenderApprovals(String tenderId) async {
_errorMessage = null;
notifyListeners();
+2 -5
View File
@@ -9,15 +9,12 @@ import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_resp
import 'package:tm_app/views/tenders/strings/tenders_strings.dart';
import '../core/utils/result.dart';
import 'home_view_model.dart';
class TendersViewModel with ChangeNotifier {
final TendersRepository _tendersRepository;
TendersViewModel({
required TendersRepository tendersRepository,
required HomeViewModel homeViewModel,
}) : _tendersRepository = tendersRepository;
TendersViewModel({required TendersRepository tendersRepository})
: _tendersRepository = tendersRepository;
bool _isLoading = false;
bool _isLoadingMore = false;
@@ -13,4 +13,5 @@ class ForgotPasswordCreateStrings {
static const String atLeastSpecial =
'• At least one special character: ! @ # \$ % & * ^';
static const String passwordResetSuccess = 'Password reset successfully';
static const String passwordResetFailed = 'Reset Password failed';
}
@@ -128,7 +128,7 @@ class _DesktopForgotPasswordOtpPageState
onPressed:
viewModel.canSubmitOtpPage
? () async {
await viewModel.verifyOtp(context);
await viewModel.verifyOtp();
}
: null,
),
@@ -122,7 +122,7 @@ class _MobileForgotPasswordOtpPageState
onPressed:
viewModel.canSubmitOtpPage
? () async {
await viewModel.verifyOtp(context);
await viewModel.verifyOtp();
}
: null,
),
@@ -127,7 +127,7 @@ class _TabletForgotPasswordOtpPageState
onPressed:
viewModel.canSubmitOtpPage
? () async {
await viewModel.verifyOtp(context);
await viewModel.verifyOtp();
}
: null,
),
@@ -117,9 +117,7 @@ class _DesktopForgotPasswordPageState extends State<DesktopForgotPasswordPage> {
onPressed:
viewModel.isEmailValid
? () async {
await viewModel.forgotPassword(
context,
);
await viewModel.forgotPassword();
}
: null,
),
@@ -110,7 +110,7 @@ class _MobileForgotPasswordPageState extends State<MobileForgotPasswordPage> {
onPressed:
viewModel.isEmailValid
? () async {
await viewModel.forgotPassword(context);
await viewModel.forgotPassword();
}
: null,
),
@@ -111,7 +111,7 @@ class _TabletForgotPasswordPageState extends State<TabletForgotPasswordPage> {
onPressed:
viewModel.isEmailValid
? () async {
await viewModel.forgotPassword(context);
await viewModel.forgotPassword();
}
: null,
),
@@ -49,83 +49,76 @@ class _LikedTendersDesktopPageState extends State<LikedTendersDesktopPage> {
@override
Widget build(BuildContext context) {
return PopScope(
onPopInvokedWithResult: (didPop, result) {
WidgetsBinding.instance.addPostFrameCallback((_) {
viewModel.updateHome();
});
},
child: Scaffold(
key: scaffoldKey,
backgroundColor: AppColors.backgroundColor,
endDrawer: const LikeFiltersDrawer(),
body: SafeArea(
child: Column(
children: [
const DesktopNavigationWidget(currentIndex: 1, haveFilter: true),
const SizedBox(height: 40),
SizedBox(
return Scaffold(
key: scaffoldKey,
backgroundColor: AppColors.backgroundColor,
endDrawer: const LikeFiltersDrawer(),
body: SafeArea(
child: Column(
children: [
const DesktopNavigationWidget(currentIndex: 1, haveFilter: true),
const SizedBox(height: 40),
SizedBox(
width: 740,
child: TabletDesktopAppbar(
title: LikedTendersStrings.likedTenders,
haveFilter: true,
onFilterPressed: () {
scaffoldKey.currentState?.openEndDrawer();
},
),
),
const SizedBox(height: 24),
Expanded(
child: SizedBox(
width: 740,
child: TabletDesktopAppbar(
title: LikedTendersStrings.likedTenders,
haveFilter: true,
onFilterPressed: () {
scaffoldKey.currentState?.openEndDrawer();
child: Consumer<LikedTendersViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading &&
(viewModel.data?.data?.feedback ?? []).isEmpty) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.secondary50,
),
);
}
if (viewModel.errorMessage != null) {
return Center(child: Text(viewModel.errorMessage!));
}
final feedback = viewModel.data?.data?.feedback ?? [];
if (feedback.isEmpty) {
return const Center(
child: Text(LikedTendersStrings.noLikedTenders),
);
}
final currentPage = viewModel.currentPage;
final totalPages = viewModel.data?.data?.meta?.pages ?? 1;
return Column(
children: [
_likedTendersList(feedback, viewModel),
SizedBox(height: 10.0.h()),
_likedTenersListPageControll(
context,
totalPages,
viewModel,
currentPage,
),
SizedBox(height: 30.0.h()),
],
);
},
),
),
const SizedBox(height: 24),
Expanded(
child: SizedBox(
width: 740,
child: Consumer<LikedTendersViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading &&
(viewModel.data?.data?.feedback ?? []).isEmpty) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.secondary50,
),
);
}
if (viewModel.errorMessage != null) {
return Center(child: Text(viewModel.errorMessage!));
}
final feedback = viewModel.data?.data?.feedback ?? [];
if (feedback.isEmpty) {
return const Center(
child: Text(LikedTendersStrings.noLikedTenders),
);
}
final currentPage = viewModel.currentPage;
final totalPages = viewModel.data?.data?.meta?.pages ?? 1;
return Column(
children: [
_likedTendersList(feedback, viewModel),
SizedBox(height: 10.0.h()),
_likedTenersListPageControll(
context,
totalPages,
viewModel,
currentPage,
),
SizedBox(height: 30.0.h()),
],
);
},
),
),
),
],
),
),
],
),
),
);
@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
@@ -62,188 +61,179 @@ class _LikedTendersMobilePageState extends State<LikedTendersMobilePage> {
@override
Widget build(BuildContext context) {
return PopScope(
onPopInvokedWithResult: (didPop, result) {
if (kIsWeb) {
WidgetsBinding.instance.addPostFrameCallback((_) {
viewModel.updateHome();
});
}
},
child: SafeArea(
child: Scaffold(
backgroundColor: AppColors.backgroundColor,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: AppColors.backgroundColor,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
return SafeArea(
child: Scaffold(
backgroundColor: AppColors.backgroundColor,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: AppColors.backgroundColor,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
LikedTendersStrings.likedTenders,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
actions: [
IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
AssetsManager.filter,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
LikedTendersStrings.likedTenders,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
actions: [
IconButton(
icon: SvgPicture.asset(
AssetsManager.filter,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () {
showModalBottomSheet(
backgroundColor: AppColors.backgroundColor,
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
onPressed: () {
showModalBottomSheet(
backgroundColor: AppColors.backgroundColor,
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
builder: (context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 10.0.h()),
Text(
LikedTendersStrings.filter,
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppColors.grey70,
fontSize: 20.0.sp(),
),
),
builder: (context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 10.0.h()),
Text(
LikedTendersStrings.filter,
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppColors.grey70,
fontSize: 20.0.sp(),
),
SizedBox(height: 20.0.h()),
_buildFilterOption('Industry'),
_buildFilterOption('Finance'),
_buildFilterOption('Business'),
_buildFilterOption('IT'),
_buildFilterOption('Software'),
SizedBox(height: 20.0.h()),
BaseButton(
backgroundColor: AppColors.primary2,
borderRadius: 24,
isEnabled: true,
onPressed: () {
Navigator.pop(context);
},
text: 'Apply',
textColor: AppColors.textBlue,
),
],
),
);
},
);
},
),
SizedBox(width: 16.0.w()),
],
),
),
body: Column(
children: [
Expanded(
child: Consumer<LikedTendersViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading &&
(viewModel.data?.data?.feedback ?? []).isEmpty) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.secondary50,
),
);
}
if (viewModel.errorMessage != null) {
return Center(child: Text(viewModel.errorMessage!));
}
final feedback = viewModel.data?.data?.feedback ?? [];
if (feedback.isEmpty) {
return const Center(
child: Text(LikedTendersStrings.noLikedTenders),
);
}
final itemCount =
feedback.length + (viewModel.isLoadingMore ? 1 : 0);
return ListView.builder(
controller: _scrollController,
padding: EdgeInsets.symmetric(
horizontal: 24.0.w(),
vertical: 15.0.h(),
),
itemCount: itemCount,
itemBuilder: (context, index) {
if (index < feedback.length) {
final item = feedback[index];
return Padding(
padding: EdgeInsets.only(bottom: 20.0.h()),
child: Dismissible(
key: ValueKey(item.tenderId ?? index),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(
horizontal: 20.0.w(),
),
child: SvgPicture.asset(
AssetsManager.trash,
width: 32.0.w(),
height: 32.0.h(),
),
),
onDismissed: (_) {
if (item.tenderId != null) {
viewModel.removeTenderById(item.tenderId!);
}
),
SizedBox(height: 20.0.h()),
_buildFilterOption('Industry'),
_buildFilterOption('Finance'),
_buildFilterOption('Business'),
_buildFilterOption('IT'),
_buildFilterOption('Software'),
SizedBox(height: 20.0.h()),
BaseButton(
backgroundColor: AppColors.primary2,
borderRadius: 24,
isEnabled: true,
onPressed: () {
Navigator.pop(context);
},
child: LikedListItem(tender: item.tender!),
text: 'Apply',
textColor: AppColors.textBlue,
),
);
} else {
return Padding(
padding: EdgeInsets.symmetric(vertical: 20.0.h()),
child: Center(
child: SizedBox(
width: 24.0.w(),
height: 24.0.h(),
child: const CircularProgressIndicator(
color: AppColors.secondary50,
strokeWidth: 2,
),
),
),
);
}
},
);
},
),
],
),
);
},
);
},
),
SizedBox(width: 16.0.w()),
],
),
),
body: Column(
children: [
Expanded(
child: Consumer<LikedTendersViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading &&
(viewModel.data?.data?.feedback ?? []).isEmpty) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.secondary50,
),
);
}
if (viewModel.errorMessage != null) {
return Center(child: Text(viewModel.errorMessage!));
}
final feedback = viewModel.data?.data?.feedback ?? [];
if (feedback.isEmpty) {
return const Center(
child: Text(LikedTendersStrings.noLikedTenders),
);
}
final itemCount =
feedback.length + (viewModel.isLoadingMore ? 1 : 0);
return ListView.builder(
controller: _scrollController,
padding: EdgeInsets.symmetric(
horizontal: 24.0.w(),
vertical: 15.0.h(),
),
itemCount: itemCount,
itemBuilder: (context, index) {
if (index < feedback.length) {
final item = feedback[index];
return Padding(
padding: EdgeInsets.only(bottom: 20.0.h()),
child: Dismissible(
key: ValueKey(item.tenderId ?? index),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(
horizontal: 20.0.w(),
),
child: SvgPicture.asset(
AssetsManager.trash,
width: 32.0.w(),
height: 32.0.h(),
),
),
onDismissed: (_) {
if (item.tenderId != null) {
viewModel.removeTenderById(item.tenderId!);
}
},
child: LikedListItem(tender: item.tender!),
),
);
} else {
return Padding(
padding: EdgeInsets.symmetric(vertical: 20.0.h()),
child: Center(
child: SizedBox(
width: 24.0.w(),
height: 24.0.h(),
child: const CircularProgressIndicator(
color: AppColors.secondary50,
strokeWidth: 2,
),
),
),
);
}
},
);
},
),
),
],
),
),
);
}
@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
@@ -50,99 +49,89 @@ class _LikedTendersTabletPageState extends State<LikedTendersTabletPage> {
@override
Widget build(BuildContext context) {
final GlobalKey<ScaffoldState> key = GlobalKey();
return PopScope(
onPopInvokedWithResult: (didPop, result) {
if (kIsWeb) {
WidgetsBinding.instance.addPostFrameCallback((_) {
viewModel.updateHome();
});
}
},
child: Scaffold(
return Scaffold(
key: key,
backgroundColor: AppColors.backgroundColor,
appBar: tabletAppBar(
title: LikedTendersStrings.likedTenders,
key: key,
backgroundColor: AppColors.backgroundColor,
appBar: tabletAppBar(
title: LikedTendersStrings.likedTenders,
key: key,
context: context,
),
drawer: const TabletNavigationWidget(currentIndex: 1),
body: SafeArea(
child: Center(
child: SizedBox(
width: 720,
child: Column(
children: [
TabletDesktopAppbar(
title: LikedTendersStrings.likedTenders,
haveFilter: true,
onFilterPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: AppColors.backgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
context: context,
),
drawer: const TabletNavigationWidget(currentIndex: 1),
body: SafeArea(
child: Center(
child: SizedBox(
width: 720,
child: Column(
children: [
TabletDesktopAppbar(
title: LikedTendersStrings.likedTenders,
haveFilter: true,
onFilterPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: AppColors.backgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
constraints: BoxConstraints(
maxWidth: MediaQuery.sizeOf(context).width,
maxHeight: MediaQuery.sizeOf(context).height,
),
builder: (context) => const LikeFiltersBottomSheet(),
);
},
),
Expanded(
child: Consumer<LikedTendersViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading &&
(viewModel.data?.data?.feedback ?? []).isEmpty) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.secondary50,
),
),
constraints: BoxConstraints(
maxWidth: MediaQuery.sizeOf(context).width,
maxHeight: MediaQuery.sizeOf(context).height,
),
builder: (context) => const LikeFiltersBottomSheet(),
);
}
if (viewModel.errorMessage != null) {
return Center(child: Text(viewModel.errorMessage!));
}
final feedback = viewModel.data?.data?.feedback ?? [];
if (feedback.isEmpty) {
return const Center(
child: Text(LikedTendersStrings.noLikedTenders),
);
}
final currentPage = viewModel.currentPage;
final totalPages = viewModel.data?.data?.meta?.pages ?? 1;
return Column(
children: [
_likedTendersList(feedback, viewModel),
SizedBox(height: 10.0.h()),
_likeTendersListPagesControll(
context,
totalPages,
viewModel,
currentPage,
),
SizedBox(height: 30.0.h()),
],
);
},
),
Expanded(
child: Consumer<LikedTendersViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading &&
(viewModel.data?.data?.feedback ?? []).isEmpty) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.secondary50,
),
);
}
if (viewModel.errorMessage != null) {
return Center(child: Text(viewModel.errorMessage!));
}
final feedback = viewModel.data?.data?.feedback ?? [];
if (feedback.isEmpty) {
return const Center(
child: Text(LikedTendersStrings.noLikedTenders),
);
}
final currentPage = viewModel.currentPage;
final totalPages =
viewModel.data?.data?.meta?.pages ?? 1;
return Column(
children: [
_likedTendersList(feedback, viewModel),
SizedBox(height: 10.0.h()),
_likeTendersListPagesControll(
context,
totalPages,
viewModel,
currentPage,
),
SizedBox(height: 30.0.h()),
],
);
},
),
),
],
),
),
],
),
),
),
+1 -15
View File
@@ -29,9 +29,6 @@ class _MobileProfilePageState extends State<MobileProfilePage> {
viewModel = context.read<ProfileViewModel>();
viewModel.addListener(_profileListener);
// Load notification info saved in SharedPreferences
WidgetsBinding.instance.addPostFrameCallback((_) {
viewModel.loadNotificationInfoFromPrefs();
});
}
void _profileListener() {
@@ -90,18 +87,7 @@ class _MobileProfilePageState extends State<MobileProfilePage> {
),
),
),
SizedBox(height: 8.0.h()),
Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
viewModel.notifSummaryText,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey60,
),
),
),
SizedBox(height: 24.0.h()),
TitleDescription(
title: ProfileStrings.companyName,