MIF_E31232435/lib/services/ml_service.dart

768 lines
22 KiB
Dart

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.16:5000';
static const int timeoutSeconds = 30;
/// Login using account data from MySQL login table.
static Future<Map<String, dynamic>> 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<String, dynamic>;
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<Map<String, dynamic>> sendForgotPasswordOtp({
required String usernameOrEmail,
}) async {
return _postJson('/api/forgot-password/send-otp', {
'email': usernameOrEmail,
'username': usernameOrEmail,
}, fallbackMessage: 'Gagal membuat OTP');
}
static Future<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> _postJson(
String path,
Map<String, dynamic> 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<String, dynamic>;
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<bool> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> batchPrediksi(
List<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<String, dynamic> ? 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<Map<String, dynamic>> _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<Map<String, dynamic>> getReportStock() async {
return _getReport('/laporan/stok');
}
/// Riwayat stok masuk
static Future<Map<String, dynamic>> getReportStockIn() async {
return _getReport('/laporan/stok-masuk');
}
/// Laporan prediksi permintaan
static Future<Map<String, dynamic>> getReportPredictions() async {
return _getReport('/laporan/prediksi');
}
/// Laporan bahan kritis
static Future<Map<String, dynamic>> getReportCritical() async {
return _getReport('/laporan/bahan-kritis');
}
// ========================================================================
// DATABASE ENDPOINTS - PRODUCTS & TRANSACTIONS
// ========================================================================
/// Consume stock (batch) after production
static Future<Map<String, dynamic>> consumeStock({
required List<Map<String, dynamic>> 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<String, dynamic>
? parsedBody
: {'status': 'success'};
}
if (parsedBody is Map<String, dynamic>) {
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<List<Map<String, dynamic>>> 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<Map<String, dynamic>>.from(data['products']);
}
}
return [];
} catch (e) {
print('Get products error: $e');
return [];
}
}
/// Create new product in database
static Future<Map<String, dynamic>> createProduct({
required String name,
required String category,
required int price,
required int currentStock,
String? unit,
String? productType,
}) async {
try {
final data = {
'name': name,
'category': category,
'price': price,
'current_stock': currentStock,
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<Map<String, dynamic>?> 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<Map<String, dynamic>> 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<bool> 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<List<Map<String, dynamic>>> 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<Map<String, dynamic>>.from(data['transactions']);
}
}
return [];
} catch (e) {
print('Get transactions error: $e');
return [];
}
}
/// Save prediction result to database
static Future<bool> 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<List<Map<String, dynamic>>> 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<Map<String, dynamic>>.from(data['recipes']);
}
}
return [];
} catch (e) {
print('Get recipes error: $e');
return [];
}
}
/// Get specific recipe by ID with ingredients
static Future<Map<String, dynamic>?> 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;
}
}
}