Merge pull request 'refactors' (#206) from refactors into main
Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/206
This commit is contained in:
@@ -46,3 +46,8 @@ app.*.map.json
|
|||||||
|
|
||||||
# FVM Version Cache
|
# FVM Version Cache
|
||||||
.fvm/
|
.fvm/
|
||||||
|
|
||||||
|
# Firebase Configuration (sensitive files - should not be in version control)
|
||||||
|
**/android/app/google-services.json
|
||||||
|
**/ios/Runner/GoogleService-Info.plist
|
||||||
|
**/lib/firebase_options.dart
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
# 📊 DI & State Management Analysis Summary
|
||||||
|
|
||||||
|
## 🎯 Executive Summary
|
||||||
|
|
||||||
|
**Overall Assessment:** 🟡 **NEEDS SIGNIFICANT REFACTORING**
|
||||||
|
|
||||||
|
Your DI and state management have **good foundational architecture** but suffer from **critical anti-patterns** that will cause maintenance and scalability issues.
|
||||||
|
|
||||||
|
### Key Metrics
|
||||||
|
|
||||||
|
| Metric | Current | Target | Gap |
|
||||||
|
|--------|---------|--------|-----|
|
||||||
|
| Average ViewModel Size | 338 lines | 50-80 lines | 🔴 4x too large |
|
||||||
|
| ViewModel Dependencies | 9 circular | 0 | 🔴 Critical |
|
||||||
|
| Providers at Startup | 25 eager | 5-8 eager | 🟡 3x too many |
|
||||||
|
| Error Handling Pattern | Anti-pattern | Proper state | 🔴 Broken |
|
||||||
|
| State Classes | 0 | 9 | 🔴 Missing |
|
||||||
|
| Test Coverage | 0% | 70%+ | 🔴 Critical |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Current Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ main.dart (App Entry) │
|
||||||
|
│ MultiProvider with ALL 25 providers │
|
||||||
|
│ ❌ Created immediately at startup │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────┴───────────┐
|
||||||
|
│ │
|
||||||
|
┌────▼────┐ ┌─────▼─────┐
|
||||||
|
│ Cores │ │ Services │
|
||||||
|
│ (2) │ │ (8) │
|
||||||
|
└────┬────┘ └─────┬─────┘
|
||||||
|
│ │
|
||||||
|
└───────────┬───────────┘
|
||||||
|
│
|
||||||
|
┌──────▼──────┐
|
||||||
|
│ Repositories│
|
||||||
|
│ (8) │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
┌──────▼──────┐
|
||||||
|
│ ViewModels │
|
||||||
|
│ (9) │
|
||||||
|
│ ❌ Depend │
|
||||||
|
│ on each │
|
||||||
|
│ other! │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
┌──────▼──────┐
|
||||||
|
│ Views │
|
||||||
|
│ (122) │
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Problem #1: ViewModel Circular Dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
TendersViewModel
|
||||||
|
│
|
||||||
|
├──> HomeViewModel ───────┐
|
||||||
|
│ │
|
||||||
|
TenderDetailViewModel │
|
||||||
|
│ │
|
||||||
|
├──> TendersViewModel │
|
||||||
|
├──> HomeViewModel ◄──────┘
|
||||||
|
│
|
||||||
|
LikedTendersViewModel
|
||||||
|
│
|
||||||
|
├──> TendersRepository
|
||||||
|
├──> HomeViewModel
|
||||||
|
|
||||||
|
ProfileViewModel
|
||||||
|
│
|
||||||
|
├──> AuthViewModel
|
||||||
|
└──> AuthRepository
|
||||||
|
|
||||||
|
NotificationViewModel
|
||||||
|
│
|
||||||
|
├──> NotificationsRepository
|
||||||
|
└──> HomeViewModel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Impact:
|
||||||
|
- ❌ **Tight Coupling:** Can't change one without affecting others
|
||||||
|
- ❌ **Testing Nightmare:** Must mock all dependent ViewModels
|
||||||
|
- ❌ **Circular Dependencies:** Potential infinite loops
|
||||||
|
- ❌ **Cascading Rebuilds:** One VM change triggers others
|
||||||
|
- ❌ **Memory Leaks:** ViewModels keep references to each other
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Problem #2: Eager Instantiation Performance
|
||||||
|
|
||||||
|
### Current Startup Sequence:
|
||||||
|
```dart
|
||||||
|
1. App starts
|
||||||
|
2. MultiProvider creates 25 providers instantly:
|
||||||
|
├─ 2 cores (NetworkManager, ThemeProvider)
|
||||||
|
├─ 8 services (AuthService, HomeService, etc.)
|
||||||
|
├─ 8 repositories
|
||||||
|
└─ 9 ViewModels
|
||||||
|
3. HomeViewModel constructor calls init()
|
||||||
|
├─ getApprovedStates() → API call
|
||||||
|
├─ getYourTenders() → API call
|
||||||
|
├─ getFeedbackStats() → API call
|
||||||
|
└─ checkUnreadNotifications() → API call
|
||||||
|
4. User sees splash screen for 2-3 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problems:
|
||||||
|
- 🐌 **Slow startup:** 4 API calls before UI appears
|
||||||
|
- 💾 **Memory waste:** ProfileViewModel created but never used on home screen
|
||||||
|
- 🔋 **Battery drain:** Unnecessary network calls
|
||||||
|
- 📱 **Poor UX:** Long loading time
|
||||||
|
|
||||||
|
### Should Be:
|
||||||
|
```dart
|
||||||
|
1. App starts
|
||||||
|
2. Create only essentials:
|
||||||
|
├─ NetworkManager
|
||||||
|
├─ ThemeProvider
|
||||||
|
└─ AuthViewModel
|
||||||
|
3. Show splash screen (< 500ms)
|
||||||
|
4. Lazy create ViewModels on navigation:
|
||||||
|
├─ Navigate to Home → Create HomeViewModel → Call init()
|
||||||
|
├─ Navigate to Tenders → Create TendersViewModel
|
||||||
|
└─ Navigate to Profile → Create ProfileViewModel
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Problem #3: Error Handling Anti-Pattern
|
||||||
|
|
||||||
|
### Current Code (Repeated in 9 ViewModels):
|
||||||
|
```dart
|
||||||
|
// Set error
|
||||||
|
_errorMessage = result.error.toString();
|
||||||
|
notifyListeners(); // Frame 1: Error shows
|
||||||
|
|
||||||
|
// Immediately clear error
|
||||||
|
_errorMessage = null;
|
||||||
|
notifyListeners(); // Frame 2: Error gone!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeline:
|
||||||
|
```
|
||||||
|
Frame 1 (16ms): User sees error message
|
||||||
|
Frame 2 (32ms): Error disappears
|
||||||
|
Result: Error flashes and vanishes!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Should Be:
|
||||||
|
```dart
|
||||||
|
// Set error with auto-clear
|
||||||
|
_setError(result.error.toString(), duration: 3.seconds);
|
||||||
|
|
||||||
|
// OR in UI with listener
|
||||||
|
if (viewModel.errorMessage != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
AppToast.error(context, viewModel.errorMessage!);
|
||||||
|
viewModel.clearError();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Problem #4: No State Classes
|
||||||
|
|
||||||
|
### Current (Scattered Primitives):
|
||||||
|
```dart
|
||||||
|
// TendersViewModel has 15+ state variables
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool _isLoadingMore = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
String? _feedbackErrorMessage;
|
||||||
|
TendersResponse? _tendersResponse;
|
||||||
|
FeedbackData? _feedbackData;
|
||||||
|
String? _searchQuery;
|
||||||
|
List<FeedbackData> feedbacks = [];
|
||||||
|
int currentPage = 1;
|
||||||
|
int _currentOffset = 0;
|
||||||
|
int _totalPages = 0;
|
||||||
|
bool _hasMoreData = true;
|
||||||
|
DateTimeRange? selectedDateRange;
|
||||||
|
String selectedStatus = 'All';
|
||||||
|
String selectedSort = '';
|
||||||
|
// ... more fields
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problems:
|
||||||
|
- ❌ No single source of truth
|
||||||
|
- ❌ Can have inconsistent state (isLoading=false but _isLoadingMore=true)
|
||||||
|
- ❌ Hard to test
|
||||||
|
- ❌ Can't easily serialize/deserialize
|
||||||
|
- ❌ No immutability guarantees
|
||||||
|
- ❌ Can't use pattern matching
|
||||||
|
|
||||||
|
### Should Be:
|
||||||
|
```dart
|
||||||
|
@freezed
|
||||||
|
class TendersState with _$TendersState {
|
||||||
|
const factory TendersState({
|
||||||
|
@Default(LoadingStatus.idle) LoadingStatus status,
|
||||||
|
@Default([]) List<Tender> tenders,
|
||||||
|
@Default([]) List<FeedbackData> feedbacks,
|
||||||
|
@Default(PaginationInfo()) PaginationInfo pagination,
|
||||||
|
@Default(FilterInfo()) FilterInfo filters,
|
||||||
|
ErrorInfo? error,
|
||||||
|
}) = _TendersState;
|
||||||
|
|
||||||
|
// Derived properties
|
||||||
|
bool get isLoading => status == LoadingStatus.loading;
|
||||||
|
bool get hasError => error != null;
|
||||||
|
bool get canLoadMore => pagination.hasMore && !isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single state variable
|
||||||
|
TendersState _state = TendersState();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Problem #5: Monolithic ViewModels
|
||||||
|
|
||||||
|
### Size Distribution:
|
||||||
|
```
|
||||||
|
YourTendersViewModel: 384 lines 🔴
|
||||||
|
AuthViewModel: 338 lines 🔴
|
||||||
|
HomeViewModel: 244 lines 🔴
|
||||||
|
TendersViewModel: 525 lines 🔴 (WORST)
|
||||||
|
ProfileViewModel: 102 lines 🟡
|
||||||
|
```
|
||||||
|
|
||||||
|
### TendersViewModel Responsibilities:
|
||||||
|
1. Tenders pagination (mobile)
|
||||||
|
2. Tenders pagination (desktop)
|
||||||
|
3. Feedback management
|
||||||
|
4. Filter state management
|
||||||
|
5. Sort state management
|
||||||
|
6. Date range selection
|
||||||
|
7. Search functionality
|
||||||
|
8. Optimistic updates
|
||||||
|
9. Error handling
|
||||||
|
10. Multiple API coordination
|
||||||
|
|
||||||
|
**Violates Single Responsibility Principle!**
|
||||||
|
|
||||||
|
### Should Be Split Into:
|
||||||
|
```
|
||||||
|
tenders/
|
||||||
|
├── tenders_view_model.dart (50 lines - orchestrator)
|
||||||
|
├── tenders_state.dart (40 lines - state class)
|
||||||
|
├── tenders_loader.dart (60 lines - load tenders)
|
||||||
|
├── feedback_manager.dart (70 lines - feedback CRUD)
|
||||||
|
├── tenders_filter.dart (40 lines - filtering)
|
||||||
|
├── tenders_pagination.dart (50 lines - pagination)
|
||||||
|
└── tenders_search.dart (30 lines - search)
|
||||||
|
|
||||||
|
Total: 340 lines split across 7 files (avg 48 lines each) ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Problem #6: Complex Manual State Management
|
||||||
|
|
||||||
|
### Optimistic Updates with Manual Rollback:
|
||||||
|
```dart
|
||||||
|
// 100+ lines of complex state manipulation
|
||||||
|
Future<void> toggleFeedback() async {
|
||||||
|
// 1. Save current state
|
||||||
|
final existing = getFeedbackForTender(tenderId);
|
||||||
|
|
||||||
|
// 2. Optimistic update
|
||||||
|
if (existing == null) {
|
||||||
|
feedbacks.add(optimisticFeedback);
|
||||||
|
} else {
|
||||||
|
if (existing.type == feedbackType) {
|
||||||
|
feedbacks.removeAt(index);
|
||||||
|
} else {
|
||||||
|
feedbacks[index] = optimisticFeedback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// 3. API call
|
||||||
|
final result = await _repository.toggleFeedback();
|
||||||
|
|
||||||
|
// 4. Manual rollback on error
|
||||||
|
switch (result) {
|
||||||
|
case Error():
|
||||||
|
// 20 lines of rollback logic
|
||||||
|
if (existing == null) {
|
||||||
|
feedbacks.removeWhere(...);
|
||||||
|
} else {
|
||||||
|
final currentIndex = feedbacks.indexWhere(...);
|
||||||
|
if (currentIndex != -1) {
|
||||||
|
feedbacks[currentIndex] = existing;
|
||||||
|
} else {
|
||||||
|
feedbacks.add(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problems:
|
||||||
|
- ❌ **Error-prone:** Easy to make mistakes in rollback
|
||||||
|
- ❌ **Hard to test:** Many edge cases
|
||||||
|
- ❌ **Not reusable:** Can't apply to other operations
|
||||||
|
- ❌ **Race conditions:** What if user clicks twice?
|
||||||
|
|
||||||
|
### Should Use:
|
||||||
|
- **Riverpod AsyncValue** (built-in optimistic updates)
|
||||||
|
- **Bloc Optimistic Pattern**
|
||||||
|
- **State machine** with rollback support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Dependency Graph Analysis
|
||||||
|
|
||||||
|
### Current (Problematic):
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ ViewModel Layer (9) │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │Tenders VM│◄────►│ Home VM │ │
|
||||||
|
│ └─────┬────┘ └────┬─────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └────┬───────────┘ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌───────────────┐ │
|
||||||
|
│ │TenderDetail VM│ │
|
||||||
|
│ └───────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ❌ Circular dependencies everywhere │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Should Be (Clean):
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ ViewModel Layer (9) │
|
||||||
|
│ │
|
||||||
|
│ Each ViewModel independent, shares data via: │
|
||||||
|
│ • Repositories (read/write) │
|
||||||
|
│ • Event Bus (notifications) │
|
||||||
|
│ • Navigation params (one-time data) │
|
||||||
|
│ │
|
||||||
|
│ ✅ No direct VM-to-VM dependencies │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Impact Analysis
|
||||||
|
|
||||||
|
### Current Problems Lead To:
|
||||||
|
|
||||||
|
#### Development Issues:
|
||||||
|
- 🐌 **Slow feature development** (tight coupling)
|
||||||
|
- 🐛 **Hard to debug** (state scattered everywhere)
|
||||||
|
- 🧪 **Can't write tests** (VM dependencies)
|
||||||
|
- 🔄 **Refactoring is risky** (changes break other features)
|
||||||
|
|
||||||
|
#### Performance Issues:
|
||||||
|
- 📱 **Slow app startup** (eager instantiation)
|
||||||
|
- 🔋 **Battery drain** (unnecessary API calls)
|
||||||
|
- 💾 **Memory waste** (unused ViewModels loaded)
|
||||||
|
- 🎨 **Unnecessary rebuilds** (cascading VM updates)
|
||||||
|
|
||||||
|
#### User Experience Issues:
|
||||||
|
- ⏳ **Long loading times**
|
||||||
|
- ⚠️ **Errors disappear instantly** (can't read them)
|
||||||
|
- 🐞 **Potential bugs** (complex state management)
|
||||||
|
- 💥 **Crashes** (race conditions in optimistic updates)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Recommended Solution
|
||||||
|
|
||||||
|
### Short-term (1-2 weeks):
|
||||||
|
1. **Remove ViewModel dependencies** → Use shared repositories
|
||||||
|
2. **Implement lazy loading** → ProxyProvider
|
||||||
|
3. **Fix error handling** → Proper error state
|
||||||
|
4. **Add state classes** → Freezed models
|
||||||
|
|
||||||
|
### Long-term (3-4 weeks):
|
||||||
|
1. **Migrate to Riverpod** → Better state management
|
||||||
|
2. **Split large ViewModels** → Follow 10-15 line rule
|
||||||
|
3. **Add comprehensive tests** → 70%+ coverage
|
||||||
|
4. **Implement event bus** → Decouple features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Success Metrics
|
||||||
|
|
||||||
|
After refactoring, you should see:
|
||||||
|
|
||||||
|
| Metric | Before | After | Improvement |
|
||||||
|
|--------|--------|-------|-------------|
|
||||||
|
| App startup time | 2-3s | < 1s | 🟢 3x faster |
|
||||||
|
| Avg ViewModel size | 338 lines | 60 lines | 🟢 5x smaller |
|
||||||
|
| Test coverage | 0% | 70%+ | 🟢 Testable |
|
||||||
|
| Build time | Unknown | Tracked | 🟢 Measurable |
|
||||||
|
| Memory usage | Unknown | Optimized | 🟢 Lower |
|
||||||
|
| Developer velocity | Slow | Fast | 🟢 Faster |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
|
||||||
|
1. **Review** this analysis with your team
|
||||||
|
2. **Prioritize** which issues to tackle first
|
||||||
|
3. **Create tickets** for each refactoring task
|
||||||
|
4. **Start with** ViewModel dependencies (highest impact)
|
||||||
|
5. **Add tests** as you refactor each ViewModel
|
||||||
|
6. **Measure** improvements with analytics
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
# 🔧 DI & State Management Refactoring Plan
|
||||||
|
|
||||||
|
## 🚨 Critical Issues Found
|
||||||
|
|
||||||
|
### 1. ViewModel-to-ViewModel Dependencies
|
||||||
|
**Current:** ViewModels directly depend on other ViewModels
|
||||||
|
**Problem:** Tight coupling, difficult testing, cascading rebuilds
|
||||||
|
**Solution:** Use shared repositories or event bus
|
||||||
|
|
||||||
|
### 2. Eager Instantiation
|
||||||
|
**Current:** All 25 providers created at app startup
|
||||||
|
**Problem:** Slow startup, memory waste
|
||||||
|
**Solution:** Lazy loading with ProxyProvider
|
||||||
|
|
||||||
|
### 3. Error Handling Anti-Pattern
|
||||||
|
**Current:** `_errorMessage = error; notifyListeners(); _errorMessage = null; notifyListeners();`
|
||||||
|
**Problem:** Errors flash and disappear instantly
|
||||||
|
**Solution:** Proper error state with timed clearing
|
||||||
|
|
||||||
|
### 4. Large ViewModels (338-525 lines)
|
||||||
|
**Current:** Monolithic ViewModels with mixed responsibilities
|
||||||
|
**Problem:** Violates SRP, hard to maintain
|
||||||
|
**Solution:** Split into smaller, focused controllers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Refactoring Strategy
|
||||||
|
|
||||||
|
### Phase 1: Remove ViewModel Dependencies (Week 1)
|
||||||
|
|
||||||
|
#### Option A: Shared Repository Pattern (Recommended)
|
||||||
|
```dart
|
||||||
|
// ✅ BEFORE: TendersViewModel depends on HomeViewModel
|
||||||
|
class TendersViewModel {
|
||||||
|
final HomeViewModel homeViewModel; // ❌ BAD
|
||||||
|
|
||||||
|
void updateFeedback() {
|
||||||
|
homeViewModel.init(); // Coupled!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ AFTER: Both share TendersRepository
|
||||||
|
class TendersViewModel {
|
||||||
|
final TendersRepository _repository; // ✅ GOOD
|
||||||
|
|
||||||
|
Future<void> updateFeedback() async {
|
||||||
|
await _repository.refreshTenders();
|
||||||
|
// Repository notifies all listeners
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeViewModel {
|
||||||
|
final TendersRepository _repository; // ✅ Same repository
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
_repository.addListener(_onTendersChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option B: Event Bus Pattern
|
||||||
|
```dart
|
||||||
|
// Create event bus
|
||||||
|
class AppEventBus {
|
||||||
|
final _controller = StreamController<AppEvent>.broadcast();
|
||||||
|
|
||||||
|
Stream<AppEvent> get events => _controller.stream;
|
||||||
|
void fire(AppEvent event) => _controller.add(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
abstract class AppEvent {}
|
||||||
|
class TenderUpdatedEvent extends AppEvent {
|
||||||
|
final String tenderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewModels listen to events
|
||||||
|
class HomeViewModel {
|
||||||
|
void init() {
|
||||||
|
eventBus.events
|
||||||
|
.whereType<TenderUpdatedEvent>()
|
||||||
|
.listen(_onTenderUpdated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Implement Lazy Loading (Week 1)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// ✅ AFTER: Lazy loading with ProxyProvider
|
||||||
|
List<SingleChildWidget> get viewModels {
|
||||||
|
return [
|
||||||
|
// Only AuthViewModel and ThemeProvider are eager
|
||||||
|
ChangeNotifierProvider(
|
||||||
|
create: (context) => AuthViewModel(
|
||||||
|
authRepository: context.read(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Others are lazy - created on first access
|
||||||
|
ChangeNotifierProxyProvider<AuthViewModel, HomeViewModel>(
|
||||||
|
create: (_) => HomeViewModel(homeRepository: _),
|
||||||
|
update: (_, auth, previous) => previous ?? HomeViewModel(...),
|
||||||
|
lazy: true, // ✅ Not created until needed
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove init() from constructor
|
||||||
|
class HomeViewModel {
|
||||||
|
HomeViewModel({required HomeRepository homeRepository})
|
||||||
|
: _homeRepository = homeRepository;
|
||||||
|
// ✅ Don't call init() here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Fix Error Handling (Week 1)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// ✅ Create proper error state
|
||||||
|
class ErrorState {
|
||||||
|
final String message;
|
||||||
|
final DateTime timestamp;
|
||||||
|
final Duration displayDuration;
|
||||||
|
|
||||||
|
bool get shouldDisplay =>
|
||||||
|
DateTime.now().difference(timestamp) < displayDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In ViewModel
|
||||||
|
ErrorState? _errorState;
|
||||||
|
Timer? _errorTimer;
|
||||||
|
|
||||||
|
void _setError(String message) {
|
||||||
|
_errorState = ErrorState(
|
||||||
|
message: message,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
displayDuration: Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
|
||||||
|
_errorTimer?.cancel();
|
||||||
|
_errorTimer = Timer(Duration(seconds: 3), () {
|
||||||
|
_errorState = null;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Introduce State Classes (Week 2)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// ✅ Create immutable state classes with Freezed
|
||||||
|
@freezed
|
||||||
|
class TendersState with _$TendersState {
|
||||||
|
const factory TendersState({
|
||||||
|
@Default(LoadingStatus.idle) LoadingStatus status,
|
||||||
|
@Default([]) List<Tender> tenders,
|
||||||
|
@Default([]) List<FeedbackData> feedbacks,
|
||||||
|
@Default(PaginationState()) PaginationState pagination,
|
||||||
|
ErrorState? error,
|
||||||
|
}) = _TendersState;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LoadingStatus { idle, loading, loadingMore, success, failure }
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class PaginationState with _$PaginationState {
|
||||||
|
const factory PaginationState({
|
||||||
|
@Default(1) int currentPage,
|
||||||
|
@Default(true) bool hasMore,
|
||||||
|
@Default(10) int pageSize,
|
||||||
|
}) = _PaginationState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewModel becomes much simpler
|
||||||
|
class TendersViewModel extends StateNotifier<TendersState> {
|
||||||
|
TendersViewModel() : super(TendersState());
|
||||||
|
|
||||||
|
Future<void> loadTenders() async {
|
||||||
|
state = state.copyWith(status: LoadingStatus.loading);
|
||||||
|
|
||||||
|
final result = await _repository.getTenders();
|
||||||
|
|
||||||
|
state = result.when(
|
||||||
|
ok: (data) => state.copyWith(
|
||||||
|
status: LoadingStatus.success,
|
||||||
|
tenders: data.tenders,
|
||||||
|
),
|
||||||
|
error: (error) => state.copyWith(
|
||||||
|
status: LoadingStatus.failure,
|
||||||
|
error: ErrorState(message: error.toString()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: Split Large ViewModels (Week 2-3)
|
||||||
|
|
||||||
|
#### Before: 525 lines in TendersViewModel
|
||||||
|
```dart
|
||||||
|
class TendersViewModel {
|
||||||
|
// Tenders logic (150 lines)
|
||||||
|
// Feedback logic (150 lines)
|
||||||
|
// Filtering logic (100 lines)
|
||||||
|
// Sorting logic (75 lines)
|
||||||
|
// Pagination logic (50 lines)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### After: Multiple focused files
|
||||||
|
```
|
||||||
|
view_models/tenders/
|
||||||
|
├── tenders_view_model.dart (50 lines - orchestrator)
|
||||||
|
├── controllers/
|
||||||
|
│ ├── tenders_loader.dart (60 lines)
|
||||||
|
│ ├── feedback_controller.dart (70 lines)
|
||||||
|
│ └── pagination_controller.dart (50 lines)
|
||||||
|
├── filters/
|
||||||
|
│ ├── tenders_filter_state.dart (30 lines)
|
||||||
|
│ └── tenders_sort_state.dart (30 lines)
|
||||||
|
└── state/
|
||||||
|
└── tenders_state.dart (40 lines)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Better State Management Options
|
||||||
|
|
||||||
|
### Option 1: Riverpod (Recommended)
|
||||||
|
**Pros:**
|
||||||
|
- ✅ Compile-time safety
|
||||||
|
- ✅ No BuildContext needed
|
||||||
|
- ✅ Built-in testing support
|
||||||
|
- ✅ Auto-dispose
|
||||||
|
- ✅ Family/AutoDispose modifiers
|
||||||
|
- ✅ No ViewModel dependencies
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- 🟡 Learning curve
|
||||||
|
- 🟡 Migration effort
|
||||||
|
|
||||||
|
### Option 2: Bloc/Cubit
|
||||||
|
**Pros:**
|
||||||
|
- ✅ Clear state transitions
|
||||||
|
- ✅ Built-in testing
|
||||||
|
- ✅ Event-driven
|
||||||
|
- ✅ Time-travel debugging
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- 🟡 More boilerplate
|
||||||
|
- 🟡 Steeper learning curve
|
||||||
|
|
||||||
|
### Option 3: Stick with Provider + Refactor
|
||||||
|
**Pros:**
|
||||||
|
- ✅ No migration needed
|
||||||
|
- ✅ Team knows it
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- 🔴 Still requires manual state management
|
||||||
|
- 🔴 No compile-time safety
|
||||||
|
- 🔴 Prone to same issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Migration Effort Estimate
|
||||||
|
|
||||||
|
| Phase | Effort | Priority | Files Affected |
|
||||||
|
|-------|--------|----------|----------------|
|
||||||
|
| 1. Remove VM deps | 3 days | 🔴 Critical | 9 ViewModels, dependencies.dart |
|
||||||
|
| 2. Lazy loading | 1 day | 🟡 High | dependencies.dart, main.dart |
|
||||||
|
| 3. Fix error handling | 2 days | 🟡 High | 9 ViewModels |
|
||||||
|
| 4. State classes | 5 days | 🟢 Medium | 9 ViewModels + new state files |
|
||||||
|
| 5. Split ViewModels | 7 days | 🟢 Medium | 9 ViewModels → 30+ files |
|
||||||
|
|
||||||
|
**Total: ~18 days (3.5 weeks) for complete refactor**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Quick Wins (Can Do Today)
|
||||||
|
|
||||||
|
### 1. Fix Error Handling (2 hours)
|
||||||
|
```dart
|
||||||
|
// Create lib/core/state/error_state.dart
|
||||||
|
// Update all ViewModels to use it
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Remove init() from Constructors (1 hour)
|
||||||
|
```dart
|
||||||
|
// Move all init() calls to explicit method calls
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add State Classes (1 day)
|
||||||
|
```dart
|
||||||
|
// Start with AuthState, HomeState
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Resources
|
||||||
|
|
||||||
|
- [Riverpod Migration Guide](https://riverpod.dev/docs/from_provider)
|
||||||
|
- [Flutter State Management Comparison](https://docs.flutter.dev/data-and-backend/state-mgmt/options)
|
||||||
|
- [Provider Best Practices](https://pub.dev/packages/provider#best-practices)
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
# ViewModel Communication Patterns
|
||||||
|
|
||||||
|
## ❌ The Problem: Direct ViewModel Dependencies
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class NotificationViewModel {
|
||||||
|
final HomeViewModel _homeViewModel; // ❌ BAD: Direct dependency
|
||||||
|
|
||||||
|
void markAsRead() {
|
||||||
|
_homeViewModel.setUnreadNotificationFalse(); // ❌ Tight coupling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why is this bad?**
|
||||||
|
- Tight coupling between ViewModels
|
||||||
|
- Difficult to test in isolation
|
||||||
|
- Circular dependency risk
|
||||||
|
- Violates Single Responsibility Principle
|
||||||
|
- Hard to maintain and scale
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Solution 1: Shared Service Pattern (BEST for your case)
|
||||||
|
|
||||||
|
Create a dedicated service that manages shared state.
|
||||||
|
|
||||||
|
### Implementation:
|
||||||
|
|
||||||
|
**Service:**
|
||||||
|
```dart
|
||||||
|
// lib/data/services/notification_state_service.dart
|
||||||
|
class NotificationStateService extends ChangeNotifier {
|
||||||
|
bool _hasUnreadNotification = false;
|
||||||
|
|
||||||
|
bool get hasUnreadNotification => _hasUnreadNotification;
|
||||||
|
|
||||||
|
void setHasUnreadNotification(bool value) {
|
||||||
|
if (_hasUnreadNotification != value) {
|
||||||
|
_hasUnreadNotification = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**ViewModels depend on the service:**
|
||||||
|
```dart
|
||||||
|
class HomeViewModel {
|
||||||
|
final NotificationStateService _notificationStateService;
|
||||||
|
|
||||||
|
void checkUnreadNotifications() async {
|
||||||
|
// ... fetch from API
|
||||||
|
_notificationStateService.setHasUnreadNotification(hasUnread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationViewModel {
|
||||||
|
final NotificationStateService _notificationStateService;
|
||||||
|
|
||||||
|
void markAsRead() {
|
||||||
|
// ... mark as read
|
||||||
|
_notificationStateService.setHasUnreadNotification(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**UI consumes the service directly:**
|
||||||
|
```dart
|
||||||
|
class NotificationBadge extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final hasUnread = context.watch<NotificationStateService>()
|
||||||
|
.hasUnreadNotification;
|
||||||
|
|
||||||
|
return Badge(showBadge: hasUnread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ No ViewModel-to-ViewModel dependency
|
||||||
|
- ✅ Single source of truth
|
||||||
|
- ✅ Easy to test
|
||||||
|
- ✅ Follows SOLID principles
|
||||||
|
- ✅ Scalable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Solution 2: Repository Pattern
|
||||||
|
|
||||||
|
Move the shared logic to a repository that both ViewModels depend on.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Repository manages the domain logic
|
||||||
|
class NotificationRepository {
|
||||||
|
bool _hasUnread = false;
|
||||||
|
|
||||||
|
Future<bool> checkUnreadNotifications() async {
|
||||||
|
final result = await api.checkUnread();
|
||||||
|
_hasUnread = result.hasUnread;
|
||||||
|
return _hasUnread;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> markAllAsRead() async {
|
||||||
|
await api.markAllAsRead();
|
||||||
|
_hasUnread = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewModels only depend on repository
|
||||||
|
class HomeViewModel {
|
||||||
|
final NotificationRepository _repo;
|
||||||
|
|
||||||
|
void init() async {
|
||||||
|
final hasUnread = await _repo.checkUnreadNotifications();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Solution 3: Event Bus / Stream Pattern
|
||||||
|
|
||||||
|
ViewModels communicate through events without knowing about each other.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Event bus service
|
||||||
|
class AppEventBus {
|
||||||
|
final _controller = StreamController<AppEvent>.broadcast();
|
||||||
|
|
||||||
|
Stream<AppEvent> get events => _controller.stream;
|
||||||
|
void emit(AppEvent event) => _controller.add(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
abstract class AppEvent {}
|
||||||
|
class NotificationReadEvent extends AppEvent {}
|
||||||
|
|
||||||
|
// ViewModel 1: Emits events
|
||||||
|
class NotificationViewModel {
|
||||||
|
final AppEventBus _eventBus;
|
||||||
|
|
||||||
|
void markAsRead() {
|
||||||
|
// ... mark as read
|
||||||
|
_eventBus.emit(NotificationReadEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewModel 2: Listens to events
|
||||||
|
class HomeViewModel {
|
||||||
|
final AppEventBus _eventBus;
|
||||||
|
|
||||||
|
HomeViewModel() {
|
||||||
|
_eventBus.events.listen((event) {
|
||||||
|
if (event is NotificationReadEvent) {
|
||||||
|
_handleNotificationRead();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Solution 4: UI Coordination
|
||||||
|
|
||||||
|
Let the UI layer coordinate between ViewModels.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class NotificationScreen extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final notificationVM = context.watch<NotificationViewModel>();
|
||||||
|
final homeVM = context.watch<HomeViewModel>();
|
||||||
|
|
||||||
|
return Button(
|
||||||
|
onPressed: () {
|
||||||
|
// UI coordinates the interaction
|
||||||
|
notificationVM.markAsRead();
|
||||||
|
homeVM.refreshNotificationBadge();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- Simple interactions
|
||||||
|
- UI-specific coordination
|
||||||
|
- Few ViewModels involved
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Solution 5: Riverpod Pattern (Advanced)
|
||||||
|
|
||||||
|
Use Riverpod's provider dependencies for better state management.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Shared state provider
|
||||||
|
final notificationStateProvider = StateNotifierProvider<NotificationState, bool>(
|
||||||
|
(ref) => NotificationState(),
|
||||||
|
);
|
||||||
|
|
||||||
|
class NotificationState extends StateNotifier<bool> {
|
||||||
|
NotificationState() : super(false);
|
||||||
|
|
||||||
|
void setHasUnread(bool value) => state = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewModels depend on the provider
|
||||||
|
final homeViewModelProvider = Provider((ref) {
|
||||||
|
final notificationState = ref.read(notificationStateProvider.notifier);
|
||||||
|
return HomeViewModel(notificationState);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Comparison Table
|
||||||
|
|
||||||
|
| Pattern | Complexity | Testability | Scalability | Best For |
|
||||||
|
|---------|-----------|-------------|-------------|----------|
|
||||||
|
| Shared Service | Low | High | High | Shared state across app |
|
||||||
|
| Repository | Medium | High | High | Domain logic sharing |
|
||||||
|
| Event Bus | Medium | Medium | High | Loosely coupled events |
|
||||||
|
| UI Coordination | Low | Medium | Low | Simple interactions |
|
||||||
|
| Riverpod | Medium | High | Very High | New projects |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Recommendation for Your Project
|
||||||
|
|
||||||
|
Use **Solution 1: Shared Service Pattern** because:
|
||||||
|
|
||||||
|
1. ✅ Your codebase already uses Provider
|
||||||
|
2. ✅ The notification state is truly shared across the app
|
||||||
|
3. ✅ Simple to implement and understand
|
||||||
|
4. ✅ Easy to test
|
||||||
|
5. ✅ Follows your modular architecture principle
|
||||||
|
6. ✅ No major refactoring required
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Implementation Checklist
|
||||||
|
|
||||||
|
- [x] Create `NotificationStateService`
|
||||||
|
- [x] Add service to dependency injection
|
||||||
|
- [x] Update `HomeViewModel` to use service
|
||||||
|
- [x] Update `NotificationViewModel` to use service
|
||||||
|
- [x] Remove direct ViewModel dependencies
|
||||||
|
- [x] Test the implementation
|
||||||
|
- [ ] Remove the example files when done
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing Example
|
||||||
|
|
||||||
|
```dart
|
||||||
|
test('NotificationViewModel updates shared state', () {
|
||||||
|
final stateService = NotificationStateService();
|
||||||
|
final repository = MockNotificationRepository();
|
||||||
|
final viewModel = NotificationViewModel(
|
||||||
|
notificationsRepository: repository,
|
||||||
|
notificationStateService: stateService,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
viewModel.markAllAsRead();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(stateService.hasUnreadNotification, false);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Key Takeaway
|
||||||
|
|
||||||
|
**ViewModels should NEVER depend on other ViewModels.**
|
||||||
|
|
||||||
|
Instead:
|
||||||
|
- Use a **shared service** for shared state
|
||||||
|
- Use **repositories** for domain logic
|
||||||
|
- Use **event buses** for loose coupling
|
||||||
|
- Let the **UI coordinate** simple interactions
|
||||||
|
|
||||||
@@ -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/repositories/your_tenders_repository.dart';
|
||||||
import 'package:tm_app/data/services/home_service.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/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_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/tender_detail_service.dart';
|
||||||
import 'package:tm_app/data/services/your_tenders_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';
|
import 'package:tm_app/view_models/final_completion_of_documents_view_model.dart';
|
||||||
@@ -30,51 +32,70 @@ import '../../view_models/home_view_model.dart';
|
|||||||
import '../../view_models/profile_view_model.dart';
|
import '../../view_models/profile_view_model.dart';
|
||||||
|
|
||||||
List<SingleChildWidget> get providersRemote {
|
List<SingleChildWidget> get providersRemote {
|
||||||
return [...cores, ...apiClients, ...repositories, ...viewModels, ...others];
|
return [...cores, ...apiClients, ...repositories, ...services, ...viewModels];
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SingleChildWidget> get cores {
|
List<SingleChildWidget> get cores {
|
||||||
return [
|
return [
|
||||||
Provider(create: (context) => NetworkManager(context.read())),
|
Provider(create: (context) => NetworkManager(context.read()), lazy: true),
|
||||||
ChangeNotifierProvider(create: (context) => ThemeProvider()),
|
ChangeNotifierProvider(create: (context) => ThemeProvider(), lazy: true),
|
||||||
|
ChangeNotifierProvider(
|
||||||
|
create: (context) => NotificationStateService(),
|
||||||
|
lazy: true,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SingleChildWidget> get apiClients {
|
List<SingleChildWidget> get apiClients {
|
||||||
return [
|
return [
|
||||||
Provider(create: (context) => AuthService(networkManager: context.read())),
|
Provider(
|
||||||
Provider(create: (context) => HomeService(networkManager: context.read())),
|
create: (context) => AuthService(networkManager: context.read()),
|
||||||
|
lazy: true,
|
||||||
|
),
|
||||||
|
Provider(
|
||||||
|
create: (context) => HomeService(networkManager: context.read()),
|
||||||
|
lazy: true,
|
||||||
|
),
|
||||||
Provider(
|
Provider(
|
||||||
create: (context) => TenderDetailService(networkManager: context.read()),
|
create: (context) => TenderDetailService(networkManager: context.read()),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
|
|
||||||
Provider(
|
Provider(
|
||||||
create: (context) => YourTendersService(networkManager: context.read()),
|
create: (context) => YourTendersService(networkManager: context.read()),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
Provider(
|
Provider(
|
||||||
create: (context) => LikedTendersService(networkManager: context.read()),
|
create: (context) => LikedTendersService(networkManager: context.read()),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
Provider(
|
Provider(
|
||||||
create: (context) => TendersService(networkManager: context.read()),
|
create: (context) => TendersService(networkManager: context.read()),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
Provider(
|
Provider(
|
||||||
create: (context) => ProfileService(networkManager: context.read()),
|
create: (context) => ProfileService(networkManager: context.read()),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
Provider(
|
Provider(
|
||||||
create: (context) => NotificationsService(networkManager: context.read()),
|
create: (context) => NotificationsService(networkManager: context.read()),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SingleChildWidget> get repositories {
|
List<SingleChildWidget> get repositories {
|
||||||
return [
|
return [
|
||||||
Provider(create: (context) => AuthRepository(authService: context.read())),
|
Provider(
|
||||||
|
create: (context) => AuthRepository(authService: context.read()),
|
||||||
|
lazy: true,
|
||||||
|
),
|
||||||
Provider(
|
Provider(
|
||||||
create:
|
create:
|
||||||
(context) => YourTendersRepository(
|
(context) => YourTendersRepository(
|
||||||
yourTendersService: context.read(),
|
yourTendersService: context.read(),
|
||||||
likedTendersService: context.read(),
|
likedTendersService: context.read(),
|
||||||
),
|
),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
Provider(
|
Provider(
|
||||||
create:
|
create:
|
||||||
@@ -82,28 +103,46 @@ List<SingleChildWidget> get repositories {
|
|||||||
homeService: context.read(),
|
homeService: context.read(),
|
||||||
yourTendersService: context.read(),
|
yourTendersService: context.read(),
|
||||||
),
|
),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
Provider(
|
Provider(
|
||||||
create:
|
create:
|
||||||
(context) =>
|
(context) =>
|
||||||
TenderDetailRepository(tenderDetailService: context.read()),
|
TenderDetailRepository(tenderDetailService: context.read()),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
|
|
||||||
Provider(
|
Provider(
|
||||||
create:
|
create:
|
||||||
(context) =>
|
(context) =>
|
||||||
LikedTendersRepository(likedTendersService: context.read()),
|
LikedTendersRepository(likedTendersService: context.read()),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
Provider(
|
Provider(
|
||||||
create: (context) => TendersRepository(tendersService: context.read()),
|
create: (context) => TendersRepository(tendersService: context.read()),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
Provider(
|
Provider(
|
||||||
create: (context) => ProfileRepository(profileService: context.read()),
|
create: (context) => ProfileRepository(profileService: context.read()),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
Provider(
|
Provider(
|
||||||
create:
|
create:
|
||||||
(context) =>
|
(context) =>
|
||||||
NotificationsRepository(notificationsService: context.read()),
|
NotificationsRepository(notificationsService: context.read()),
|
||||||
|
lazy: true,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SingleChildWidget> get services {
|
||||||
|
return [
|
||||||
|
Provider(
|
||||||
|
create:
|
||||||
|
(context) => NotificationCheckService(
|
||||||
|
homeRepository: context.read(),
|
||||||
|
notificationStateService: context.read(),
|
||||||
|
),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -112,61 +151,62 @@ List<SingleChildWidget> get viewModels {
|
|||||||
return [
|
return [
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (context) => AuthViewModel(authRepository: context.read()),
|
create: (context) => AuthViewModel(authRepository: context.read()),
|
||||||
),
|
lazy: true,
|
||||||
ChangeNotifierProvider(
|
|
||||||
create: (context) => HomeViewModel(homeRepository: context.read()),
|
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create:
|
create:
|
||||||
(context) => TendersViewModel(
|
(context) => HomeViewModel(
|
||||||
tendersRepository: context.read(),
|
homeRepository: context.read(),
|
||||||
homeViewModel: context.read(),
|
notificationStateService: context.read(),
|
||||||
),
|
),
|
||||||
|
lazy: true,
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider(
|
||||||
|
create: (context) => TendersViewModel(tendersRepository: context.read()),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create:
|
create:
|
||||||
(context) => TenderDetailViewModel(
|
(context) => TenderDetailViewModel(
|
||||||
tenderDetailRepository: context.read(),
|
tenderDetailRepository: context.read(),
|
||||||
tendersRepository: context.read(),
|
tendersRepository: context.read(),
|
||||||
tendersViewModel: context.read(),
|
|
||||||
homeViewModel: context.read(),
|
|
||||||
),
|
),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
|
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create:
|
create:
|
||||||
(context) =>
|
(context) =>
|
||||||
YourTendersViewModel(yourTendersRepository: context.read()),
|
YourTendersViewModel(yourTendersRepository: context.read()),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create:
|
create:
|
||||||
(context) => LikedTendersViewModel(
|
(context) => LikedTendersViewModel(
|
||||||
likedTendersRepository: context.read(),
|
likedTendersRepository: context.read(),
|
||||||
tendersRepository: context.read(),
|
tendersRepository: context.read(),
|
||||||
homeViewModel: context.read(),
|
|
||||||
),
|
),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create:
|
create:
|
||||||
(context) => ProfileViewModel(
|
(context) => ProfileViewModel(
|
||||||
profileRepository: context.read(),
|
profileRepository: context.read(),
|
||||||
authRepository: context.read(),
|
authRepository: context.read(),
|
||||||
authViewModel: context.read(),
|
|
||||||
),
|
),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (context) => FinalCompletionOfDocumentsViewModel(),
|
create: (context) => FinalCompletionOfDocumentsViewModel(),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create:
|
create:
|
||||||
(context) => NotificationViewModel(
|
(context) => NotificationViewModel(
|
||||||
repositoryViewModel: context.read(),
|
notificationsRepository: context.read(),
|
||||||
homeViewModel: context.read(),
|
notificationStateService: context.read(),
|
||||||
|
notificationCheckService: context.read(),
|
||||||
),
|
),
|
||||||
|
lazy: true,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
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/desktop_shell_page.dart';
|
||||||
import 'package:tm_app/core/routes/app_shell/mobile_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/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/home_view_model.dart';
|
||||||
import 'package:tm_app/view_models/tenders_view_model.dart';
|
import 'package:tm_app/view_models/tenders_view_model.dart';
|
||||||
import 'package:tm_app/views/shared/responsive_builder.dart';
|
import 'package:tm_app/views/shared/responsive_builder.dart';
|
||||||
|
|
||||||
import '../../../view_models/notification_view_model.dart';
|
import '../../../view_models/notification_view_model.dart';
|
||||||
|
|
||||||
class AppShellScreen extends StatelessWidget {
|
class AppShellScreen extends StatefulWidget {
|
||||||
const AppShellScreen({required this.navigationShell, super.key});
|
const AppShellScreen({required this.navigationShell, super.key});
|
||||||
|
|
||||||
final StatefulNavigationShell navigationShell;
|
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) {
|
void _gotoBranch(int index, BuildContext context) {
|
||||||
if (index == navigationShell.currentIndex) {
|
if (index == widget.navigationShell.currentIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate first
|
// Navigate first
|
||||||
navigationShell.goBranch(
|
widget.navigationShell.goBranch(
|
||||||
index,
|
index,
|
||||||
initialLocation: index == navigationShell.currentIndex,
|
initialLocation: index == widget.navigationShell.currentIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Then trigger data loading after navigation is complete
|
// Then trigger data loading after navigation is complete
|
||||||
@@ -45,16 +60,16 @@ class AppShellScreen extends StatelessWidget {
|
|||||||
return ResponsiveBuilder(
|
return ResponsiveBuilder(
|
||||||
mobile: SafeArea(
|
mobile: SafeArea(
|
||||||
child: MobileShellPage(
|
child: MobileShellPage(
|
||||||
navigationShell: navigationShell,
|
navigationShell: widget.navigationShell,
|
||||||
onTap: (value) => _gotoBranch(value, context),
|
onTap: (value) => _gotoBranch(value, context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
tablet: TabletShellPage(
|
tablet: TabletShellPage(
|
||||||
navigationShell: navigationShell,
|
navigationShell: widget.navigationShell,
|
||||||
onTap: (value) => _gotoBranch(value, context),
|
onTap: (value) => _gotoBranch(value, context),
|
||||||
),
|
),
|
||||||
desktop: DesktopShellPage(
|
desktop: DesktopShellPage(
|
||||||
navigationShell: navigationShell,
|
navigationShell: widget.navigationShell,
|
||||||
onTap: (value) => _gotoBranch(value, context),
|
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/constants/assets.dart';
|
||||||
import 'package:tm_app/core/theme/colors.dart';
|
import 'package:tm_app/core/theme/colors.dart';
|
||||||
import 'package:tm_app/core/utils/size_config.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 '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/home/strings/home_strings.dart';
|
||||||
import '../../../views/tenders/strings/tenders_strings.dart';
|
import '../../../views/tenders/strings/tenders_strings.dart';
|
||||||
|
|
||||||
@@ -65,7 +65,10 @@ class MobileShellPage extends StatelessWidget {
|
|||||||
onTap: () => onTap(3),
|
onTap: () => onTap(3),
|
||||||
iconPath: AssetsManager.notify,
|
iconPath: AssetsManager.notify,
|
||||||
activeIconPath: AssetsManager.notifyActive,
|
activeIconPath: AssetsManager.notifyActive,
|
||||||
showBadge: context.watch<HomeViewModel>().hasUnreadNotification,
|
showBadge:
|
||||||
|
context
|
||||||
|
.watch<NotificationStateService>()
|
||||||
|
.hasUnreadNotification,
|
||||||
),
|
),
|
||||||
_bottomNavigationItem(
|
_bottomNavigationItem(
|
||||||
context: context,
|
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);
|
||||||
|
}
|
||||||
@@ -66,45 +66,9 @@ class _MyAppState extends State<MyApp> {
|
|||||||
// if (kIsWeb) {
|
// if (kIsWeb) {
|
||||||
FirebaseMessaging.onMessage.listen((event) {
|
FirebaseMessaging.onMessage.listen((event) {
|
||||||
final overlayCtx = rootNavigatorKey.currentContext;
|
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 != null) {
|
||||||
if (overlayCtx.mounted) {
|
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(
|
AppToast.notification(
|
||||||
overlayCtx,
|
overlayCtx,
|
||||||
event.notification?.title ?? 'New Notification',
|
event.notification?.title ?? 'New Notification',
|
||||||
@@ -119,32 +83,6 @@ class _MyAppState extends State<MyApp> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleMessage(RemoteMessage message) {
|
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(
|
appRouter.go(
|
||||||
const SplashScreenRoute(navigateToNotification: true).location,
|
const SplashScreenRoute(navigateToNotification: true).location,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ 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/forgot_password_response/forgot_password_response_model.dart';
|
||||||
import 'package:tm_app/data/services/model/verify_otp_response/verify_otp_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 '../core/utils/result.dart';
|
||||||
import '../data/repositories/auth_repository.dart';
|
import '../data/repositories/auth_repository.dart';
|
||||||
import '../data/services/model/login_response/login_response_model.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 '../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 {
|
class AuthViewModel with ChangeNotifier {
|
||||||
final AuthRepository _authRepository;
|
final AuthRepository _authRepository;
|
||||||
@@ -139,9 +141,6 @@ class AuthViewModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
// reset error after notify
|
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -151,33 +150,13 @@ class AuthViewModel with ChangeNotifier {
|
|||||||
await _authRepository.localLogout();
|
await _authRepository.localLogout();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Handle local logout exceptions gracefully
|
// Handle local logout exceptions gracefully
|
||||||
|
AppLogger().error('❌ Failed to local logout: $e');
|
||||||
}
|
}
|
||||||
_loggedInUser = null;
|
_loggedInUser = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> forgotPassword() async {
|
||||||
_errorMessage = null;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
final result = await _authRepository.logout();
|
|
||||||
|
|
||||||
switch (result) {
|
|
||||||
case Ok<LogoutResponse>():
|
|
||||||
await localLogout();
|
|
||||||
break;
|
|
||||||
case Error<LogoutResponse>():
|
|
||||||
_errorMessage = result.error.toString();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
_errorMessage = null;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> forgotPassword(BuildContext context) async {
|
|
||||||
_isLoadingForgot = true;
|
_isLoadingForgot = true;
|
||||||
_errorMessageForget = null;
|
_errorMessageForget = null;
|
||||||
successMessage = null;
|
successMessage = null;
|
||||||
@@ -214,7 +193,7 @@ class AuthViewModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> verifyOtp(BuildContext context) async {
|
Future<void> verifyOtp() async {
|
||||||
_isLoadingOtp = true;
|
_isLoadingOtp = true;
|
||||||
_errorMessageOtp = null;
|
_errorMessageOtp = null;
|
||||||
_verifyOtpSuccess = false;
|
_verifyOtpSuccess = false;
|
||||||
@@ -273,16 +252,16 @@ class AuthViewModel with ChangeNotifier {
|
|||||||
_resetPasswordSuccess = true;
|
_resetPasswordSuccess = true;
|
||||||
break;
|
break;
|
||||||
case Error<ResetPasswordResponse>():
|
case Error<ResetPasswordResponse>():
|
||||||
_resetPasswordErrorMessage = result.error.toString();
|
_resetPasswordErrorMessage = ErrorUtils.getErrorMessage(result.error);
|
||||||
_resetPasswordSuccess = false;
|
_resetPasswordSuccess = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_isResetPasswordLoading = false;
|
_isResetPasswordLoading = false;
|
||||||
notifyListeners();
|
|
||||||
_resetPasswordErrorMessage = null;
|
_resetPasswordErrorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} else {
|
} else {
|
||||||
_resetPasswordErrorMessage = 'Reset token is required';
|
_resetPasswordErrorMessage =
|
||||||
|
ForgotPasswordCreateStrings.passwordResetFailed;
|
||||||
_isResetPasswordLoading = false;
|
_isResetPasswordLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -325,7 +304,6 @@ class AuthViewModel with ChangeNotifier {
|
|||||||
emailController.dispose();
|
emailController.dispose();
|
||||||
newPasswordController.dispose();
|
newPasswordController.dispose();
|
||||||
otpController.dispose();
|
otpController.dispose();
|
||||||
|
|
||||||
usernameFocus.dispose();
|
usernameFocus.dispose();
|
||||||
passwordFocus.dispose();
|
passwordFocus.dispose();
|
||||||
emailFocus.dispose();
|
emailFocus.dispose();
|
||||||
|
|||||||
@@ -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/feedback_stats_response/feedback_stat_response.dart';
|
||||||
import '../data/services/model/home/home_response/home_response_model.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/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
|
||||||
|
import '../data/services/notification_state_service.dart';
|
||||||
|
|
||||||
class HomeViewModel with ChangeNotifier {
|
class HomeViewModel with ChangeNotifier {
|
||||||
final HomeRepository _homeRepository;
|
final HomeRepository _homeRepository;
|
||||||
|
final NotificationStateService _notificationStateService;
|
||||||
|
|
||||||
HomeViewModel({required HomeRepository homeRepository})
|
HomeViewModel({
|
||||||
: _homeRepository = homeRepository {
|
required HomeRepository homeRepository,
|
||||||
|
required NotificationStateService notificationStateService,
|
||||||
|
}) : _homeRepository = homeRepository,
|
||||||
|
_notificationStateService = notificationStateService {
|
||||||
|
_notificationStateService.addListener(_onNotificationStateChanged);
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onNotificationStateChanged() {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_notificationStateService.removeListener(_onNotificationStateChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
HomeResponseModel? _homeResponse;
|
HomeResponseModel? _homeResponse;
|
||||||
@@ -44,8 +60,8 @@ class HomeViewModel with ChangeNotifier {
|
|||||||
bool get hasMore => _hasMore;
|
bool get hasMore => _hasMore;
|
||||||
bool get isRecommendedMode => _isRecommendedMode;
|
bool get isRecommendedMode => _isRecommendedMode;
|
||||||
|
|
||||||
bool _hasUnreadNotification = false;
|
bool get hasUnreadNotification =>
|
||||||
bool get hasUnreadNotification => _hasUnreadNotification;
|
_notificationStateService.hasUnreadNotification;
|
||||||
|
|
||||||
void init() async {
|
void init() async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
@@ -54,7 +70,6 @@ class HomeViewModel with ChangeNotifier {
|
|||||||
await getApprovedStates();
|
await getApprovedStates();
|
||||||
await getYourTenders(reset: true);
|
await getYourTenders(reset: true);
|
||||||
await getFeedbackStats();
|
await getFeedbackStats();
|
||||||
await checkUnreadNotifications();
|
|
||||||
|
|
||||||
if (_errorMessage != null) {
|
if (_errorMessage != null) {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
@@ -212,32 +227,4 @@ class HomeViewModel with ChangeNotifier {
|
|||||||
double get selfApplyPercent {
|
double get selfApplyPercent {
|
||||||
return totalCount > 0 ? (selfApplyCount / totalCount) * 100 : 0;
|
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/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_tender_feedback/liked_tender_feedback.dart';
|
||||||
import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_response.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/feedback_response/feedback_response.dart';
|
||||||
import '../data/services/model/request_models/tender_feedback_request_model/tender_feedback_request_model.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 {
|
class LikedTendersViewModel with ChangeNotifier {
|
||||||
final LikedTendersRepository _likedTendersRepository;
|
final LikedTendersRepository _likedTendersRepository;
|
||||||
final TendersRepository _tendersRepository;
|
final TendersRepository _tendersRepository;
|
||||||
final HomeViewModel _homeViewModel;
|
|
||||||
LikedTendersViewModel({
|
LikedTendersViewModel({
|
||||||
required LikedTendersRepository likedTendersRepository,
|
required LikedTendersRepository likedTendersRepository,
|
||||||
required TendersRepository tendersRepository,
|
required TendersRepository tendersRepository,
|
||||||
required HomeViewModel homeViewModel,
|
|
||||||
}) : _likedTendersRepository = likedTendersRepository,
|
}) : _likedTendersRepository = likedTendersRepository,
|
||||||
_homeViewModel = homeViewModel,
|
|
||||||
_tendersRepository = tendersRepository;
|
_tendersRepository = tendersRepository;
|
||||||
|
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
@@ -44,10 +42,6 @@ class LikedTendersViewModel with ChangeNotifier {
|
|||||||
bool get isSubmitApprovalLoading => _isSubmitApprovalLoading;
|
bool get isSubmitApprovalLoading => _isSubmitApprovalLoading;
|
||||||
bool get isRejectApprovalLoading => _isRejectApprovalLoading;
|
bool get isRejectApprovalLoading => _isRejectApprovalLoading;
|
||||||
|
|
||||||
void updateHome() {
|
|
||||||
_homeViewModel.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearApprovalErrorMessage() {
|
void clearApprovalErrorMessage() {
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -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/core/utils/result.dart';
|
||||||
import 'package:tm_app/data/repositories/notification_repository.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/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';
|
import 'package:tm_app/views/notification/strings/notification_strings.dart';
|
||||||
|
|
||||||
class NotificationViewModel with ChangeNotifier {
|
class NotificationViewModel with ChangeNotifier {
|
||||||
final NotificationsRepository _repository;
|
final NotificationsRepository _repository;
|
||||||
final HomeViewModel _homeViewModel;
|
final NotificationStateService _notificationStateService;
|
||||||
|
final NotificationCheckService _notificationCheckService;
|
||||||
|
|
||||||
NotificationViewModel({
|
NotificationViewModel({
|
||||||
required NotificationsRepository repositoryViewModel,
|
required NotificationsRepository notificationsRepository,
|
||||||
required HomeViewModel homeViewModel,
|
required NotificationStateService notificationStateService,
|
||||||
}) : _repository = repositoryViewModel,
|
required NotificationCheckService notificationCheckService,
|
||||||
_homeViewModel = homeViewModel {
|
}) : _repository = notificationsRepository,
|
||||||
|
_notificationStateService = notificationStateService,
|
||||||
|
_notificationCheckService = notificationCheckService {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
@@ -347,7 +351,7 @@ class NotificationViewModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setNotificationFalse(BuildContext context) {
|
void setNotificationFalse(BuildContext context) {
|
||||||
_homeViewModel.setUnreadNotificationFalse();
|
_notificationStateService.clearUnreadNotification();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _checkAndUpdateUnreadState() {
|
void _checkAndUpdateUnreadState() {
|
||||||
@@ -379,8 +383,9 @@ class NotificationViewModel with ChangeNotifier {
|
|||||||
return importantCount > 0;
|
return importantCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkNotifications() {
|
Future<void> checkNotifications() async {
|
||||||
_homeViewModel.checkUnreadNotifications();
|
// Re-check unread notifications from server
|
||||||
|
await _notificationCheckService.checkUnreadNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> markAsRead({required String notificationId}) async {
|
Future<void> markAsRead({required String notificationId}) async {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
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/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_data/company_profile_data.dart';
|
||||||
import 'package:tm_app/data/services/model/company_profile_response/company_profile_response.dart';
|
import 'package:tm_app/data/services/model/company_profile_response/company_profile_response.dart';
|
||||||
@@ -9,20 +8,16 @@ import '../core/utils/result.dart';
|
|||||||
import '../data/repositories/profile_repository.dart';
|
import '../data/repositories/profile_repository.dart';
|
||||||
import '../data/services/model/logout_response/logout_response.dart';
|
import '../data/services/model/logout_response/logout_response.dart';
|
||||||
import '../data/services/model/profile_response/profile_response.dart';
|
import '../data/services/model/profile_response/profile_response.dart';
|
||||||
import 'auth_view_model.dart';
|
|
||||||
|
|
||||||
class ProfileViewModel with ChangeNotifier {
|
class ProfileViewModel with ChangeNotifier {
|
||||||
final ProfileRepository _profileRepository;
|
final ProfileRepository _profileRepository;
|
||||||
final AuthRepository _authRepository;
|
final AuthRepository _authRepository;
|
||||||
final AuthViewModel _authViewModel;
|
|
||||||
|
|
||||||
ProfileViewModel({
|
ProfileViewModel({
|
||||||
required ProfileRepository profileRepository,
|
required ProfileRepository profileRepository,
|
||||||
required AuthRepository authRepository,
|
required AuthRepository authRepository,
|
||||||
required AuthViewModel authViewModel,
|
|
||||||
}) : _profileRepository = profileRepository,
|
}) : _profileRepository = profileRepository,
|
||||||
_authRepository = authRepository,
|
_authRepository = authRepository;
|
||||||
_authViewModel = authViewModel;
|
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
@@ -30,65 +25,12 @@ class ProfileViewModel with ChangeNotifier {
|
|||||||
CompanyProfileData? _companyProfileData;
|
CompanyProfileData? _companyProfileData;
|
||||||
bool _loggedOut = false;
|
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 isLoading => _isLoading;
|
||||||
bool get loggedOut => _loggedOut;
|
bool get loggedOut => _loggedOut;
|
||||||
String? get errorMessage => _errorMessage;
|
String? get errorMessage => _errorMessage;
|
||||||
ProfileData? get profileData => _profileData;
|
ProfileData? get profileData => _profileData;
|
||||||
CompanyProfileData? get companyProfileData => _companyProfileData;
|
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 {
|
Future<void> getProfile() async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
@@ -139,7 +81,7 @@ class ProfileViewModel with ChangeNotifier {
|
|||||||
|
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case Ok<LogoutResponse>():
|
case Ok<LogoutResponse>():
|
||||||
await _authViewModel.localLogout();
|
await _authRepository.localLogout();
|
||||||
_loggedOut = true;
|
_loggedOut = true;
|
||||||
break;
|
break;
|
||||||
case Error<LogoutResponse>():
|
case Error<LogoutResponse>():
|
||||||
@@ -152,28 +94,4 @@ class ProfileViewModel with ChangeNotifier {
|
|||||||
_loggedOut = false;
|
_loggedOut = false;
|
||||||
notifyListeners();
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,23 +9,16 @@ 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_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_data/tender_approvals_data.dart';
|
||||||
import '../data/services/model/tender_approvals_toggle_response/tender_approvals_toggle_response.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 {
|
class TenderDetailViewModel with ChangeNotifier {
|
||||||
final TenderDetailRepository _tenderDetailRepository;
|
final TenderDetailRepository _tenderDetailRepository;
|
||||||
final TendersRepository _tendersRepository;
|
final TendersRepository _tendersRepository;
|
||||||
final HomeViewModel _homeViewModel;
|
|
||||||
|
|
||||||
TenderDetailViewModel({
|
TenderDetailViewModel({
|
||||||
required TenderDetailRepository tenderDetailRepository,
|
required TenderDetailRepository tenderDetailRepository,
|
||||||
required TendersRepository tendersRepository,
|
required TendersRepository tendersRepository,
|
||||||
required TendersViewModel tendersViewModel,
|
|
||||||
required HomeViewModel homeViewModel,
|
|
||||||
}) : _tenderDetailRepository = tenderDetailRepository,
|
}) : _tenderDetailRepository = tenderDetailRepository,
|
||||||
_tendersRepository = tendersRepository,
|
_tendersRepository = tendersRepository;
|
||||||
|
|
||||||
_homeViewModel = homeViewModel;
|
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _isSubmitApprovalLoading = false;
|
bool _isSubmitApprovalLoading = false;
|
||||||
@@ -122,10 +115,6 @@ class TenderDetailViewModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateHome() async {
|
|
||||||
_homeViewModel.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> getTenderApprovals(String tenderId) async {
|
Future<void> getTenderApprovals(String tenderId) async {
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -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 'package:tm_app/views/tenders/strings/tenders_strings.dart';
|
||||||
|
|
||||||
import '../core/utils/result.dart';
|
import '../core/utils/result.dart';
|
||||||
import 'home_view_model.dart';
|
|
||||||
|
|
||||||
class TendersViewModel with ChangeNotifier {
|
class TendersViewModel with ChangeNotifier {
|
||||||
final TendersRepository _tendersRepository;
|
final TendersRepository _tendersRepository;
|
||||||
|
|
||||||
TendersViewModel({
|
TendersViewModel({required TendersRepository tendersRepository})
|
||||||
required TendersRepository tendersRepository,
|
: _tendersRepository = tendersRepository;
|
||||||
required HomeViewModel homeViewModel,
|
|
||||||
}) : _tendersRepository = tendersRepository;
|
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _isLoadingMore = false;
|
bool _isLoadingMore = false;
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ class ForgotPasswordCreateStrings {
|
|||||||
static const String atLeastSpecial =
|
static const String atLeastSpecial =
|
||||||
'• At least one special character: ! @ # \$ % & * ^';
|
'• At least one special character: ! @ # \$ % & * ^';
|
||||||
static const String passwordResetSuccess = 'Password reset successfully';
|
static const String passwordResetSuccess = 'Password reset successfully';
|
||||||
|
static const String passwordResetFailed = 'Reset Password failed';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ class _DesktopForgotPasswordOtpPageState
|
|||||||
onPressed:
|
onPressed:
|
||||||
viewModel.canSubmitOtpPage
|
viewModel.canSubmitOtpPage
|
||||||
? () async {
|
? () async {
|
||||||
await viewModel.verifyOtp(context);
|
await viewModel.verifyOtp();
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class _MobileForgotPasswordOtpPageState
|
|||||||
onPressed:
|
onPressed:
|
||||||
viewModel.canSubmitOtpPage
|
viewModel.canSubmitOtpPage
|
||||||
? () async {
|
? () async {
|
||||||
await viewModel.verifyOtp(context);
|
await viewModel.verifyOtp();
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class _TabletForgotPasswordOtpPageState
|
|||||||
onPressed:
|
onPressed:
|
||||||
viewModel.canSubmitOtpPage
|
viewModel.canSubmitOtpPage
|
||||||
? () async {
|
? () async {
|
||||||
await viewModel.verifyOtp(context);
|
await viewModel.verifyOtp();
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -117,9 +117,7 @@ class _DesktopForgotPasswordPageState extends State<DesktopForgotPasswordPage> {
|
|||||||
onPressed:
|
onPressed:
|
||||||
viewModel.isEmailValid
|
viewModel.isEmailValid
|
||||||
? () async {
|
? () async {
|
||||||
await viewModel.forgotPassword(
|
await viewModel.forgotPassword();
|
||||||
context,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class _MobileForgotPasswordPageState extends State<MobileForgotPasswordPage> {
|
|||||||
onPressed:
|
onPressed:
|
||||||
viewModel.isEmailValid
|
viewModel.isEmailValid
|
||||||
? () async {
|
? () async {
|
||||||
await viewModel.forgotPassword(context);
|
await viewModel.forgotPassword();
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class _TabletForgotPasswordPageState extends State<TabletForgotPasswordPage> {
|
|||||||
onPressed:
|
onPressed:
|
||||||
viewModel.isEmailValid
|
viewModel.isEmailValid
|
||||||
? () async {
|
? () async {
|
||||||
await viewModel.forgotPassword(context);
|
await viewModel.forgotPassword();
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -49,83 +49,76 @@ class _LikedTendersDesktopPageState extends State<LikedTendersDesktopPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopScope(
|
return Scaffold(
|
||||||
onPopInvokedWithResult: (didPop, result) {
|
key: scaffoldKey,
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
backgroundColor: AppColors.backgroundColor,
|
||||||
viewModel.updateHome();
|
endDrawer: const LikeFiltersDrawer(),
|
||||||
});
|
body: SafeArea(
|
||||||
},
|
child: Column(
|
||||||
child: Scaffold(
|
children: [
|
||||||
key: scaffoldKey,
|
const DesktopNavigationWidget(currentIndex: 1, haveFilter: true),
|
||||||
backgroundColor: AppColors.backgroundColor,
|
const SizedBox(height: 40),
|
||||||
endDrawer: const LikeFiltersDrawer(),
|
SizedBox(
|
||||||
body: SafeArea(
|
width: 740,
|
||||||
child: Column(
|
child: TabletDesktopAppbar(
|
||||||
children: [
|
title: LikedTendersStrings.likedTenders,
|
||||||
const DesktopNavigationWidget(currentIndex: 1, haveFilter: true),
|
haveFilter: true,
|
||||||
const SizedBox(height: 40),
|
onFilterPressed: () {
|
||||||
SizedBox(
|
scaffoldKey.currentState?.openEndDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Expanded(
|
||||||
|
child: SizedBox(
|
||||||
width: 740,
|
width: 740,
|
||||||
child: TabletDesktopAppbar(
|
child: Consumer<LikedTendersViewModel>(
|
||||||
title: LikedTendersStrings.likedTenders,
|
builder: (context, viewModel, child) {
|
||||||
haveFilter: true,
|
if (viewModel.isLoading &&
|
||||||
onFilterPressed: () {
|
(viewModel.data?.data?.feedback ?? []).isEmpty) {
|
||||||
scaffoldKey.currentState?.openEndDrawer();
|
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/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -62,188 +61,179 @@ class _LikedTendersMobilePageState extends State<LikedTendersMobilePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopScope(
|
return SafeArea(
|
||||||
onPopInvokedWithResult: (didPop, result) {
|
child: Scaffold(
|
||||||
if (kIsWeb) {
|
backgroundColor: AppColors.backgroundColor,
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
appBar: PreferredSize(
|
||||||
viewModel.updateHome();
|
preferredSize: const Size.fromHeight(60),
|
||||||
});
|
child: AppBar(
|
||||||
}
|
backgroundColor: AppColors.backgroundColor,
|
||||||
},
|
elevation: 0,
|
||||||
child: SafeArea(
|
titleSpacing: 0,
|
||||||
child: Scaffold(
|
leading: IconButton(
|
||||||
backgroundColor: AppColors.backgroundColor,
|
icon: SvgPicture.asset(
|
||||||
appBar: PreferredSize(
|
AssetsManager.arrowLeft,
|
||||||
preferredSize: const Size.fromHeight(60),
|
height: 24.0.w(),
|
||||||
child: AppBar(
|
width: 24.0.w(),
|
||||||
backgroundColor: AppColors.backgroundColor,
|
),
|
||||||
elevation: 0,
|
onPressed: () => Navigator.pop(context),
|
||||||
titleSpacing: 0,
|
),
|
||||||
leading: IconButton(
|
title: Text(
|
||||||
|
LikedTendersStrings.likedTenders,
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.grey70,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 20.0.sp(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
icon: SvgPicture.asset(
|
icon: SvgPicture.asset(
|
||||||
AssetsManager.arrowLeft,
|
AssetsManager.filter,
|
||||||
height: 24.0.w(),
|
height: 24.0.w(),
|
||||||
width: 24.0.w(),
|
width: 24.0.w(),
|
||||||
),
|
),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () {
|
||||||
),
|
showModalBottomSheet(
|
||||||
title: Text(
|
backgroundColor: AppColors.backgroundColor,
|
||||||
LikedTendersStrings.likedTenders,
|
context: context,
|
||||||
style: TextStyle(
|
isScrollControlled: true,
|
||||||
color: AppColors.grey70,
|
shape: const RoundedRectangleBorder(
|
||||||
fontWeight: FontWeight.w600,
|
borderRadius: BorderRadius.vertical(
|
||||||
fontSize: 20.0.sp(),
|
top: Radius.circular(20),
|
||||||
),
|
|
||||||
),
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
builder: (context) {
|
),
|
||||||
return Padding(
|
builder: (context) {
|
||||||
padding: const EdgeInsets.all(16.0),
|
return Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(16.0),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
SizedBox(height: 10.0.h()),
|
children: [
|
||||||
Text(
|
SizedBox(height: 10.0.h()),
|
||||||
LikedTendersStrings.filter,
|
Text(
|
||||||
style: TextStyle(
|
LikedTendersStrings.filter,
|
||||||
fontWeight: FontWeight.w600,
|
style: TextStyle(
|
||||||
color: AppColors.grey70,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 20.0.sp(),
|
color: AppColors.grey70,
|
||||||
),
|
fontSize: 20.0.sp(),
|
||||||
),
|
),
|
||||||
SizedBox(height: 20.0.h()),
|
),
|
||||||
_buildFilterOption('Industry'),
|
SizedBox(height: 20.0.h()),
|
||||||
_buildFilterOption('Finance'),
|
_buildFilterOption('Industry'),
|
||||||
_buildFilterOption('Business'),
|
_buildFilterOption('Finance'),
|
||||||
_buildFilterOption('IT'),
|
_buildFilterOption('Business'),
|
||||||
_buildFilterOption('Software'),
|
_buildFilterOption('IT'),
|
||||||
SizedBox(height: 20.0.h()),
|
_buildFilterOption('Software'),
|
||||||
BaseButton(
|
SizedBox(height: 20.0.h()),
|
||||||
backgroundColor: AppColors.primary2,
|
BaseButton(
|
||||||
borderRadius: 24,
|
backgroundColor: AppColors.primary2,
|
||||||
isEnabled: true,
|
borderRadius: 24,
|
||||||
onPressed: () {
|
isEnabled: true,
|
||||||
Navigator.pop(context);
|
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!);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
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/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -50,99 +49,89 @@ class _LikedTendersTabletPageState extends State<LikedTendersTabletPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final GlobalKey<ScaffoldState> key = GlobalKey();
|
final GlobalKey<ScaffoldState> key = GlobalKey();
|
||||||
return PopScope(
|
return Scaffold(
|
||||||
onPopInvokedWithResult: (didPop, result) {
|
key: key,
|
||||||
if (kIsWeb) {
|
backgroundColor: AppColors.backgroundColor,
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
appBar: tabletAppBar(
|
||||||
viewModel.updateHome();
|
title: LikedTendersStrings.likedTenders,
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Scaffold(
|
|
||||||
key: key,
|
key: key,
|
||||||
backgroundColor: AppColors.backgroundColor,
|
context: context,
|
||||||
appBar: tabletAppBar(
|
),
|
||||||
title: LikedTendersStrings.likedTenders,
|
drawer: const TabletNavigationWidget(currentIndex: 1),
|
||||||
key: key,
|
body: SafeArea(
|
||||||
context: context,
|
child: Center(
|
||||||
),
|
child: SizedBox(
|
||||||
drawer: const TabletNavigationWidget(currentIndex: 1),
|
width: 720,
|
||||||
body: SafeArea(
|
child: Column(
|
||||||
child: Center(
|
children: [
|
||||||
child: SizedBox(
|
TabletDesktopAppbar(
|
||||||
width: 720,
|
title: LikedTendersStrings.likedTenders,
|
||||||
child: Column(
|
haveFilter: true,
|
||||||
children: [
|
onFilterPressed: () {
|
||||||
TabletDesktopAppbar(
|
showModalBottomSheet(
|
||||||
title: LikedTendersStrings.likedTenders,
|
context: context,
|
||||||
haveFilter: true,
|
isScrollControlled: true,
|
||||||
onFilterPressed: () {
|
backgroundColor: AppColors.backgroundColor,
|
||||||
showModalBottomSheet(
|
shape: const RoundedRectangleBorder(
|
||||||
context: context,
|
borderRadius: BorderRadius.vertical(
|
||||||
isScrollControlled: true,
|
top: Radius.circular(20),
|
||||||
backgroundColor: AppColors.backgroundColor,
|
),
|
||||||
shape: const RoundedRectangleBorder(
|
),
|
||||||
borderRadius: BorderRadius.vertical(
|
constraints: BoxConstraints(
|
||||||
top: Radius.circular(20),
|
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,
|
if (viewModel.errorMessage != null) {
|
||||||
),
|
return Center(child: Text(viewModel.errorMessage!));
|
||||||
builder: (context) => const LikeFiltersBottomSheet(),
|
}
|
||||||
|
|
||||||
|
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()),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ class _MobileProfilePageState extends State<MobileProfilePage> {
|
|||||||
viewModel = context.read<ProfileViewModel>();
|
viewModel = context.read<ProfileViewModel>();
|
||||||
viewModel.addListener(_profileListener);
|
viewModel.addListener(_profileListener);
|
||||||
// Load notification info saved in SharedPreferences
|
// Load notification info saved in SharedPreferences
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
viewModel.loadNotificationInfoFromPrefs();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _profileListener() {
|
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()),
|
SizedBox(height: 24.0.h()),
|
||||||
TitleDescription(
|
TitleDescription(
|
||||||
title: ProfileStrings.companyName,
|
title: ProfileStrings.companyName,
|
||||||
|
|||||||
Reference in New Issue
Block a user