import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tm_app/core/utils/device_utils.dart'; import 'package:tm_app/core/utils/logger.dart'; import 'package:tm_app/data/repositories/tenders_repository.dart'; import 'package:tm_app/data/services/model/feedback_data/feedback_data.dart'; import 'package:tm_app/data/services/model/feedback_response/feedback_response.dart'; import 'package:tm_app/data/services/model/tender_data/tender_data.dart'; import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_response.dart'; import 'package:tm_app/views/tenders/strings/tenders_strings.dart'; import '../core/utils/result.dart'; class TendersViewModel with ChangeNotifier { final TendersRepository _tendersRepository; TendersViewModel({required TendersRepository tendersRepository}) : _tendersRepository = tendersRepository; bool _isLoading = false; bool _isLoadingMore = false; String? _errorMessage; String? _feedbackErrorMessage; TendersResponse? _tendersResponse; FeedbackData? _feedbackData; String? _searchQuery; bool get isLoading => _isLoading; bool get isLoadingMore => _isLoadingMore; String? get errorMessage => _errorMessage; String? get feedbackErrorMessage => _feedbackErrorMessage; TendersResponse? get tendersResponse => _tendersResponse; FeedbackData? get feedbackData => _feedbackData; String? get searchQuery => _searchQuery; List feedbacks = []; static const int _pageSize = 10; // --- Offset pagination state --- int _currentOffset = 0; int _lastValidPages = 1; int _lastValidLimit = _pageSize; bool _hasMoreData = true; bool get hasMoreData => _hasMoreData; /// 0-based current page index. int get currentPageIndex { final meta = _tendersResponse?.meta; final limit = meta?.limit ?? _lastValidLimit; if (limit > 0) { return _currentOffset ~/ limit; } if (meta?.offset != null && limit > 0) { return meta!.offset! ~/ limit; } if (meta?.page != null) { return meta!.page! - 1; } return 0; } /// 1-based current page (for display). int get currentPage => currentPageIndex + 1; int get totalPages { final pages = _tendersResponse?.meta?.pages; if (pages != null && pages > 0) { return pages; } return _lastValidPages; } // Filter States DateTimeRange? selectedDateRange; String selectedStatus = 'All'; String selectedSort = ''; String sortBy = ''; String sortOrder = ''; final _fmt = DateFormat('yyyy/MM/dd'); String get startDateFormatted => selectedDateRange?.start != null ? _fmt.format(selectedDateRange!.start) : 'YYYY/MM/DD'; String get endDateFormatted => selectedDateRange?.end != null ? _fmt.format(selectedDateRange!.end) : 'YYYY/MM/DD'; String get startDateUnix => selectedDateRange?.start != null ? (selectedDateRange!.start.millisecondsSinceEpoch ~/ 1000).toString() : ''; String get endDateUnix => selectedDateRange?.end != null ? (selectedDateRange!.end.millisecondsSinceEpoch ~/ 1000).toString() : ''; // --- Filter Actions --- void setDateRange(DateTimeRange? range) { selectedDateRange = range; notifyListeners(); } void setStatus(String status) { _clearAllData(); selectedStatus = status; notifyListeners(); } void setSort(String sort) { _clearAllData(); selectedSort = sort; if (sort == TendersStrings.minimumTime) { sortBy = 'submission_deadline'; sortOrder = 'asc'; } else if (sort == TendersStrings.mostOfTheTime) { sortBy = 'submission_deadline'; sortOrder = 'desc'; } notifyListeners(); } void _clearAllData() { _tendersResponse = null; _errorMessage = null; _resetPagination(); sortBy = ''; sortOrder = ''; selectedSort = ''; selectedStatus = ''; notifyListeners(); } void _resetPagination() { _currentOffset = 0; _hasMoreData = true; } void init(BuildContext context) async { _isLoading = true; notifyListeners(); clearAll(); await getTenders(context: context); } /// Get feedback for a specific tender FeedbackData? getFeedbackForTender(String tenderId) { try { return feedbacks.firstWhere((f) => f.tenderId == tenderId); } catch (e) { return null; } } /// Check if feedback exists for a specific tender bool hasFeedbackForTender(String tenderId) { return feedbacks.any((f) => f.tenderId == tenderId); } Future getTenders({ required BuildContext context, String? query, }) async { _isLoading = true; notifyListeners(); if (DeviceUtils.isDesktop(context)) { await getTendersDesktop(query: query); } else { await getTendersMobile(query: query); } } Future getTendersMobile({String? query}) async { _resetPagination(); _searchQuery = query; final result = await _tendersRepository.getTenders( limit: _pageSize, offset: _currentOffset, query: query, sortBy: '', sortOrder: '', ); _handleListResult(result, fetchAllFeedbacks: false); _isLoading = false; notifyListeners(); _errorMessage = null; notifyListeners(); } Future getTendersDesktop({ String? query, String? sortBy, String? sortOrder, String? publicationDateFrom, String? publicationDateTo, }) async { _isLoading = true; notifyListeners(); _resetPagination(); _searchQuery = query; final result = await _tendersRepository.getTenders( limit: _pageSize, offset: _currentOffset, query: query, sortBy: sortBy ?? '', sortOrder: sortOrder ?? '', publicationDateFrom: publicationDateFrom ?? '', publicationDateTo: publicationDateTo ?? '', ); _handleListResult(result, fetchAllFeedbacks: true); _isLoading = false; notifyListeners(); _errorMessage = null; notifyListeners(); } void _handleListResult( Result result, { required bool fetchAllFeedbacks, }) { switch (result) { case Ok(): _tendersResponse = result.value; final meta = _tendersResponse?.meta; if (meta?.pages != null && meta!.pages! > 0) { _lastValidPages = meta.pages!; } if (meta?.limit != null && meta!.limit! > 0) { _lastValidLimit = meta.limit!; } if (meta?.offset != null) { _currentOffset = meta!.offset!; } final tenders = _tendersResponse?.data?.tenders ?? []; _hasMoreData = meta?.hasMore ?? (tenders.length >= _pageSize); if (tenders.isNotEmpty) { if (fetchAllFeedbacks) { for (final tender in tenders) { if (tender.id != null) { getTenderFeedback(tender.id!); } } } else { getTenderFeedback(tenders.first.id!); } } break; case Error(): _handleListError(result.error); break; } } void _handleListError(Exception error) { _errorMessage = error.toString(); } Future loadMoreTenders() async { if (_isLoadingMore || !_hasMoreData) { return; } _isLoadingMore = true; _errorMessage = null; notifyListeners(); final meta = _tendersResponse?.meta; final limit = meta?.limit ?? _pageSize; _currentOffset += limit; final result = await _tendersRepository.getTenders( limit: _pageSize, offset: _currentOffset, query: _searchQuery, sortBy: sortBy, sortOrder: sortOrder, ); switch (result) { case Ok(): final newTenders = result.value.data?.tenders ?? []; if (newTenders.isNotEmpty) { final existingTenders = _tendersResponse?.data?.tenders ?? []; final combined = [...existingTenders, ...newTenders]; _tendersResponse = result.value.copyWith( data: result.value.data?.copyWith(tenders: combined), ); final newMeta = result.value.meta; if (newMeta?.pages != null && newMeta!.pages! > 0) { _lastValidPages = newMeta.pages!; } if (newMeta?.limit != null && newMeta!.limit! > 0) { _lastValidLimit = newMeta.limit!; } _hasMoreData = newMeta?.hasMore ?? (newTenders.length >= _pageSize); } else { _hasMoreData = false; } break; case Error(): final limit = meta?.limit ?? _pageSize; _currentOffset = (_currentOffset - limit).clamp(0, 1 << 30); _handleListError(result.error); break; } _isLoadingMore = false; notifyListeners(); _errorMessage = null; notifyListeners(); } Future getTenderFeedback(String tenderId) async { _errorMessage = null; notifyListeners(); final result = await _tendersRepository.getTenderFeedback( tenderId: tenderId, ); switch (result) { case Ok(): _feedbackData = result.value.data; if (_feedbackData != null) { final index = feedbacks.indexWhere( (f) => f.tenderId == _feedbackData!.tenderId, ); if (index != -1) { feedbacks[index] = _feedbackData!; AppLogger().info( 'Updated feedback for tender: ${_feedbackData!.tenderId}', ); } else { feedbacks.add(_feedbackData!); AppLogger().info( 'Added new feedback for tender: ${_feedbackData!.tenderId}', ); } AppLogger().info('Total feedbacks: ${feedbacks.length}'); } break; case Error(): _errorMessage = result.error.toString(); break; } notifyListeners(); _errorMessage = null; notifyListeners(); } Future toogleFeedback({ required String tenderId, required String feedbackType, }) async { _feedbackErrorMessage = null; final existingFeedback = getFeedbackForTender(tenderId); final existingFeedbackIndex = feedbacks.indexWhere( (f) => f.tenderId == tenderId, ); FeedbackData? optimisticFeedback; if (existingFeedback == null) { optimisticFeedback = FeedbackData( companyId: null, createdAt: DateTime.now().millisecondsSinceEpoch, customerId: null, feedbackType: feedbackType, id: null, tenderId: tenderId, tender: null, updatedAt: DateTime.now().millisecondsSinceEpoch, ); feedbacks.add(optimisticFeedback); } else { if (existingFeedback.feedbackType == feedbackType) { if (existingFeedbackIndex != -1) { feedbacks.removeAt(existingFeedbackIndex); } optimisticFeedback = null; } else { optimisticFeedback = existingFeedback.copyWith( feedbackType: feedbackType, updatedAt: DateTime.now().millisecondsSinceEpoch, ); if (existingFeedbackIndex != -1) { feedbacks[existingFeedbackIndex] = optimisticFeedback; } } } notifyListeners(); final result = await _tendersRepository.toggleFeedback( tenderId: tenderId, feedbackType: feedbackType, ); switch (result) { case Ok(): _feedbackData = result.value.data; if (_feedbackData != null) { final index = feedbacks.indexWhere( (f) => f.tender == _feedbackData!.tender, ); if (index != -1) { feedbacks[index] = _feedbackData!; } else { feedbacks.add(_feedbackData!); } } else { feedbacks.removeWhere((f) => f.tenderId == tenderId); } break; case Error(): if (existingFeedback == null) { feedbacks.removeWhere((f) => f.tenderId == tenderId); } else { final currentIndex = feedbacks.indexWhere( (f) => f.tenderId == tenderId, ); if (currentIndex != -1) { feedbacks[currentIndex] = existingFeedback; } else { feedbacks.add(existingFeedback); } } _feedbackErrorMessage = result.error.toString(); break; } notifyListeners(); _feedbackErrorMessage = null; notifyListeners(); } void clearFeedbacks() { feedbacks.clear(); _feedbackData = null; _feedbackErrorMessage = null; notifyListeners(); } void clearTenders() { _tendersResponse = null; _errorMessage = null; _resetPagination(); notifyListeners(); } Future callWithSortAndStatus() async { await getTendersDesktop( sortBy: sortBy, sortOrder: sortOrder, publicationDateFrom: startDateUnix, publicationDateTo: endDateUnix, ); } Future searchTenders(String query) async { await getTendersDesktop( query: query.trim(), sortBy: sortBy, sortOrder: sortOrder, publicationDateFrom: startDateUnix, publicationDateTo: endDateUnix, ); } void clearAll() { clearTenders(); clearFeedbacks(); _searchQuery = null; selectedSort = ''; selectedStatus = ''; selectedDateRange = null; sortBy = ''; sortOrder = ''; notifyListeners(); } /// [selected] is the absolute 0-based target page index. Future handlePaginationChange(int selected) async { if (selected < 0) { return; } final maxPage = totalPages - 1; if (maxPage >= 0 && selected > maxPage) { return; } if (selected == currentPageIndex) { return; } await _goToOffsetPage(selected); } Future _goToOffsetPage(int pageIndex) async { final limit = _tendersResponse?.meta?.limit ?? _pageSize; _currentOffset = pageIndex * limit; await _fetchPage(); } Future _fetchPage() async { _isLoading = true; notifyListeners(); final result = await _tendersRepository.getTenders( limit: _pageSize, offset: _currentOffset, query: _searchQuery, sortBy: sortBy, sortOrder: sortOrder, publicationDateFrom: startDateUnix, publicationDateTo: endDateUnix, ); _handleListResult(result, fetchAllFeedbacks: true); _isLoading = false; notifyListeners(); _errorMessage = null; notifyListeners(); } /// Kept for backwards compatibility with existing callers. /// Accepts a 1-based page number. void jumpToPage(int page) { handlePaginationChange(page - 1); } }