import 'package:http/http.dart' as http; import 'dart:async'; import 'dart:convert'; class MLService { // API URL - Change based on environment // Untuk emulator Android: 10.0.2.2 // Untuk device fisik: 192.168.x.x atau 127.0.0.1 kalau local static const String baseUrl = 'http://192.168.1.2:5000'; static const int timeoutSeconds = 30; /// Login using account data from MySQL login table. static Future> login({ required String usernameOrEmail, required String password, }) async { try { final response = await http .post( Uri.parse('$baseUrl/api/login'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'email': usernameOrEmail, 'username': usernameOrEmail, 'password': password, }), ) .timeout(Duration(seconds: timeoutSeconds)); final body = jsonDecode(response.body) as Map; if (response.statusCode == 200) { return body; } return {'status': 'error', 'message': body['message'] ?? 'Login gagal'}; } on TimeoutException { return { 'status': 'error', 'message': 'Server tidak merespons. Pastikan Flask aktif dan IP/port API sudah benar.', }; } catch (e) { return { 'status': 'error', 'message': 'Tidak dapat terhubung ke server: $e', }; } } static Future> sendForgotPasswordOtp({ required String usernameOrEmail, }) async { return _postJson('/api/forgot-password/send-otp', { 'email': usernameOrEmail, 'username': usernameOrEmail, }, fallbackMessage: 'Gagal membuat OTP'); } static Future> verifyForgotPasswordOtp({ required String usernameOrEmail, required String otp, }) async { return _postJson('/api/forgot-password/verify-otp', { 'email': usernameOrEmail, 'username': usernameOrEmail, 'otp': otp, }, fallbackMessage: 'Gagal verifikasi OTP'); } static Future> resetPasswordWithOtp({ required String usernameOrEmail, required String otp, required String newPassword, }) async { return _postJson('/api/forgot-password/reset', { 'email': usernameOrEmail, 'username': usernameOrEmail, 'otp': otp, 'new_password': newPassword, }, fallbackMessage: 'Gagal reset password'); } static Future> changePassword({ required String usernameOrEmail, required String currentPassword, required String newPassword, }) async { return _postJson('/api/change-password', { 'email': usernameOrEmail, 'username': usernameOrEmail, 'current_password': currentPassword, 'new_password': newPassword, }, fallbackMessage: 'Gagal mengubah password'); } static Future> _postJson( String path, Map payload, { required String fallbackMessage, }) async { try { final response = await http .post( Uri.parse('$baseUrl$path'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(payload), ) .timeout(Duration(seconds: timeoutSeconds)); final body = jsonDecode(response.body) as Map; if (response.statusCode >= 200 && response.statusCode < 300) { return body; } return {'status': 'error', 'message': body['message'] ?? fallbackMessage}; } on TimeoutException { return { 'status': 'error', 'message': 'Server tidak merespons. Pastikan Flask aktif dan IP/port API sudah benar.', }; } catch (e) { return { 'status': 'error', 'message': 'Tidak dapat terhubung ke server: $e', }; } } /// Convert grams to kilograms with rounding rules: /// - <= 500g -> 0.5kg /// - 500g < g <= 1000g -> 1kg /// - > 1000g -> round up to nearest 0.5kg static double gramsToKgRounded(num grams) { if (grams <= 0) { throw ArgumentError('grams must be greater than 0'); } if (grams <= 500) return 0.5; if (grams <= 1000) return 1.0; return (grams / 500).ceilToDouble() * 0.5; } /// Health Check - Test if API is running static Future healthCheck() async { try { final response = await http .get(Uri.parse('$baseUrl/health')) .timeout(Duration(seconds: 5)); return response.statusCode == 200; } catch (e) { print('Health check error: $e'); return false; } } /// Get Metadata - Retrieve model information static Future> getMetadata() async { try { final response = await http .get(Uri.parse('$baseUrl/metadata')) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 200) { return jsonDecode(response.body); } else { return {'status': 'error', 'message': 'Failed to get metadata'}; } } catch (e) { return {'status': 'error', 'message': 'Connection error: $e'}; } } /// Simplified Prediction - For UI use static Future> simplePrediksi({ required String productName, required String category, required int unitPrice, DateTime? predictionDate, }) async { try { final date = predictionDate ?? DateTime.now(); // Map product names to encoded values (adjust based on your encoding) final productMap = { 'Tepung Terigu 1kg': 1, 'Telur 1kg': 2, 'Gula Pasir 1kg': 3, 'Susu Bubuk': 4, 'Cokelat Bubuk 250gr': 5, 'Mentega 500gr': 6, 'Keju Parut 250gr': 7, 'Baking Powder': 8, }; final categoryMap = { 'Tepung': 1, 'Telur': 2, 'Gula': 3, 'Susu': 4, 'Cokelat': 5, 'Mentega': 6, 'Keju': 7, 'Bahan Tambahan': 8, }; final produkEncoded = productMap[productName] ?? 1; final kategoriEncoded = categoryMap[category] ?? 1; return await prediksiStok( tahun: date.year, bulan: date.month, hari: date.day, hariDalamMinggu: date.weekday, hariMinggu: date.weekday, hargaSatuanUpdate: unitPrice, totalHargaUpdate: unitPrice, // Simplified produkEncoded: produkEncoded, namaProdukEncoded: produkEncoded, kategoriProdukEncoded: kategoriEncoded, ); } catch (e) { return {'status': 'error', 'message': 'Connection error: $e'}; } } /// Single Prediction - Predict stock demand static Future> prediksiStok({ required int tahun, required int bulan, required int hari, required int hariDalamMinggu, required int hariMinggu, required int hargaSatuanUpdate, required int totalHargaUpdate, required int produkEncoded, required int namaProdukEncoded, required int kategoriProdukEncoded, }) async { try { final data = { 'tahun': tahun, 'bulan': bulan, 'hari': hari, 'hari_dalam_minggu': hariDalamMinggu, 'hari_minggu': hariMinggu, 'harga_satuan_update': hargaSatuanUpdate, 'total_harga_update': totalHargaUpdate, 'produk_encoded': produkEncoded, 'nama_produk_encoded': namaProdukEncoded, 'kategori_produk_encoded': kategoriProdukEncoded, }; final response = await http .post( Uri.parse('$baseUrl/prediksi'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(data), ) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 200) { return jsonDecode(response.body); } else { return { 'status': 'error', 'message': 'Server error: ${response.statusCode}', }; } } catch (e) { return {'status': 'error', 'message': 'Connection error: $e'}; } } /// Batch Prediction - Predict multiple items static Future> batchPrediksi( List> items, ) async { try { final data = {'items': items}; final response = await http .post( Uri.parse('$baseUrl/batch-prediksi'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(data), ) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 200) { return jsonDecode(response.body); } else { return { 'status': 'error', 'message': 'Server error: ${response.statusCode}', }; } } catch (e) { return {'status': 'error', 'message': 'Connection error: $e'}; } } /// Get API Info static Future> getInfo() async { try { final response = await http .get(Uri.parse('$baseUrl/info')) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 200) { return jsonDecode(response.body); } else { return {'status': 'error', 'message': 'Failed to get info'}; } } catch (e) { return {'status': 'error', 'message': 'Connection error: $e'}; } } // ======================================================================== // DASHBOARD ENDPOINTS // ======================================================================== /// Ringkasan dashboard: penggunaan bahan static Future> getDashboardSummary() async { try { final response = await http .get(Uri.parse('$baseUrl/api/dashboard/summary')) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 200) { return jsonDecode(response.body); } return { 'status': false, 'message': 'Server error: ${response.statusCode}', 'penggunaan_bahan': [], }; } catch (e) { return { 'status': false, 'message': 'Connection error: $e', 'penggunaan_bahan': [], }; } } /// Total bahan keluar yang digunakan hari ini. static Future> getBahanDigunakanHariIni() async { const fallback = { 'total': 0, 'satuan': 'kg', 'keterangan': 'total penggunaan hari ini', }; try { final response = await http .get(Uri.parse('$baseUrl/api/dashboard/bahan-digunakan-hari-ini')) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 200) { final data = jsonDecode(response.body); return data is Map ? data : fallback; } return { ...fallback, 'status': false, 'message': 'Server error: ${response.statusCode}', }; } catch (e) { return {...fallback, 'status': false, 'message': 'Connection error: $e'}; } } // ======================================================================== // REPORT ENDPOINTS - LAPORAN // ======================================================================== /// Helper GET untuk endpoint laporan (response: {status, message, data}) static Future> _getReport(String path) async { try { final response = await http .get(Uri.parse('$baseUrl$path')) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 200) { return jsonDecode(response.body); } print('Report request failed [$path]: ${response.statusCode}'); return { 'status': false, 'message': 'Server error: ${response.statusCode}', 'data': [], }; } catch (e) { print('Report request error [$path]: $e'); return {'status': false, 'message': 'Connection error: $e', 'data': []}; } } /// Laporan stok bahan static Future> getReportStock() async { return _getReport('/laporan/stok'); } /// Riwayat stok masuk static Future> getReportStockIn() async { return _getReport('/laporan/stok-masuk'); } /// Laporan prediksi permintaan static Future> getReportPredictions() async { return _getReport('/laporan/prediksi'); } /// Laporan bahan kritis static Future> getReportCritical() async { return _getReport('/laporan/bahan-kritis'); } // ======================================================================== // DATABASE ENDPOINTS - PRODUCTS & TRANSACTIONS // ======================================================================== /// Consume stock (batch) after production static Future> consumeStock({ required List> items, String? recipeName, int? productionQuantity, }) async { try { final data = { 'items': items, if (recipeName != null) 'recipe_name': recipeName, if (productionQuantity != null) 'production_quantity': productionQuantity, }; print('[consumeStock] Request payload: ${jsonEncode(data)}'); final response = await http .post( Uri.parse('$baseUrl/stock/consume'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(data), ) .timeout(Duration(seconds: timeoutSeconds)); print('[consumeStock] Response status: ${response.statusCode}'); print('[consumeStock] Response body: ${response.body}'); final parsedBody = response.body.isNotEmpty ? jsonDecode(response.body) : null; if (response.statusCode == 200) { return parsedBody is Map ? parsedBody : {'status': 'success'}; } if (parsedBody is Map) { return parsedBody; } return { 'status': 'error', 'message': 'Server error: ${response.statusCode}', }; } catch (e) { print('Consume stock error: $e'); return {'status': 'error', 'message': 'Connection error: $e'}; } } /// Get all products from database static Future>> getProducts() async { try { final response = await http .get(Uri.parse('$baseUrl/products')) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['status'] == 'success') { return List>.from(data['products']); } } return []; } catch (e) { print('Get products error: $e'); return []; } } /// Create new product in database static Future> createProduct({ required String name, required String category, required int price, required int currentStock, double? minStock, String? unit, String? productType, }) async { try { final data = { 'name': name, 'category': category, 'price': price, 'current_stock': currentStock, if (minStock != null) 'min_stock': minStock, if (unit != null) 'unit': unit, if (productType != null) 'product_type': productType, }; final response = await http .post( Uri.parse('$baseUrl/products'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(data), ) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 201) { return jsonDecode(response.body); } else if (response.statusCode == 409) { final result = jsonDecode(response.body); return result; } else if (response.statusCode == 400) { final result = jsonDecode(response.body); return { 'status': 'error', 'message': result['message'] ?? 'Bad request', }; } else { print('Create product error - Status: ${response.statusCode}'); print('Response body: ${response.body}'); return { 'status': 'error', 'message': 'Server error: ${response.statusCode}', }; } } catch (e) { print('Create product exception: $e'); return {'status': 'error', 'message': 'Connection error: $e'}; } } /// Get specific product by ID static Future?> getProduct(int productId) async { try { final response = await http .get(Uri.parse('$baseUrl/products/$productId')) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['status'] == 'success') { return data['product']; } } return null; } catch (e) { print('Get product error: $e'); return null; } } /// Save transaction and update stock automatically static Future> addTransactionWithStockUpdate({ required int productId, required int quantity, required int unitPrice, required int totalPrice, required String transactionDate, // Format: 'YYYY-MM-DD' }) async { try { final data = { 'produk_id': productId, 'jumlah': quantity, 'unit_price': unitPrice, 'total_price': totalPrice, 'transaction_date': transactionDate, }; final response = await http .post( Uri.parse('$baseUrl/transaksi'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(data), ) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 201) { final result = jsonDecode(response.body); return result; } else { return { 'status': 'error', 'message': 'Server error: ${response.statusCode}', }; } } catch (e) { print('Add transaction with stock update error: $e'); return {'status': 'error', 'message': 'Connection error: $e'}; } } /// Save transaction to database (legacy - without stock update) static Future saveTransaction({ required String productName, required String category, required int quantity, required int unitPrice, required int totalPrice, required String transactionDate, // Format: 'YYYY-MM-DD' }) async { try { final data = { 'product_name': productName, 'category': category, 'quantity': quantity, 'unit_price': unitPrice, 'total_price': totalPrice, 'transaction_date': transactionDate, }; final response = await http .post( Uri.parse('$baseUrl/transactions'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(data), ) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 201) { final result = jsonDecode(response.body); return result['status'] == 'success'; } return false; } catch (e) { print('Save transaction error: $e'); return false; } } /// Get transaction history from database static Future>> getTransactions({ int limit = 100, int offset = 0, String? productName, }) async { try { var url = '$baseUrl/transactions?limit=$limit&offset=$offset'; if (productName != null && productName.isNotEmpty) { url += '&product_name=$productName'; } final response = await http .get(Uri.parse(url)) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['status'] == 'success') { return List>.from(data['transactions']); } } return []; } catch (e) { print('Get transactions error: $e'); return []; } } /// Save prediction result to database static Future savePrediction({ required String productName, required String category, required int unitPrice, required String predictionDate, // Format: 'YYYY-MM-DD' required int predictedQuantity, double? rawValue, int? estimatedTotalPrice, String? estimatedNeeds, double? accuracyR2, double? errorMae, }) async { try { final data = { 'product_name': productName, 'category': category, 'unit_price': unitPrice, 'prediction_date': predictionDate, 'predicted_quantity': predictedQuantity, if (rawValue != null) 'raw_value': rawValue, if (estimatedTotalPrice != null) 'estimated_total_price': estimatedTotalPrice, if (estimatedNeeds != null) 'estimated_needs': estimatedNeeds, if (accuracyR2 != null) 'accuracy_r2': accuracyR2, if (errorMae != null) 'error_mae': errorMae, }; final response = await http .post( Uri.parse('$baseUrl/predictions'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(data), ) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 201) { final result = jsonDecode(response.body); return result['status'] == 'success'; } return false; } catch (e) { print('Save prediction error: $e'); return false; } } // ======================================================================== // RECIPES ENDPOINTS // ======================================================================== /// Get all recipes with ingredients static Future>> getRecipes() async { try { final response = await http .get(Uri.parse('$baseUrl/recipes')) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['status'] == 'success') { return List>.from(data['recipes']); } } return []; } catch (e) { print('Get recipes error: $e'); return []; } } /// Get specific recipe by ID with ingredients static Future?> getRecipe(int recipeId) async { try { final response = await http .get(Uri.parse('$baseUrl/recipes/$recipeId')) .timeout(Duration(seconds: timeoutSeconds)); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['status'] == 'success') { return data['recipe']; } } return null; } catch (e) { print('Get recipe error: $e'); return null; } } }