2026-04-11
This commit is contained in:
commit
27e27fb70b
270 files changed
+30296
No files matched your search
@@ -0,0 +1,30 @@
|
||||
class ConstsApi {
|
||||
static const String baseUrl = "https://www.taxglide.amrithaa.net";
|
||||
|
||||
static const String login = "$baseUrl/api/otp/sms/request";
|
||||
static const String verifyOtp = "$baseUrl/api/otp/sms/verify";
|
||||
static const String signup = "$baseUrl/api/register";
|
||||
static const String terms = "$baseUrl/api/get_terms_conditions";
|
||||
static const String policy = "$baseUrl/api/get_privacy_policy";
|
||||
static const String getprofile = "$baseUrl/api/get_profile";
|
||||
static const String employeelogin = "$baseUrl/api/otp/employee/sms/request";
|
||||
static const String serivcelist = "$baseUrl/api/get_services_list";
|
||||
static const String conturystate = "$baseUrl/api/kyc_master";
|
||||
static const String city = "$baseUrl/api/state_based_districts";
|
||||
static const String kycupdate = "$baseUrl/api/profile_update";
|
||||
static const String serivcerequest = "$baseUrl/api/service_register";
|
||||
static const String serivcehistory = "$baseUrl/api/get_service_requests_list";
|
||||
static const String serivcedetails =
|
||||
"$baseUrl/api/get_service_request_detail";
|
||||
static const String logout = "$baseUrl/api/logout";
|
||||
static const String chatList = "$baseUrl/api/chat";
|
||||
static const String sendChatMessage = "$baseUrl/api/chat/message/send";
|
||||
static const String stafflist = "$baseUrl/api/get_employee_list";
|
||||
static const String count = "$baseUrl/api/chat/un_read_message_count";
|
||||
static const String chatdocument = "$baseUrl/api/chat/document_lists";
|
||||
static const String notificationList = "$baseUrl/api/notification/lists";
|
||||
static const String notificationcount =
|
||||
"$baseUrl/api/notification/un_read_count";
|
||||
static const String dashboard = "$baseUrl/api/get_dashboard_data";
|
||||
static const String proformaaccept = "$baseUrl/api/proforma/accept";
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user