import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:get/get.dart'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:get_storage/get_storage.dart'; import 'package:inventory_app/constants.dart'; import 'package:inventory_app/models/famille_produit.dart'; import 'package:inventory_app/models/depot.dart'; import 'package:inventory_app/models/inventory_line.dart'; import 'package:inventory_app/models/product.dart'; import 'package:inventory_app/models/tracking_number.dart'; import 'package:inventory_app/utils.dart'; import 'package:path_provider/path_provider.dart'; class AxelorClient { late Dio dio; late PersistCookieJar cookieJar; static final GetStorage _storage = GetStorage(); AxelorClient._internal(); static Future create() async { final client = AxelorClient._internal(); Directory appDocDir = await getApplicationDocumentsDirectory(); String cookiePath = '${appDocDir.path}/cookies'; client.cookieJar = PersistCookieJar(storage: FileStorage(cookiePath)); client.dio = Dio(BaseOptions(baseUrl: Utils.url)); client.dio.interceptors.add(CookieManager(client.cookieJar)); // Set saved session ID if available final storedSessionId = _storage.read('sessionId'); if (storedSessionId != null) { client.dio.options.headers['Cookie'] = 'JSESSIONID=$storedSessionId'; } return client; } Future login(String username, String password) async { try { final response = await dio.post( '${Utils.url}/login.jsp', data: {'username': username, 'password': password}, options: Options( contentType: Headers.formUrlEncodedContentType, followRedirects: false, // Make sure redirect is not automatically followed validateStatus: (status) => status != null && status < 500, headers: { 'User-Agent': 'Mozilla/5.0 (Android; Flutter App)', 'Accept': '*/*', }, ), ); // Handle 302 redirect: cookies should be in headers final cookies = response.headers['set-cookie']; if (cookies != null) { // Parse JSESSIONID String? sessionId; for (var cookie in cookies) { if (cookie.contains('JSESSIONID')) { sessionId = cookie.split(';').first.split('=').last; break; } } if (sessionId != null && sessionId.isNotEmpty) { // Set the cookie so the validation request is authenticated dio.options.headers['Cookie'] = 'JSESSIONID=$sessionId'; // Verify the session is actually authenticated (not just an anonymous session) final valid = await isSessionValid(); if (!valid) { dio.options.headers.remove('Cookie'); return false; } await _storage.write('sessionId', sessionId); await _storage.write('username', username); return true; } } return false; } catch (e) {} return false; } Future logout() async { try { final response = await dio.get('/logout'); if (response.statusCode == 200) { await _storage.remove('sessionId'); await _storage.remove('username'); await cookieJar.deleteAll(); return true; } } catch (e) {} return false; } Future?> fetchLocations() async { try { final response = await dio.post( '/ws/rest/com.axelor.apps.stock.db.StockLocation/search', data: { "offset": 0, "limit": 800, "data": { "criteria": [ { "fieldName": "usableOnImmobilisation", "operator": "=", "value": true, }, ], }, }, ); if (response.statusCode == 200 && response.data['data'] != null) { List depots = []; for (var depot in response.data["data"]) { depots.add(Depot.fromJson(depot)); } return depots; } else { return []; } } catch (e) {} return null; } Future saveInventoryLine({required InventoryLine inventoryLine}) async { final String url = '${Utils.url}/ws/rest/com.axelor.apps.stock.db.InventoryLine'; String domain; if (inventoryLine.trackingNumberId == null) { domain = "self.inventory.id = :inventoryId AND self.product.id = :product AND self.trackingNumber IS NULL"; } else { domain = "self.inventory.id = :inventoryId AND self.product.id = :product AND self.trackingNumber.id = :trackingNumberId"; } try { // 1. Check if InventoryLine already exists final searchResponse = await dio.post( '$url/search', data: { "data": { "_domain": domain, "_domainContext": { "inventoryId": inventoryLine.inventoryId, "product": inventoryLine.productId, "trackingNumberId": inventoryLine.trackingNumberId, }, "_archived": false, }, }, ); if (searchResponse.statusCode == 200 && searchResponse.data['status'] == 0) { final existing = searchResponse.data['data']; if (existing != null && existing.isNotEmpty) { final existingLine = existing[0]; inventoryLine.id = existingLine['id']; inventoryLine.version = existingLine['version']; inventoryLine.firstCounting = inventoryLine.firstCounting != 0 ? inventoryLine.firstCounting : double.tryParse( existingLine['firstCounting']?.toString() ?? '', ) ?? 0.0; inventoryLine.secondCounting = inventoryLine.secondCounting != 0 ? inventoryLine.secondCounting : double.tryParse( existingLine['secondCounting']?.toString() ?? '', ) ?? 0.0; inventoryLine.thirdCounting = inventoryLine.thirdCounting != 0 ? inventoryLine.thirdCounting : double.tryParse( existingLine['thirdCounting']?.toString() ?? '', ) ?? 0.0; inventoryLine.firstCountingDate = inventoryLine.firstCountingDate ?? existingLine['firstCountingDate']; inventoryLine.secondCountingDate = inventoryLine.secondCountingDate ?? existingLine['secondCountingDate']; inventoryLine.thirdCountingDate = inventoryLine.thirdCountingDate ?? existingLine['thirdCountingDate']; inventoryLine.firstCountingByUser = inventoryLine.firstCountingByUser ?? existingLine['firstCountingByUser']; inventoryLine.secondCountingByUser = inventoryLine.secondCountingByUser ?? existingLine['secondCountingByUser']; inventoryLine.thirdCountingByUser = inventoryLine.thirdCountingByUser ?? existingLine['thirdCountingByUser']; } else {} } // 2. Create or update the InventoryLine final saveResponse = await dio.post( url, data: jsonEncode({"data": inventoryLine.toJson()}), ); if (saveResponse.statusCode == 200) { final result = saveResponse.data; if (result['status'] == 0) { } else {} } else {} } on DioException catch (e) { if (e.response != null) {} } catch (e) {} } Future fetchProductByCode(String code) async { try { final response = await dio.post( '${Utils.url}/ws/rest/com.axelor.apps.base.db.Product/search', data: { "offset": 0, "limit": 1, "sortBy": ["code", "name", "unit"], "data": { "criteria": [ {"fieldName": "code", "operator": "=", "value": code}, ], }, }, ); if (response.statusCode == 200 && response.data['status'] == 0) { final list = response.data["data"]; if (list != null && list.isNotEmpty) { return Product.fromJson(list[0]); } } } catch (e) {} return null; } Future?> fetchTrackingNumberByProduct(int id) async { try { final response = await dio.post( '${Utils.url}/ws/rest/com.axelor.apps.stock.db.TrackingNumber/search', data: { "offset": 0, "limit": 100, "fields": ["trackingNumberSeq", "perishableExpirationDate"], "data": { "criteria": [ {"fieldName": "product.id", "operator": "=", "value": id}, ], }, }, ); if (response.statusCode == 200 && response.data['data'] != null) { List trackingNumbers = []; for (var trackingNumber in response.data["data"]) { trackingNumbers.add(TrackingNumber.fromJson(trackingNumber)); } return trackingNumbers; } else { return []; } } catch (e) {} return null; } Future?> fetchInventoryLinesByLocation( int locationId, ) async { try { final response = await dio.post( '${Utils.url}/ws/rest/com.axelor.apps.stock.db.InventoryLine/search', data: { "data": { "_domain": "self.stockLocation.id = :locationId and self.inventory.id = :inventoryId", "_domainContext": { "locationId": locationId, "inventoryId": kInventoryId, }, "_archived": false, }, }, ); if (response.statusCode == 200 && response.data['data'] != null) { List lines = []; for (var line in response.data["data"]) { lines.add(InventoryLine.fromJson(line)); } return lines; } return []; } catch (e) { return null; } } Future isSessionValid() async { try { final res = await dio.get( '/ws/rest/com.axelor.apps.stock.db.StockLocation?offset=0&limit=1', data: { "offset": 0, "limit": 1, "data": { "criteria": [ { "fieldName": "usableOnImmobilisation", "operator": "=", "value": true, }, ], }, }, ); return res.statusCode == 200 && res.data['status'] == 0; } catch (_) { return false; } } Future?> fetchUserProfile() async { final username = _storage.read('username'); if (username == null) { return null; } try { final response = await dio.post( '/ws/rest/com.axelor.auth.db.User/search', data: { "offset": 0, "limit": 1, "data": { "criteria": [ {"fieldName": "code", "operator": "=", "value": username}, ], }, }, ); if (response.statusCode == 200 && response.data['status'] == 0 && response.data['data'] != null && response.data['data'].isNotEmpty) { final user = response.data['data'][0]; return user; } else {} } catch (e) {} return null; } Future?> fetchStockLocationByName(String name) async { try { final response = await dio.post( '${Utils.url}/ws/rest/com.axelor.apps.stock.db.StockLocation/search', data: { "offset": 0, "limit": 10000, "data": { "criteria": [ {"fieldName": "name", "operator": "=", "value": "$name"}, ], }, }, ); if (response.statusCode == 200 && response.data['status'] == 0 && response.data['data'] != null && response.data['data'].isNotEmpty) { final location = response.data['data'][0]; return location; } else {} } catch (e) {} return null; } Future fetchImageBytes(int imageId) async { final url = '${Utils.url}/ws/rest/com.axelor.meta.db.MetaFile/$imageId/content/download'; try { final response = await dio.get( url, options: Options(responseType: ResponseType.bytes), ); final contentType = response.headers.value('content-type'); if (contentType == null || (!contentType.startsWith('image/') && contentType != 'application/octet-stream')) { return null; } return Uint8List.fromList(response.data); } catch (e) { return null; } } Future createProducts(Product product, Depot? depot) async { final String url = '${Utils.url}/ws/rest/com.axelor.apps.base.db.Product'; try { // Create or update the InventoryLine final saveResponse = await dio.post( url, data: jsonEncode({"data": product.toJson()}), ); if (saveResponse.statusCode == 200) { final result = saveResponse.data; if (result['status'] == 0) { Product newProduct = Product.fromJson(result["data"][0]); TrackingNumber? trackingNumber = await createTrackingNumber( trackingNumber: TrackingNumber( trackingNumberSeq: product.internalDescription, product: Product(id: newProduct.id), ), ); // Create initial inventory line await createInitialInventoryLine(newProduct, trackingNumber, depot); return newProduct; } else {} } else {} } on DioException catch (e) { if (e.response != null) {} } catch (e) {} return null; } Future?> getFamilleProduit() async { final response = await dio.post( '${Utils.url}/ws/rest/com.axelor.apps.base.db.FamilleProduit/search', data: { "data": { "criteria": [ { "operator": "and", "criteria": [ {"fieldName": "niveau", "operator": "=", "value": 0}, { "fieldName": "usableOnImmobilisation", "operator": "=", "value": true, }, ], }, ], }, }, ); if (response.statusCode == 200 && response.data['data'] != null) { List familleProduits = []; for (var familleProduit in response.data["data"]) { familleProduits.add(FamilleProduit.fromJson(familleProduit)); } return familleProduits; } else { return []; } return null; } Future?> getSousFamilleProduit(parentId) async { final response = await dio.post( '${Utils.url}/ws/rest/com.axelor.apps.base.db.FamilleProduit/search', data: { "data": { "_domain": "self.parente.id = :parente", "_domainContext": {"parente": parentId}, "_archived": false, }, }, ); if (response.statusCode == 200 && response.data['data'] != null) { List sousfamilleProduits = []; for (var sousfamilleProduit in response.data["data"]) { sousfamilleProduits.add(FamilleProduit.fromJson(sousfamilleProduit)); } return sousfamilleProduits; } else { return []; } } Future?> getMyInventoryLines() async { final user = await fetchUserProfile(); final response = await dio.post( '${Utils.url}/ws/rest/com.axelor.apps.stock.db.InventoryLine/search', data: { "data": { "_domain": "self.createdBy.id = :createdBy and self.inventory.id = :inventoryId", "_domainContext": { "inventoryId": kInventoryId, "createdBy": user!['id'], }, "_archived": false, }, }, ); if (response.statusCode == 200 && response.data['data'] != null) { List? inventoryLines = []; for (var inventoryLine in response.data["data"]) { inventoryLines.add(InventoryLine.fromJson(inventoryLine)); } return inventoryLines; } else { return []; } return null; } Future createTrackingNumber({ required TrackingNumber trackingNumber, }) async { final String url = '${Utils.url}/ws/rest/com.axelor.apps.stock.db.TrackingNumber'; try { // 1. Check if product already exists final searchResponse = await dio.post( '${Utils.url}/ws/rest/com.axelor.apps.stock.db.TrackingNumber/search', data: { "data": { "_domain": "self.trackingNumberSeq = :trackingNumberSeq and self.product.id = :productId", "_domainContext": { "trackingNumberSeq": trackingNumber.trackingNumberSeq, "productId": trackingNumber.product, }, "_archived": false, }, }, ); if (searchResponse.statusCode == 200 && searchResponse.data['status'] == 0) { final existing = searchResponse.data['data']; if (existing != null && existing.isNotEmpty) { Get.snackbar('Erreur', "N° de serie existe deja"); return null; } else {} } // 2. Create or update the InventoryLine final saveResponse = await dio.post( url, data: jsonEncode({"data": trackingNumber.toJson()}), ); if (saveResponse.statusCode == 200) { final result = saveResponse.data; if (result['status'] == 0) { return TrackingNumber.fromJson(result["data"][0]); } else {} } else {} } on DioException catch (e) { if (e.response != null) {} } catch (e) {} return null; } Future createInitialInventoryLine( Product product, TrackingNumber? trackingNumber, Depot? depot, ) async { final user = await fetchUserProfile(); final line = InventoryLine( inventoryId: kInventoryId, productId: product.id!, productName: product.name ?? '', currentQty: 1, realQty: 1, description: "Initial entry from mobile", unitId: product.unit?.id ?? 4, countingTypeSelect: 1, firstCounting: 1, firstCountingDate: DateTime.now().toIso8601String(), firstCountingByUser: user, stockLocationId: depot!.id!, ticketId: product.internalDescription ?? '', secondCounting: null, thirdCounting: null, trackingNumberId: trackingNumber?.id, ); await saveInventoryLine(inventoryLine: line); } }