taxgilde/lib/controller/api_repository.dart
2026-04-15 12:32:30 +05:30

1145 lines
37 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:taxglide/consts/local_store.dart';
import 'package:taxglide/controller/api_consts.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';
import 'package:taxglide/router/consts_routers.dart';
class ApiRepository {
final LocalStore _localStore = LocalStore();
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
// 🔹 Common headers without token (for login/signup)
Map<String, String> get _baseHeaders => {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
// 🔹 Authorized headers with token
Future<Map<String, String>> _authorizedHeaders() async {
final token = await _localStore.getToken();
return {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
/// 🔑 Get fresh FCM token on-demand (not from storage)
Future<String?> _getFreshFcmToken() async {
try {
debugPrint('🔄 Generating fresh FCM token...');
final token = await _messaging.getToken();
debugPrint('✅ FCM Token generated: $token');
return token;
} catch (e) {
debugPrint('❌ Error getting FCM token: $e');
return null;
}
}
Future<Map<String, String?>> _regenerateFcmToken() async {
try {
// 📌 Print old token BEFORE deleting
final oldToken = await _messaging.getToken();
debugPrint('📋 OLD FCM Token (before delete): $oldToken');
debugPrint('🔄 Deleting old FCM token...');
// Delete the current token
await _messaging.deleteToken();
debugPrint('🗑️ Old FCM token deleted');
// Wait for Firebase to process the deletion
await Future.delayed(const Duration(milliseconds: 800));
// Request permission again (important for iOS)
await _messaging.requestPermission();
// Generate new tokens
debugPrint('🔄 Requesting new tokens...');
final newToken = await _messaging.getToken();
final apnsToken = await _messaging.getAPNSToken();
debugPrint('✅ NEW FCM Token: $newToken');
debugPrint('✅ NEW APNS Token: $apnsToken');
// Compare tokens
if (oldToken == newToken) {
debugPrint('⚠️ WARNING: Old and New tokens are SAME!');
} else {
debugPrint('✅ SUCCESS: New token is DIFFERENT from old token');
}
return {'fcm_token': newToken, 'apns_token': apnsToken};
} catch (e) {
debugPrint('❌ Error regenerating tokens: $e');
return {'fcm_token': null, 'apns_token': null};
}
}
// 🔐 Centralized token expiry handler
Future<void> _handleUnauthorized() async {
debugPrint('🔐 Token expired - Auto logout');
await _localStore.clearAll();
Get.offAllNamed(ConstRouters.login);
Get.snackbar(
'Session Expired',
'Please login again',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
// 🔹 Wrapper to handle token expiry for all authenticated requests
Future<http.Response> _makeAuthenticatedRequest(
Future<http.Response> Function() request,
) async {
try {
final response = await request();
if (response.statusCode == 401 || response.statusCode == 403) {
await _handleUnauthorized();
throw Exception('Unauthorized - Token expired');
}
return response;
} catch (e) {
rethrow;
}
}
// 🔹 LOGIN API - sends OTP
Future<Map<String, dynamic>> loginUser(LoginModel model) async {
try {
final params = model.toJsonMobile();
final uri = Uri.parse(ConstsApi.login).replace(
queryParameters: params.map(
(key, value) => MapEntry(key, value.toString()),
),
);
final response = await http.post(uri, headers: _baseHeaders);
final data = jsonDecode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
return {'success': true, 'data': data};
} else {
return {'success': false, 'error': _extractErrorMessage(data)};
}
} catch (e) {
return {
'success': false,
'error': 'Connection error. Please check your internet.',
};
}
}
// 🔹 VERIFY OTP (LOGIN) - ✅ Regenerates FCM token
Future<Map<String, dynamic>> verifyOtp(LoginModel model) async {
try {
final params = model.toJsonOtp();
// ✅ Regenerate FCM and APNS tokens on every login
final tokens = await _regenerateFcmToken();
final fcmToken = tokens['fcm_token'];
final apnsToken = tokens['apns_token'];
if (fcmToken != null && fcmToken.isNotEmpty) {
params['fcm_token'] = fcmToken;
}
if (apnsToken != null && apnsToken.isNotEmpty) {
params['apns_token'] = apnsToken;
}
if (params['fcm_token'] == null) {
debugPrint('⚠️ Failed to generate FCM token, trying fallback...');
final fallbackToken = await _getFreshFcmToken();
if (fallbackToken != null && fallbackToken.isNotEmpty) {
params['fcm_token'] = fallbackToken;
}
}
final uri = Uri.parse(ConstsApi.verifyOtp).replace(
queryParameters: params.map(
(key, value) => MapEntry(key, value.toString()),
),
);
final response = await http.post(uri, headers: _baseHeaders);
final data = jsonDecode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
await _localStore.saveLoginData(data);
return {'success': true, 'data': data};
} else {
return {'success': false, 'error': _extractErrorMessage(data)};
}
} catch (e) {
return {
'success': false,
'error': 'Connection error. Please check your internet.',
};
}
}
// 🔹 EMPLOYEE LOGIN
Future<Map<String, dynamic>> employeeLogin(String mobile) async {
try {
final uri = Uri.parse(
ConstsApi.employeelogin,
).replace(queryParameters: {'mobile': mobile});
final response = await http.post(uri, headers: _baseHeaders);
final data = jsonDecode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
return {'success': true, 'data': data};
} else {
return {'success': false, 'error': _extractErrorMessage(data)};
}
} catch (e) {
return {
'success': false,
'error': 'Connection error. Please check your internet.',
};
}
}
// 🔹 SIGNUP API
Future<Map<String, dynamic>> signupUser(SignupModel model) async {
try {
final params = model.toJson();
final uri = Uri.parse(ConstsApi.signup).replace(
queryParameters: params.map(
(key, value) => MapEntry(key, value.toString()),
),
);
final response = await http.post(uri, headers: _baseHeaders);
final data = jsonDecode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
return {'success': true, 'data': data};
} else {
return {'success': false, 'error': _extractErrorMessage(data)};
}
} catch (e) {
return {
'success': false,
'error': 'Connection error. Please check your internet.',
};
}
}
// 🔹 VERIFY OTP (SIGNUP) - ✅ Regenerates FCM token
Future<Map<String, dynamic>> verifySignupOtp(
String mobile,
String otp,
) async {
try {
// ✅ Regenerate FCM and APNS tokens on signup
final tokens = await _regenerateFcmToken();
final fcmToken = tokens['fcm_token'];
final apnsToken = tokens['apns_token'];
final params = {'mobile': mobile, 'otp': otp};
if (fcmToken != null && fcmToken.isNotEmpty) {
params['fcm_token'] = fcmToken;
}
if (apnsToken != null && apnsToken.isNotEmpty) {
params['apns_token'] = apnsToken;
}
if (params['fcm_token'] == null) {
debugPrint('⚠️ Failed to generate FCM token, trying fallback...');
final fallbackToken = await _getFreshFcmToken();
if (fallbackToken != null && fallbackToken.isNotEmpty) {
params['fcm_token'] = fallbackToken;
}
}
final uri = Uri.parse(
ConstsApi.verifyOtp,
).replace(queryParameters: params);
final response = await http.post(uri, headers: _baseHeaders);
final data = jsonDecode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
await _localStore.saveLoginData(data);
return {'success': true, 'data': data};
} else {
return {'success': false, 'error': _extractErrorMessage(data)};
}
} catch (e) {
return {
'success': false,
'error': 'Connection error. Please check your internet.',
};
}
}
// 🔹 FETCH TERMS & CONDITIONS
Future<TermsModel> fetchTermsAndConditions() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.terms),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return TermsModel.fromJson(data);
} else {
throw Exception('Failed to fetch terms and conditions');
}
}
// 🔹 FETCH DASHBOARD
Future<DashBoardModel> fetchDashboard() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.dashboard),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return DashBoardModel.fromJson(data);
} else {
throw Exception('Failed to fetch dashboard');
}
}
// 🔹 FETCH POLICY
Future<TermsModel> fetchpolicy() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.policy),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return TermsModel.fromJson(data);
} else {
throw Exception('Failed to fetch Policy');
}
}
// 🔹 FETCH PROFILE
Future<ProfileGetModel> fetchProfile() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.getprofile),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return ProfileGetModel.fromJson(data);
} else {
throw Exception('Failed to fetch Profile');
}
}
// 🔹 FETCH EMPLOYEE PROFILE
Future<EmployeeProfileModel> fetchEmployeeProfile() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.getprofile),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return EmployeeProfileModel.fromJson(data);
} else {
throw Exception('Failed to fetch Employee Profile');
}
}
// 🔹 FETCH SERVICE HISTORY
Future<ServiceListHistoryModel> fetchServiceHistory(
String type, {
String? fromDate,
String? toDate,
}) async {
final uri = Uri.parse(ConstsApi.serivcehistory).replace(
queryParameters: {
'type': type,
if (fromDate != null) 'from_date': fromDate,
if (toDate != null) 'to_date': toDate,
},
);
final response = await _makeAuthenticatedRequest(() async {
return await http.get(uri, headers: await _authorizedHeaders());
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return ServiceListHistoryModel.fromJson(data);
} else {
throw Exception('Failed to fetch Service History');
}
}
// 🔹 FETCH COUNT
Future<CountModel> fetchCount(String type) async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse("${ConstsApi.count}?chat_id=$type"),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return CountModel.fromJson(data);
} else {
throw Exception('Failed to fetch count');
}
}
// 🔹 FETCH NOTIFICATION COUNT
Future<NotificationCountModel> fetchNotificationCount() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.notificationcount),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return NotificationCountModel.fromJson(data);
} else {
throw Exception('Failed to fetch notification count');
}
}
// 🔹 FETCH SERVICE DETAIL
Future<DetailModel> fetchServiceDetail(int id) async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse("${ConstsApi.serivcedetails}?service_request_id=$id"),
headers: await _authorizedHeaders(),
);
});
debugPrint("response: ${response.body}");
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return DetailModel.fromJson(data);
} else {
throw Exception('Failed to fetch Service Detail');
}
}
// 🔹 FETCH CHAT DOCUMENTS
Future<ChatProfileModel> fetchChatDocuments(int id) async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse("${ConstsApi.chatdocument}?chat_id=$id"),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return ChatProfileModel.fromJson(data);
} else {
throw Exception('Failed to fetch Chat Documents');
}
}
// 🔹 FETCH SERVICE LIST
Future<List<ServiceListModel>> fetchServiceList() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.serivcelist),
headers: await _authorizedHeaders(),
);
});
debugPrint("📡 Status Code: ${response.statusCode}");
debugPrint("📡 Response: ${response.body}");
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final List<dynamic> list = data['data'] ?? [];
return list.map((e) => ServiceListModel.fromJson(e)).toList();
}
throw Exception('Failed to fetch service list');
}
// 🔹 FETCH STAFF LIST
Future<List<StaffModel>> fetchStaffList() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.stafflist),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final List<dynamic> list = data['data'] ?? [];
return list.map((e) => StaffModel.fromJson(e)).toList();
} else {
throw Exception('Failed to fetch staff list');
}
}
// 🔹 FETCH NOTIFICATION LIST
Future<List<NotificationModel>> fetchNotificationList({
required int page,
}) async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse("${ConstsApi.notificationList}?page=$page"),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final List<dynamic> list = data['data'] ?? [];
return list.map((e) => NotificationModel.fromJson(e)).toList();
} else {
throw Exception('Failed to fetch notifications');
}
}
// 🔹 FETCH CHAT MESSAGES
Future<List<MessageModel>> fetchChatMessages({
required int chatId,
required int page,
}) async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse("${ConstsApi.chatList}/$chatId/messages?page=$page"),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final messages = data["messages"]["data"] ?? [];
return List<MessageModel>.from(
messages.map((e) => MessageModel.fromJson(e)),
);
} else {
throw Exception("Failed to fetch chat messages → ${response.statusCode}");
}
}
// 🔹 FETCH COUNTRY AND STATES
Future<CountryModel> fetchCountryAndStates(String url) async {
try {
debugPrint('🌐 Fetching Country & States from: $url');
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(url),
headers: await _authorizedHeaders(),
);
});
debugPrint('📡 API Status: ${response.statusCode}');
debugPrint('📡 API Body: ${response.body}');
if (response.statusCode == 200) {
final jsonBody = jsonDecode(response.body);
final model = CountryModel.fromJson(jsonBody);
debugPrint(
'✅ Countries: ${model.countriesData?.length ?? 0}, States: ${model.statesData?.length ?? 0}',
);
return model;
} else {
final error = jsonDecode(response.body);
throw Exception(error['message'] ?? 'Failed to fetch data');
}
} catch (e) {
debugPrint('❌ fetchCountryAndStates Error: $e');
throw Exception('Error fetching country & state data: $e');
}
}
// 🔹 FETCH CITY BY STATE
Future<CityModel> fetchCityByState(String url, int stateId) async {
try {
final fullUrl = "$url?state_id=$stateId";
debugPrint('🌐 Fetching Cities (Districts) from: $fullUrl');
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(fullUrl),
headers: await _authorizedHeaders(),
);
});
debugPrint('📡 City API Status: ${response.statusCode}');
debugPrint('📡 City API Body: ${response.body}');
if (response.statusCode == 200) {
final jsonBody = jsonDecode(response.body);
final model = CityModel.fromJson(jsonBody);
debugPrint('✅ Districts fetched: ${model.districtsData?.length ?? 0}');
return model;
} else {
final error = jsonDecode(response.body);
throw Exception(error['message'] ?? 'Failed to fetch city data');
}
} catch (e) {
debugPrint('❌ fetchCityByState Error: $e');
throw Exception('Error fetching city data: $e');
}
}
// 🔹 KYC FORM UPDATE
Future<void> kycFormUpdate({
File? logo,
File? panFile,
File? gstFile,
File? incorporationFile,
int? countryId,
int? stateId,
int? cityId,
String? companyPincode,
String? companyAddress,
String? panNumber,
String? gstNumber,
String? tanNumber,
String? cinNumber,
String? yearOfIncorporation,
String? address,
}) async {
try {
final token = await _localStore.getToken();
final uri = Uri.parse(ConstsApi.kycupdate);
var request = http.MultipartRequest('POST', uri)
..headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Connection': 'keep-alive',
})
..fields.addAll({
if (countryId != null) 'country_id': countryId.toString(),
if (stateId != null) 'state_id': stateId.toString(),
if (cityId != null) 'district_id': cityId.toString(),
if (companyPincode != null) 'company_pincode': companyPincode,
if (companyAddress != null) 'company_address': companyAddress,
if (panNumber != null) 'pan_number': panNumber,
if (gstNumber != null) 'gst_number': gstNumber,
if (tanNumber != null) 'tan_number': tanNumber,
if (cinNumber != null) 'cin': cinNumber,
if (yearOfIncorporation != null) 'year_of_incorporation': yearOfIncorporation,
if (address != null) 'address': address,
});
await Future.wait([
_addFileIfExists(request, logo, 'logo'),
_addFileIfExists(request, panFile, 'pan_file'),
_addFileIfExists(request, gstFile, 'gst_file'),
_addFileIfExists(request, incorporationFile, 'incorporation_file'),
]);
final streamedResponse = await request.send().timeout(
const Duration(seconds: 60),
onTimeout: () {
throw TimeoutException('Request timed out after 60 seconds');
},
);
final response = await http.Response.fromStream(streamedResponse);
debugPrint('📦 KYC Response Status: ${response.statusCode}');
debugPrint('📦 KYC Response Body: ${response.body}');
// Handle token expiry
if (response.statusCode == 401 || response.statusCode == 403) {
await _handleUnauthorized();
throw Exception('Unauthorized - Token expired');
}
if (response.statusCode == 200) {
debugPrint('✅ KYC update success');
return;
}
final decoded = _safeJsonDecode(response.body);
if (response.statusCode == 422) {
throw _formatValidationErrors(decoded);
} else {
final message =
decoded?['message'] ?? decoded?['error'] ?? 'Something went wrong';
throw {'error': message.toString()};
}
} on SocketException {
throw {'error': 'No internet connection. Please check your network.'};
} on TimeoutException {
throw {'error': 'Request timed out. Please try again.'};
} on FormatException {
throw {'error': 'Invalid server response. Please try again.'};
} catch (e) {
if (e is Map<String, dynamic>) rethrow;
debugPrint('❌ KYC update error: $e');
throw {'error': 'Failed to update KYC: ${e.toString()}'};
}
}
// 🔹 SEND CHAT MESSAGE
Future<void> sendChatMessage({
required int chatId,
required String messages,
int? tagId,
List<File>? files,
}) async {
try {
final token = await _localStore.getToken();
final uri = Uri.parse(ConstsApi.sendChatMessage);
var request = http.MultipartRequest('POST', uri)
..headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
})
..fields.addAll({'chat_id': chatId.toString(), 'message': messages});
if (tagId != null && tagId > 0) {
request.fields['tag_id'] = tagId.toString();
}
if (files != null && files.isNotEmpty) {
for (int i = 0; i < files.length; i++) {
request.files.add(
await http.MultipartFile.fromPath(
'file[$i]',
files[i].path,
filename: files[i].path.split('/').last,
),
);
}
}
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
// Handle token expiry
if (response.statusCode == 401 || response.statusCode == 403) {
await _handleUnauthorized();
throw Exception('Unauthorized - Token expired');
}
final decoded = _safeJsonDecode(response.body);
if (response.statusCode == 200) {
debugPrint("✅ Chat message sent successfully");
return;
}
if (response.statusCode == 422) {
throw _formatValidationErrors(decoded);
} else {
final message =
decoded?['message'] ?? decoded?['error'] ?? 'Something went wrong';
throw {'error': message.toString()};
}
} catch (e) {
debugPrint('❌ Chat send error: $e');
if (e is Map<String, dynamic>) rethrow;
throw {'error': 'Failed to send message: ${e.toString()}'};
}
}
// 🔹 UPDATE SERVICE REQUEST
Future<void> updateServiceRequest({
required int serviceId,
required String message,
List<File>? documents,
}) async {
try {
final token = await _localStore.getToken();
final uri = Uri.parse(ConstsApi.serivcerequest);
var request = http.MultipartRequest('POST', uri)
..headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Connection': 'keep-alive',
})
..fields.addAll({
'service_id': serviceId.toString(),
'message': message,
});
if (documents != null && documents.isNotEmpty) {
for (int i = 0; i < documents.length; i++) {
final file = documents[i];
final fileName = file.path.split('/').last;
request.files.add(
await http.MultipartFile.fromPath(
'documents[$i]',
file.path,
filename: fileName,
),
);
}
}
final streamedResponse = await request.send().timeout(
const Duration(seconds: 60),
onTimeout: () {
throw TimeoutException('Request timed out after 60 seconds');
},
);
final response = await http.Response.fromStream(streamedResponse);
// Handle token expiry
if (response.statusCode == 401 || response.statusCode == 403) {
await _handleUnauthorized();
throw Exception('Unauthorized - Token expired');
}
if (response.statusCode == 200) {
debugPrint('✅ Service request updated successfully');
return;
}
final decoded = _safeJsonDecode(response.body);
if (response.statusCode == 422) {
throw _formatValidationErrors(decoded);
} else {
final message =
decoded?['message'] ?? decoded?['error'] ?? 'Something went wrong';
throw {'error': message.toString()};
}
} on SocketException {
throw {'error': 'No internet connection. Please check your network.'};
} on TimeoutException {
throw {'error': 'Request timed out. Please try again.'};
} on FormatException {
throw {'error': 'Invalid server response. Please try again.'};
} catch (e) {
if (e is Map<String, dynamic>) rethrow;
debugPrint('❌ Service request update error: $e');
throw {'error': 'Failed to update service request: ${e.toString()}'};
}
}
Future<void> proformaaccept({required int proformaId}) async {
try {
final token = await _localStore.getToken();
final uri = Uri.parse(ConstsApi.proformaaccept);
var request = http.MultipartRequest('POST', uri)
..headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Connection': 'keep-alive',
})
..fields.addAll({
'id': proformaId.toString(), // ✅ FIXED
});
final streamedResponse = await request.send().timeout(
const Duration(seconds: 60),
onTimeout: () {
throw TimeoutException('Request timed out after 60 seconds');
},
);
final response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 401 || response.statusCode == 403) {
await _handleUnauthorized();
throw Exception('Unauthorized - Token expired');
}
if (response.statusCode == 200) {
debugPrint('✅ Proforma accepted successfully');
return;
}
final decoded = _safeJsonDecode(response.body);
if (response.statusCode == 422) {
throw _formatValidationErrors(decoded);
} else {
final message =
decoded?['message'] ?? decoded?['error'] ?? 'Something went wrong';
throw {'error': message.toString()};
}
} on SocketException {
throw {'error': 'No internet connection. Please check your network.'};
} on TimeoutException {
throw {'error': 'Request timed out. Please try again.'};
} on FormatException {
throw {'error': 'Invalid server response. Please try again.'};
} catch (e) {
if (e is Map<String, dynamic>) rethrow;
debugPrint('❌ Proforma accept error: $e');
throw {'error': 'Failed to accept proforma: ${e.toString()}'};
}
}
// 🔹 LOGOUT - ✅ Deletes FCM token
Future<void> logout() async {
try {
final token = await _localStore.getToken();
if (token == null || token.isEmpty) {
debugPrint('⚠️ No token found — clearing data and deleting FCM token');
await _messaging.deleteToken();
await _localStore.clearAll(keepFcm: false);
return;
}
final uri = Uri.parse(ConstsApi.logout);
debugPrint('📡 Logging out from: $uri');
final request = http.Request('POST', uri)
..headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Content-Type': 'application/json',
'Connection': 'keep-alive',
});
final streamedResponse = await request.send().timeout(
const Duration(seconds: 30),
onTimeout: () =>
throw TimeoutException('Logout request timed out after 30 seconds'),
);
final response = await http.Response.fromStream(streamedResponse);
debugPrint('📡 Logout Status Code: ${response.statusCode}');
debugPrint('📦 Logout Body: ${response.body}');
final decoded = _safeJsonDecode(response.body);
if (response.statusCode == 200) {
final message = decoded?['message'] ?? 'Logout successful';
debugPrint('$message');
} else if (response.statusCode == 401) {
debugPrint('⚠️ Token expired or unauthorized. Clearing storage.');
} else {
final message = decoded?['message'] ?? 'Logout failed';
debugPrint('⚠️ $message');
}
} on SocketException {
debugPrint('❌ No internet connection');
} on TimeoutException {
debugPrint('❌ Request timed out');
} catch (e, stack) {
debugPrint('❌ Logout error: $e');
debugPrint('🧩 Stack trace: $stack');
} finally {
// ✅ ALWAYS delete FCM token and clear data on logout
try {
debugPrint('🗑️ Deleting FCM token on logout...');
await _messaging.deleteToken();
debugPrint('✅ FCM token deleted successfully');
} catch (e) {
debugPrint('⚠️ Error deleting FCM token: $e');
}
await _localStore.clearAll(keepFcm: false);
debugPrint('✅ Local storage cleared');
}
}
// Helper methods remain the same...
Map<String, dynamic>? _safeJsonDecode(String body) {
try {
return jsonDecode(body) as Map<String, dynamic>?;
} catch (e) {
debugPrint('⚠️ JSON decode error: $e');
return null;
}
}
String _extractErrorMessage(
dynamic data, [
String defaultMsg = 'Operation failed',
]) {
if (data is! Map) return defaultMsg;
if (data['status'] == 'error') {
if (data['errors'] is Map) {
final errors = data['errors'] as Map;
final firstErrorKey = errors.keys.first;
final errorList = errors[firstErrorKey];
if (errorList is List && errorList.isNotEmpty) {
return errorList.first.toString();
} else if (errorList is String) {
return errorList;
}
} else if (data['errors'] is List &&
(data['errors'] as List).isNotEmpty) {
final firstError = (data['errors'] as List).first;
if (firstError is String) {
return firstError;
} else if (firstError is Map && firstError['message'] != null) {
return firstError['message'];
}
}
}
return data['message'] ??
data['error'] ??
data['msg'] ??
data['detail'] ??
defaultMsg;
}
// Additional helper methods (fetchProfile, fetchServiceHistory, etc.) remain unchanged
// ... (include all other methods from your original code)
}
// 🔹 HELPER: Add file if exists
Future<void> _addFileIfExists(
http.MultipartRequest request,
File? file,
String fieldName,
) async {
if (file == null) return;
try {
if (!file.existsSync()) {
debugPrint('⚠️ File not found: ${file.path}');
return;
}
request.files.add(
await http.MultipartFile.fromPath(
fieldName,
file.path,
filename: basename(file.path),
),
);
} catch (e) {
debugPrint('⚠️ Error adding file $fieldName: $e');
}
}
// // 🔹 HELPER: Safe JSON decode
// Map<String, dynamic>? _safeJsonDecode(String body) {
// try {
// return jsonDecode(body) as Map<String, dynamic>?;
// } catch (e) {
// debugPrint('⚠️ JSON decode error: $e');
// return null;
// }
// }
// // 🔹 HELPER: Extract error message from API response
// String _extractErrorMessage(
// dynamic data, [
// String defaultMsg = 'Operation failed',
// ]) {
// if (data is! Map) return defaultMsg;
// // Check if status is error
// if (data['status'] == 'error') {
// // Handle errors object with field-specific errors
// if (data['errors'] is Map) {
// final errors = data['errors'] as Map;
// final firstErrorKey = errors.keys.first;
// final errorList = errors[firstErrorKey];
// if (errorList is List && errorList.isNotEmpty) {
// return errorList.first.toString();
// } else if (errorList is String) {
// return errorList;
// }
// }
// // Handle errors as List
// else if (data['errors'] is List && (data['errors'] as List).isNotEmpty) {
// final firstError = (data['errors'] as List).first;
// if (firstError is String) {
// return firstError;
// } else if (firstError is Map && firstError['message'] != null) {
// return firstError['message'];
// }
// }
// }
// // Fallback to other common error formats
// return data['message'] ??
// data['error'] ??
// data['msg'] ??
// data['detail'] ??
// defaultMsg;
// }
// 🔹 HELPER: Format validation errors
Map<String, String> _formatValidationErrors(Map<String, dynamic>? decoded) {
if (decoded == null) return {'error': 'Validation failed'};
final rawErrors =
(decoded['errors'] ?? decoded['error'] ?? {}) as Map<String, dynamic>;
final Map<String, String> formattedErrors = {};
rawErrors.forEach((key, value) {
if (value is List && value.isNotEmpty) {
formattedErrors[key] = value.first.toString();
} else if (value is String) {
formattedErrors[key] = value;
} else {
formattedErrors[key] = 'Invalid input';
}
});
return formattedErrors.isNotEmpty
? formattedErrors
: {'error': 'Validation failed'};
}
// ⚡ Optimized HTTP Client for better performance
class OptimizedHttpClient {
static final http.Client _client = http.Client();
static http.Client get client => _client;
static void dispose() {
_client.close();
}
}