537 lines
17 KiB
Dart
537 lines
17 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:flutter_riverpod/legacy.dart';
|
||
import 'package:taxglide/controller/api_consts.dart';
|
||
import 'package:taxglide/controller/api_repository.dart';
|
||
import 'package:taxglide/model/chat_model.dart';
|
||
import 'package:taxglide/model/chat_profile_model.dart';
|
||
import 'package:taxglide/model/city_model.dart';
|
||
import 'package:taxglide/model/count_model.dart';
|
||
import 'package:taxglide/model/country_model.dart';
|
||
import 'package:taxglide/model/dash_board_model.dart';
|
||
import 'package:taxglide/model/detail_model.dart';
|
||
import 'package:taxglide/model/employeeprofile_model.dart';
|
||
import 'package:taxglide/model/login_model.dart';
|
||
import 'package:taxglide/model/notification_model.dart';
|
||
import 'package:taxglide/model/notificationcount_model.dart';
|
||
import 'package:taxglide/model/profile_get_model.dart';
|
||
import 'package:taxglide/model/serivce_list_model.dart';
|
||
import 'package:taxglide/model/service_list_history_model.dart';
|
||
import 'package:taxglide/model/signup_model.dart';
|
||
import 'package:taxglide/model/staff_model.dart';
|
||
import 'package:taxglide/model/terms_model.dart';
|
||
|
||
final apiRepositoryProvider = Provider((ref) => ApiRepository());
|
||
|
||
//login controller
|
||
final loginProvider =
|
||
StateNotifierProvider<LoginNotifier, AsyncValue<Map<String, dynamic>>>(
|
||
(ref) => LoginNotifier(ref.read(apiRepositoryProvider)),
|
||
);
|
||
|
||
class LoginNotifier extends StateNotifier<AsyncValue<Map<String, dynamic>>> {
|
||
final ApiRepository _apiRepository;
|
||
|
||
LoginNotifier(this._apiRepository) : super(const AsyncValue.data({}));
|
||
|
||
Future<void> login(String mobile) async {
|
||
if (!mounted) return;
|
||
state = const AsyncValue.loading();
|
||
try {
|
||
final model = LoginModel(mobile: mobile);
|
||
final result = await _apiRepository.loginUser(model);
|
||
if (mounted) state = AsyncValue.data(result);
|
||
} catch (e, st) {
|
||
if (mounted) state = AsyncValue.error(e, st);
|
||
}
|
||
}
|
||
|
||
Future<void> verifyOtp(String mobile, String otp) async {
|
||
if (!mounted) return;
|
||
state = const AsyncValue.loading();
|
||
try {
|
||
final model = LoginModel(mobile: mobile, otp: otp);
|
||
final result = await _apiRepository.verifyOtp(model);
|
||
if (mounted) state = AsyncValue.data(result);
|
||
} catch (e, st) {
|
||
if (mounted) state = AsyncValue.error(e, st);
|
||
}
|
||
}
|
||
}
|
||
|
||
//signup controller
|
||
final signupProvider =
|
||
StateNotifierProvider<SignupNotifier, AsyncValue<Map<String, dynamic>>>(
|
||
(ref) => SignupNotifier(ref.read(apiRepositoryProvider)),
|
||
);
|
||
|
||
class SignupNotifier extends StateNotifier<AsyncValue<Map<String, dynamic>>> {
|
||
final ApiRepository _apiRepository;
|
||
|
||
SignupNotifier(this._apiRepository) : super(const AsyncValue.data({}));
|
||
|
||
Future<void> signup(String name, String contactNumber, String email) async {
|
||
if (!mounted) return;
|
||
state = const AsyncValue.loading();
|
||
try {
|
||
final model = SignupModel(
|
||
name: name,
|
||
contactNumber: contactNumber,
|
||
email: email,
|
||
);
|
||
final result = await _apiRepository.signupUser(model);
|
||
if (mounted) state = AsyncValue.data(result);
|
||
} catch (e, st) {
|
||
if (mounted) state = AsyncValue.error(e, st);
|
||
}
|
||
}
|
||
|
||
// Verify OTP for signup
|
||
Future<void> verifySignupOtp(String mobile, String otp) async {
|
||
if (!mounted) return;
|
||
state = const AsyncValue.loading();
|
||
try {
|
||
final result = await _apiRepository.verifySignupOtp(mobile, otp);
|
||
if (mounted) state = AsyncValue.data(result);
|
||
} catch (e, st) {
|
||
if (mounted) state = AsyncValue.error(e, st);
|
||
}
|
||
}
|
||
|
||
// Reset state
|
||
void reset() {
|
||
state = const AsyncValue.data({});
|
||
}
|
||
}
|
||
|
||
final employeeloginProvider =
|
||
StateNotifierProvider<
|
||
EmployeeLoginNotifier,
|
||
AsyncValue<Map<String, dynamic>>
|
||
>((ref) => EmployeeLoginNotifier(ref.read(apiRepositoryProvider)));
|
||
|
||
class EmployeeLoginNotifier
|
||
extends StateNotifier<AsyncValue<Map<String, dynamic>>> {
|
||
final ApiRepository _apiRepository;
|
||
|
||
EmployeeLoginNotifier(this._apiRepository) : super(const AsyncValue.data({}));
|
||
|
||
Future<void> login(String mobile) async {
|
||
if (!mounted) return;
|
||
state = const AsyncValue.loading();
|
||
try {
|
||
final result = await _apiRepository.employeeLogin(mobile);
|
||
if (mounted) state = AsyncValue.data(result);
|
||
} catch (e, st) {
|
||
if (mounted) state = AsyncValue.error(e, st);
|
||
}
|
||
}
|
||
|
||
Future<void> verifyOtp(String mobile, String otp) async {
|
||
if (!mounted) return;
|
||
state = const AsyncValue.loading();
|
||
try {
|
||
final model = LoginModel(mobile: mobile, otp: otp);
|
||
final result = await _apiRepository.verifyOtp(model);
|
||
if (mounted) state = AsyncValue.data(result);
|
||
} catch (e, st) {
|
||
if (mounted) state = AsyncValue.error(e, st);
|
||
}
|
||
}
|
||
}
|
||
|
||
final termsProvider = FutureProvider<TermsModel>((ref) async {
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
return await repo.fetchTermsAndConditions();
|
||
});
|
||
final dashboardProvider = FutureProvider<DashBoardModel>((ref) async {
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
return await repo.fetchDashboard();
|
||
});
|
||
|
||
final policyProvider = FutureProvider<TermsModel>((ref) async {
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
return await repo.fetchpolicy();
|
||
});
|
||
final profileProvider = FutureProvider<ProfileGetModel>((ref) async {
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
return await repo.fetchProfile();
|
||
});
|
||
final employeeProfileProvider = FutureProvider<EmployeeProfileModel>((
|
||
ref,
|
||
) async {
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
return await repo.fetchEmployeeProfile();
|
||
});
|
||
|
||
final serviceHistoryNotifierProvider =
|
||
StateNotifierProvider.family<
|
||
ServiceHistoryNotifier,
|
||
AsyncValue<ServiceListHistoryModel>,
|
||
String
|
||
>((ref, type) {
|
||
return ServiceHistoryNotifier(ref: ref, type: type);
|
||
});
|
||
|
||
class ServiceHistoryNotifier
|
||
extends StateNotifier<AsyncValue<ServiceListHistoryModel>> {
|
||
final Ref ref;
|
||
final String type;
|
||
|
||
String? fromDate;
|
||
String? toDate;
|
||
|
||
ServiceHistoryNotifier({required this.ref, required this.type})
|
||
: super(const AsyncValue.loading()) {
|
||
fetchServiceHistory();
|
||
}
|
||
|
||
Future<void> fetchServiceHistory() async {
|
||
try {
|
||
if (!mounted) return;
|
||
state = const AsyncValue.loading();
|
||
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
|
||
final result = await repo.fetchServiceHistory(
|
||
type,
|
||
fromDate: fromDate,
|
||
toDate: toDate,
|
||
);
|
||
|
||
if (!mounted) return;
|
||
state = AsyncValue.data(result);
|
||
} catch (e, st) {
|
||
if (!mounted) return;
|
||
state = AsyncValue.error(e, st);
|
||
}
|
||
}
|
||
|
||
// ✅ APPLY FILTER
|
||
void applyDateFilter({String? from, String? to}) {
|
||
fromDate = from;
|
||
toDate = to;
|
||
fetchServiceHistory();
|
||
}
|
||
|
||
// ✅ CLEAR FILTER
|
||
void clearFilter() {
|
||
fromDate = null;
|
||
toDate = null;
|
||
fetchServiceHistory();
|
||
}
|
||
|
||
// ✅ MANUAL REFRESH
|
||
Future<void> refresh() async {
|
||
await fetchServiceHistory();
|
||
}
|
||
}
|
||
|
||
final countProvider = FutureProvider.family<CountModel, String>((
|
||
ref,
|
||
type,
|
||
) async {
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
return await repo.fetchCount(type);
|
||
});
|
||
final notificationTriggerProvider = StateProvider<int>((ref) => 0);
|
||
|
||
// 🔥 fetch notification count API
|
||
final notificationCountProvider =
|
||
FutureProvider.autoDispose<NotificationCountModel>((ref) async {
|
||
ref.watch(notificationTriggerProvider); // listen for refresh trigger
|
||
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
return await repo.fetchNotificationCount();
|
||
});
|
||
|
||
final serviceDetailProvider = FutureProvider.family<DetailModel, int>((
|
||
ref,
|
||
id,
|
||
) async {
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
return await repo.fetchServiceDetail(id);
|
||
});
|
||
final chatDocumentProvider = FutureProvider.family<ChatProfileModel, int>((
|
||
ref,
|
||
id,
|
||
) async {
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
return await repo.fetchChatDocuments(id);
|
||
});
|
||
|
||
final serviceListProvider = FutureProvider<List<ServiceListModel>>((ref) async {
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
return await repo.fetchServiceList();
|
||
});
|
||
final staffListProvider = FutureProvider<List<StaffModel>>((ref) async {
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
return await repo.fetchStaffList();
|
||
});
|
||
final notificationProvider = StateNotifierProvider.family
|
||
.autoDispose<
|
||
NotificationNotifier,
|
||
AsyncValue<List<NotificationModel>>,
|
||
int
|
||
>((ref, page) {
|
||
return NotificationNotifier(ref: ref, page: page);
|
||
});
|
||
|
||
// ---------------- NOTIFIER --------------------
|
||
|
||
class NotificationNotifier
|
||
extends StateNotifier<AsyncValue<List<NotificationModel>>> {
|
||
final Ref ref;
|
||
final int page;
|
||
|
||
NotificationNotifier({required this.ref, required this.page})
|
||
: super(const AsyncValue.loading()) {
|
||
fetchNotifications();
|
||
}
|
||
|
||
Future<void> fetchNotifications() async {
|
||
if (!mounted) return;
|
||
try {
|
||
state = const AsyncValue.loading();
|
||
|
||
final List<NotificationModel> notifications = await ApiRepository()
|
||
.fetchNotificationList(page: page);
|
||
|
||
if (mounted) state = AsyncValue.data(notifications);
|
||
} catch (e, st) {
|
||
if (mounted) state = AsyncValue.error(e, st);
|
||
}
|
||
}
|
||
}
|
||
|
||
final chatMessagesProvider =
|
||
StateNotifierProvider.family<
|
||
ChatMessagesNotifier,
|
||
AsyncValue<List<MessageModel>>,
|
||
int
|
||
>((ref, chatId) {
|
||
return ChatMessagesNotifier(ref: ref, chatId: chatId);
|
||
});
|
||
|
||
class ChatMessagesNotifier
|
||
extends StateNotifier<AsyncValue<List<MessageModel>>> {
|
||
final Ref ref;
|
||
final int chatId;
|
||
|
||
int _currentPage = 1;
|
||
bool _hasNextPage = true;
|
||
bool _isFetching = false;
|
||
bool _isInitialLoad = true;
|
||
|
||
final List<MessageModel> _messages = [];
|
||
|
||
ChatMessagesNotifier({required this.ref, required this.chatId})
|
||
: super(const AsyncLoading()) {
|
||
loadMessages();
|
||
}
|
||
|
||
/// ⭐ Getter to expose hasNextPage
|
||
bool get hasNextPage => _hasNextPage;
|
||
|
||
/// Load paginated messages (used for scroll pagination)
|
||
Future<void> loadMessages() async {
|
||
if (_isFetching || !_hasNextPage || !mounted) return;
|
||
|
||
_isFetching = true;
|
||
|
||
try {
|
||
print("📥 Loading page $_currentPage for chat $chatId");
|
||
|
||
final result = await ref
|
||
.read(apiRepositoryProvider)
|
||
.fetchChatMessages(chatId: chatId, page: _currentPage);
|
||
|
||
if (result.isEmpty) {
|
||
print("✅ No more messages - pagination complete");
|
||
_hasNextPage = false;
|
||
} else {
|
||
print("✅ Loaded ${result.length} messages from page $_currentPage");
|
||
|
||
if (_isInitialLoad) {
|
||
// ⭐ FIX: On initial load, sort messages in ascending order (oldest first, newest last)
|
||
result.sort((a, b) {
|
||
final dateA =
|
||
DateTime.tryParse(a.createdAt ?? '') ?? DateTime(1970);
|
||
final dateB =
|
||
DateTime.tryParse(b.createdAt ?? '') ?? DateTime(1970);
|
||
return dateA.compareTo(dateB);
|
||
});
|
||
_messages.clear();
|
||
_messages.addAll(result);
|
||
} else {
|
||
// ⭐ FIX: On pagination load, insert older messages at beginning
|
||
// Make sure pagination result is sorted in ascending order
|
||
result.sort((a, b) {
|
||
final dateA =
|
||
DateTime.tryParse(a.createdAt ?? '') ?? DateTime(1970);
|
||
final dateB =
|
||
DateTime.tryParse(b.createdAt ?? '') ?? DateTime(1970);
|
||
return dateA.compareTo(dateB);
|
||
});
|
||
_messages.insertAll(0, result);
|
||
}
|
||
|
||
_currentPage++;
|
||
if (mounted) state = AsyncData(List.from(_messages));
|
||
}
|
||
|
||
if (_isInitialLoad && result.isNotEmpty) {
|
||
_isInitialLoad = false;
|
||
}
|
||
} catch (e, st) {
|
||
print("❌ Error loading messages: $e");
|
||
if (mounted) state = AsyncError(e, st);
|
||
} finally {
|
||
_isFetching = false;
|
||
}
|
||
}
|
||
|
||
/// ⭐ Refresh chat (pull to refresh or after sending message)
|
||
/// This is a SMOOTH refresh that doesn't show loading screen
|
||
Future<void> refresh() async {
|
||
print("🔄 Refreshing chat...");
|
||
|
||
try {
|
||
// ⭐ Get the latest messages from page 1
|
||
final result = await ref
|
||
.read(apiRepositoryProvider)
|
||
.fetchChatMessages(chatId: chatId, page: 1);
|
||
|
||
if (result.isNotEmpty) {
|
||
// Sort in ascending order
|
||
result.sort((a, b) {
|
||
final dateA = DateTime.tryParse(a.createdAt ?? '') ?? DateTime(1970);
|
||
final dateB = DateTime.tryParse(b.createdAt ?? '') ?? DateTime(1970);
|
||
return dateA.compareTo(dateB);
|
||
});
|
||
|
||
// ⭐ Check if we have any new messages
|
||
final lastMessageId = _messages.isNotEmpty ? _messages.last.id : -1;
|
||
final latestMessageId = result.last.id;
|
||
|
||
if (latestMessageId != lastMessageId) {
|
||
// ⭐ New messages found - merge them smoothly
|
||
print("✅ Found new messages, updating list");
|
||
|
||
// Add only new messages that don't exist in current list
|
||
for (var newMsg in result) {
|
||
final exists = _messages.any((m) => m.id == newMsg.id);
|
||
if (!exists) {
|
||
_messages.add(newMsg);
|
||
}
|
||
}
|
||
|
||
// Sort the entire list
|
||
_messages.sort((a, b) {
|
||
final dateA =
|
||
DateTime.tryParse(a.createdAt ?? '') ?? DateTime(1970);
|
||
final dateB =
|
||
DateTime.tryParse(b.createdAt ?? '') ?? DateTime(1970);
|
||
return dateA.compareTo(dateB);
|
||
});
|
||
|
||
if (mounted) state = AsyncData(List.from(_messages));
|
||
} else {
|
||
print("✅ No new messages");
|
||
}
|
||
}
|
||
} catch (e) {
|
||
print("❌ Error refreshing: $e");
|
||
// Don't throw error, just continue with existing messages
|
||
}
|
||
}
|
||
|
||
/// ⭐ Hard refresh (for pull to refresh gesture)
|
||
Future<void> hardRefresh() async {
|
||
print("🔄 Hard refreshing chat...");
|
||
_currentPage = 1;
|
||
_hasNextPage = true;
|
||
_messages.clear();
|
||
_isInitialLoad = true;
|
||
if (mounted) state = const AsyncLoading();
|
||
await loadMessages();
|
||
}
|
||
|
||
/// Add new message to list (socket or send API)
|
||
void addNewMessage(MessageModel msg) {
|
||
print("➕ Adding new message: ${msg.message} (ID: ${msg.id})");
|
||
|
||
// ⭐ DEDUPLICATION: Avoid adding the same message twice
|
||
// 1. Check if ID already exists
|
||
final existingIndex = _messages.indexWhere((m) => m.id == msg.id);
|
||
if (existingIndex != -1) {
|
||
print("♻️ Message ID ${msg.id} already exists, updating instead");
|
||
_messages[existingIndex] = msg;
|
||
if (mounted) state = AsyncData(List.from(_messages));
|
||
return;
|
||
}
|
||
|
||
// 2. Check if this is a server confirmation of an optimistic message
|
||
// Optimistic messages have large positive IDs from DateTime.now().millisecondsSinceEpoch
|
||
// OR content match for very recent messages
|
||
final optimisticIndex = _messages.indexWhere(
|
||
(m) =>
|
||
// Match by content and sender if it's a very recent "local" message
|
||
(m.id > 1000000000000 &&
|
||
m.message == msg.message &&
|
||
m.chatBy == msg.chatBy),
|
||
);
|
||
|
||
if (optimisticIndex != -1) {
|
||
print("🔄 Replacing optimistic message with server version");
|
||
_messages[optimisticIndex] = msg;
|
||
} else {
|
||
// ⭐ Add at the END (newest messages at bottom)
|
||
_messages.add(msg);
|
||
}
|
||
|
||
if (mounted) state = AsyncData(List.from(_messages));
|
||
}
|
||
|
||
/// Update existing message (e.g., delivery status, read status)
|
||
void updateMessage(MessageModel updatedMsg) {
|
||
final index = _messages.indexWhere((m) => m.id == updatedMsg.id);
|
||
if (index != -1) {
|
||
_messages[index] = updatedMsg;
|
||
if (mounted) state = AsyncData(List.from(_messages));
|
||
}
|
||
}
|
||
|
||
/// Delete a message
|
||
void deleteMessage(int messageId) {
|
||
_messages.removeWhere((m) => m.id == messageId);
|
||
if (mounted) state = AsyncData(List.from(_messages));
|
||
}
|
||
}
|
||
|
||
// ✅ Provider for fetching both country and state data
|
||
final countryAndStatesProvider = FutureProvider<CountryModel>((ref) async {
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
try {
|
||
final model = await repo.fetchCountryAndStates(ConstsApi.conturystate);
|
||
ref.keepAlive(); // Keep cached until manually disposed
|
||
return model;
|
||
} catch (e) {
|
||
print('❌ Error in countryAndStatesProvider: $e');
|
||
throw e;
|
||
}
|
||
});
|
||
final fetchCityProvider = FutureProvider.family<CityModel, int>((
|
||
ref,
|
||
stateId,
|
||
) async {
|
||
final repo = ref.read(apiRepositoryProvider);
|
||
try {
|
||
final cityModel = await repo.fetchCityByState(ConstsApi.city, stateId);
|
||
ref.keepAlive();
|
||
return cityModel;
|
||
} catch (e) {
|
||
print('❌ Error in fetchCityProvider: $e');
|
||
throw e;
|
||
}
|
||
});
|