Files
tm_app/lib/view_models/notification_view_model.dart
T
AmirReza Jamali 984f2e5139
continuous-integration/drone/push Build is passing
refactor: use GET tenders/recommend and improve tender title UX
Load the Recommended tab from GET /api/v1/tenders/recommend instead of the
AI POST flow with 503 fallback. Fix notification pagination offset, make
tender titles selectable across list and detail views, and ignore .cursor.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 12:23:53 +03:30

452 lines
13 KiB
Dart

// notification_view_model.dart
import 'package:flutter/material.dart';
import 'package:tm_app/core/utils/app_toast.dart';
import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/repositories/notification_repository.dart';
import 'package:tm_app/data/services/model/notification__response/notification_response_model.dart';
import 'package:tm_app/data/services/notification_check_service.dart';
import 'package:tm_app/data/services/notification_state_service.dart';
import 'package:tm_app/views/notification/strings/notification_strings.dart';
class NotificationViewModel with ChangeNotifier {
final NotificationsRepository _repository;
final NotificationStateService _notificationStateService;
final NotificationCheckService _notificationCheckService;
NotificationViewModel({
required NotificationsRepository notificationsRepository,
required NotificationStateService notificationStateService,
required NotificationCheckService notificationCheckService,
}) : _repository = notificationsRepository,
_notificationStateService = notificationStateService,
_notificationCheckService = notificationCheckService {
WidgetsBinding.instance.addPostFrameCallback((_) {
init();
});
}
void init() async {
clearData();
await getNotifications();
await getUnreadNotifications();
await getImportantNotifications();
}
void clearData() {
allNotificationResponse = null;
unreadNotificationResponse = null;
importantNotificationResponse = null;
_errorMessage = null;
currentPageAll = 1;
totalPagesAll = 0;
currentPageUnread = 1;
totalPagesUnread = 0;
currentPageImportant = 1;
totalPagesImportant = 0;
notifyListeners();
}
static const int _pageSize = 10;
String? _errorMessage;
String? get errorMessage => _errorMessage;
// All
NotificationResponseModel? allNotificationResponse;
bool isLoadingAll = false;
bool isMoreLoadingAll = false;
bool hasMoreAll = true;
int currentPageAll = 1;
int totalPagesAll = 0;
// Unread
NotificationResponseModel? unreadNotificationResponse;
bool isLoadingUnread = false;
bool isMoreLoadingUnread = false;
bool hasMoreUnread = true;
int currentPageUnread = 1;
int totalPagesUnread = 0;
// Important
NotificationResponseModel? importantNotificationResponse;
bool isLoadingImportant = false;
bool isMoreLoadingImportant = false;
bool hasMoreImportant = true;
int currentPageImportant = 1;
int totalPagesImportant = 0;
Future<void> getNotifications({
int page = 1,
bool isPagination = false,
}) async {
if (isPagination) {
isMoreLoadingAll = true;
} else {
isLoadingAll = true;
}
notifyListeners();
final int offset = (page - 1) * _pageSize;
final result = await _repository.getNotifications(
limit: _pageSize,
offset: offset,
);
switch (result) {
case Ok<NotificationResponseModel>():
final data = result.value;
if (isPagination && allNotificationResponse != null) {
final old = allNotificationResponse?.data ?? [];
final fresh = data.data ?? [];
allNotificationResponse = allNotificationResponse!.copyWith(
data: [...old, ...fresh],
meta: data.meta,
);
} else {
allNotificationResponse = data;
}
_updateImportantFromAll();
currentPageAll = page;
totalPagesAll = data.meta?.pages ?? 1;
hasMoreAll = currentPageAll < totalPagesAll;
break;
case Error<NotificationResponseModel>():
_errorMessage = result.error.toString();
break;
}
isLoadingAll = false;
isMoreLoadingAll = false;
notifyListeners();
}
Future<void> getUnreadNotifications({
int page = 1,
bool isPagination = false,
}) async {
if (isPagination) {
isMoreLoadingUnread = true;
} else {
isLoadingUnread = true;
}
notifyListeners();
final int offset = (page - 1) * _pageSize;
final result = await _repository.getUnreadNotifications(
limit: _pageSize,
offset: offset,
);
switch (result) {
case Ok<NotificationResponseModel>():
final data = result.value;
if (isPagination && unreadNotificationResponse != null) {
final old = unreadNotificationResponse?.data ?? [];
final fresh = data.data ?? [];
unreadNotificationResponse = data.copyWith(data: [...old, ...fresh]);
} else {
unreadNotificationResponse = data;
}
currentPageUnread = page;
totalPagesUnread = data.meta?.pages ?? 1;
hasMoreUnread = (data.data?.length ?? 0) >= _pageSize;
break;
case Error<NotificationResponseModel>():
_errorMessage = result.error.toString();
break;
}
isLoadingUnread = false;
isMoreLoadingUnread = false;
notifyListeners();
}
Future<void> getImportantNotifications({
int page = 1,
bool isPagination = false,
}) async {
if (isPagination) {
isMoreLoadingImportant = true;
} else {
isLoadingImportant = true;
}
notifyListeners();
final int offset = (page - 1) * _pageSize;
final result = await _repository.getImportantNotifications(
limit: _pageSize,
offset: offset,
);
switch (result) {
case Ok<NotificationResponseModel>():
final data = result.value;
if (isPagination && importantNotificationResponse != null) {
final old = importantNotificationResponse?.data ?? [];
final fresh = data.data ?? [];
importantNotificationResponse = data.copyWith(
data: [...old, ...fresh],
);
} else {
importantNotificationResponse = data;
}
currentPageImportant = page;
totalPagesImportant = data.meta?.pages ?? 1;
hasMoreImportant = (data.data?.length ?? 0) >= _pageSize;
break;
case Error<NotificationResponseModel>():
_errorMessage = result.error.toString();
_updateImportantFromAll();
break;
}
isLoadingImportant = false;
isMoreLoadingImportant = false;
notifyListeners();
}
void _updateImportantFromAll() {
final all = allNotificationResponse?.data ?? [];
final filtered =
all.where((n) {
final p = n.priority ?? '';
return p.toLowerCase() == 'important';
}).toList();
if (allNotificationResponse != null) {
importantNotificationResponse = allNotificationResponse!.copyWith(
data: filtered,
);
} else {
importantNotificationResponse = null;
}
}
Future<void> jumpToPageAll(int page) async {
if (totalPagesAll == 0 || page < 1 || page > totalPagesAll) {
return;
}
await getNotifications(page: page);
}
Future<void> jumpToPageUnread(int page) async {
if (totalPagesUnread == 0 || page < 1 || page > totalPagesUnread) {
return;
}
await getUnreadNotifications(page: page);
}
Future<void> jumpToPageImportant(int page) async {
if (totalPagesImportant == 0 || page < 1 || page > totalPagesImportant) {
return;
}
await getImportantNotifications(page: page);
}
Future<void> markAllAsRead(BuildContext context) async {
isLoadingAll = true;
_errorMessage = null;
notifyListeners();
final result = await _repository.markAllAsRead();
if (!context.mounted) {
return;
}
switch (result) {
case Ok<Map<String, dynamic>>():
final response = result.value;
final success = response['success'] as bool? ?? false;
final message =
response['message'] as String? ??
NotificationStrings.unknownResponse;
if (success) {
if (allNotificationResponse?.data != null) {
final updatedNotifications =
allNotificationResponse!.data!
.map((n) => n.copyWith(seen: true))
.toList();
allNotificationResponse = allNotificationResponse!.copyWith(
data: updatedNotifications,
);
unreadNotificationResponse = unreadNotificationResponse?.copyWith(
data: [],
);
if (importantNotificationResponse?.data != null) {
final updatedImportant =
importantNotificationResponse!.data!
.map((n) => n.copyWith(seen: true))
.toList();
importantNotificationResponse = importantNotificationResponse!
.copyWith(data: updatedImportant);
}
}
// Ensure unread list is empty after marking all as read
_checkAndUpdateUnreadState();
if (context.mounted) {
AppToast.success(
context,
NotificationStrings.allNotificationMarkedAsRead,
);
}
setNotificationFalse(context);
} else {
_errorMessage = message;
if (context.mounted) {
AppToast.error(context, message);
}
}
break;
case Error<Map<String, dynamic>>():
_errorMessage = result.error.toString();
if (context.mounted) {
AppToast.error(context, _errorMessage!);
}
break;
}
isLoadingAll = false;
notifyListeners();
}
String timeAgo(timestamp) {
final int ts = int.tryParse(timestamp.toString()) ?? 0;
final createdAt = DateTime.fromMillisecondsSinceEpoch(ts * 1000).toLocal();
final now = DateTime.now();
final difference = now.difference(createdAt);
if (difference.inMinutes < 1) {
return NotificationStrings.now;
} else if (difference.inMinutes < 60) {
return '${difference.inMinutes} ${NotificationStrings.min}';
} else if (difference.inHours < 24) {
return '${difference.inHours} ${NotificationStrings.hour}';
} else if (difference.inDays == 1) {
return NotificationStrings.yesterday;
} else {
return '${difference.inDays} ${NotificationStrings.daysAgo}';
}
}
void setNotificationFalse(BuildContext context) {
_notificationStateService.clearUnreadNotification();
}
void _checkAndUpdateUnreadState() {
// Check if all notifications are read
final allNotifications = allNotificationResponse?.data ?? [];
final hasUnreadNotifications = allNotifications.any((n) => n.seen != true);
if (!hasUnreadNotifications) {
// Clear unread list if all notifications are read
unreadNotificationResponse = unreadNotificationResponse?.copyWith(
data: [],
);
}
}
int get unreadCount {
return unreadNotificationResponse?.data?.length ?? 0;
}
int get importantCount {
return importantNotificationResponse?.data?.length ?? 0;
}
bool get hasUnreadNotifications {
return unreadCount > 0;
}
bool get hasImportantNotifications {
return importantCount > 0;
}
Future<void> checkNotifications() async {
// Re-check unread notifications from server
await _notificationCheckService.checkUnreadNotifications();
}
Future<void> markAsRead({required String notificationId}) async {
final result = await _repository.markAsRead(notificationId: notificationId);
switch (result) {
case Ok<NotificationResponseModel>():
// Find and update only the specific notification that matches the ID
final updatedNotifications =
allNotificationResponse?.data?.map((n) {
if (n.id == notificationId) {
return n.copyWith(seen: true);
}
return n;
}).toList();
if (updatedNotifications != null) {
allNotificationResponse = allNotificationResponse!.copyWith(
data: updatedNotifications,
);
}
// Update unread list - remove the notification that was marked as read
if (unreadNotificationResponse?.data != null) {
final updatedUnreadList =
unreadNotificationResponse!.data!
.where((n) => n.id != notificationId)
.toList();
unreadNotificationResponse = unreadNotificationResponse!.copyWith(
data: updatedUnreadList,
);
}
// Update important list - mark the notification as seen if it exists
if (importantNotificationResponse?.data != null) {
final updatedImportantList =
importantNotificationResponse!.data!.map((n) {
if (n.id == notificationId) {
return n.copyWith(seen: true);
}
return n;
}).toList();
importantNotificationResponse = importantNotificationResponse!
.copyWith(data: updatedImportantList);
}
// Check if all notifications are now read
_checkAndUpdateUnreadState();
notifyListeners();
checkNotifications();
break;
case Error<NotificationResponseModel>():
_errorMessage = result.error.toString();
break;
}
notifyListeners();
}
}