import 'package:finalproject/services/ml_service.dart'; import 'package:flutter/material.dart'; class PredictionController extends ChangeNotifier { String? selectedRecipe; int productionQuantity = 0; bool isCalculated = false; bool isLoading = true; bool isSubmitting = false; List> recipes = []; Map> recipeIngredients = {}; Map ingredientSelections = {}; final Map currentStock = { 'Tepung Terigu 1kg': 45000, 'Telur 1kg': 12, 'Gula Pasir 1kg': 28000, 'Susu Bubuk': 8000, 'Cokelat Bubuk 250gr': 22000, 'Mentega 500gr': 15000, 'Keju Parut 250gr': 3000, 'Baking Powder': 60000, }; final Map productIds = {}; final Map productUnits = {}; static const double eggGramPerButir = 50; Future loadRecipes() async { isLoading = true; notifyListeners(); try { final fetchedRecipes = await MLService.getRecipes(); final fetchedProducts = await MLService.getProducts(); recipes = fetchedRecipes; recipeIngredients = {}; for (final recipe in recipes) { final recipeName = recipe['recipe_name'] as String?; if (recipeName == null) continue; recipeIngredients[recipeName] = {}; if (recipe['ingredients'] != null) { for (final ingredient in recipe['ingredients']) { recipeIngredients[recipeName]![ingredient['product_name']] = { 'quantity': ingredient['quantity_needed'], 'unit': ingredient['unit'], }; } } } _applyProductStocks(fetchedProducts); _initializeIngredientSelections(); return null; } catch (e) { return 'Gagal memuat resep: $e'; } finally { isLoading = false; notifyListeners(); } } Future refreshStock() async { try { final fetchedProducts = await MLService.getProducts(); _applyProductStocks(fetchedProducts); } catch (_) { // Ignore refresh errors; keep existing stock } finally { notifyListeners(); } } void _applyProductStocks(List> products) { if (products.isEmpty) return; currentStock.clear(); productIds.clear(); productUnits.clear(); for (final product in products) { final name = (product['name'] ?? product['product_name'] ?? '').toString(); if (name.isEmpty) continue; final id = _toInt(product['id']); final unit = (product['unit'] ?? '').toString(); final stock = _toDouble(product['current_stock'] ?? product['stock']); currentStock[name] = stock; if (id != null) { productIds[name] = id; } if (unit.isNotEmpty) { productUnits[name] = unit; } } } void setSelectedRecipe(String? value) { selectedRecipe = value; isCalculated = false; _initializeIngredientSelections(); notifyListeners(); } void setProductionQuantity(String value) { productionQuantity = int.tryParse(value) ?? 0; isCalculated = false; notifyListeners(); } bool get canCalculate => selectedRecipe != null && productionQuantity > 0; void calculate() { isCalculated = true; notifyListeners(); } void reset() { selectedRecipe = null; productionQuantity = 0; isCalculated = false; ingredientSelections = {}; notifyListeners(); } void setIngredientSelection(String ingredient, bool isSelected) { ingredientSelections[ingredient] = isSelected; notifyListeners(); } bool isIngredientSelected(String ingredient) { return ingredientSelections[ingredient] ?? true; } void _initializeIngredientSelections() { if (selectedRecipe == null) { ingredientSelections = {}; return; } final ingredients = recipeIngredients[selectedRecipe] ?? {}; ingredientSelections = { for (final ingredient in ingredients.keys) ingredient: true, }; } Map get requiredIngredients { if (selectedRecipe == null || productionQuantity == 0) { return {}; } final ingredients = recipeIngredients[selectedRecipe] ?? {}; final required = {}; ingredients.forEach((productName, details) { if (!isIngredientSelected(productName)) return; final quantity = _toDouble(details['quantity']); required[productName] = quantity * productionQuantity; }); return required; } Map get insufficientStock { final required = requiredIngredients; final insufficient = {}; required.forEach((ingredient, neededAmount) { final requiredInStockUnit = _convertToStockUnit( ingredient: ingredient, amount: neededAmount, fromUnit: getIngredientUnit(ingredient), ); final available = getCurrentStock(ingredient); if (available < requiredInStockUnit) { insufficient[ingredient] = requiredInStockUnit - available; } }); return insufficient; } bool get isStockSufficient => insufficientStock.isEmpty; String getIngredientUnit(String ingredient) { if (selectedRecipe == null) return 'gr'; final recipeIngs = recipeIngredients[selectedRecipe]; if (recipeIngs == null) return 'gr'; final ingData = recipeIngs[ingredient]; if (ingData == null) return 'gr'; return ingData['unit'] as String? ?? 'gr'; } String getStockUnit(String ingredient) { final key = _matchingProductName(ingredient); final unit = key == null ? productUnits[ingredient] : productUnits[key]; if (unit != null && unit.isNotEmpty) return _normalizeUnit(unit); return 'kg'; } double getCurrentStock(String ingredient) { final key = _matchingProductName(ingredient); return key == null ? currentStock[ingredient] ?? 0 : currentStock[key] ?? 0; } int? getProductId(String ingredient) { final key = _matchingProductName(ingredient); return key == null ? productIds[ingredient] : productIds[key]; } double toGram({required double amount, required String unit}) { final normalized = _normalizeUnit(unit); if (normalized == 'gr') return amount; if (normalized == 'kg') return amount * 1000; if (normalized == 'ml') return amount; if (normalized == 'l') return amount * 1000; if (normalized == 'butir') return amount * eggGramPerButir; return amount; } double getRequiredInStockUnit(String ingredient) { final required = requiredIngredients[ingredient] ?? 0; return _convertToStockUnit( ingredient: ingredient, amount: required, fromUnit: getIngredientUnit(ingredient), ); } Color getStatusColor(String ingredient) { final required = _convertToStockUnit( ingredient: ingredient, amount: requiredIngredients[ingredient] ?? 0, fromUnit: getIngredientUnit(ingredient), ); final available = getCurrentStock(ingredient); return available >= required ? const Color(0xFF10B981) : const Color(0xFFDC2626); } String cleanIngredientName(String ingredient) { return ingredient .replaceAll( RegExp( r'\s+\d+([,.]\d+)?\s*(kg|kilogram|g|gr|gram|ml|l|liter|ltr|butir|pcs)\b', caseSensitive: false, ), '', ) .trim(); } String formatQuantity(double value) { if (value % 1 == 0) { return value.toInt().toString(); } return value .toStringAsFixed(2) .replaceAll(RegExp(r'0+$'), '') .replaceAll(RegExp(r'\.$'), ''); } String formatStockQuantity(String ingredient, double value) { final unit = getStockUnit(ingredient); if (unit == 'kg' && value > 0 && value < 1) { return '${formatQuantity(value * 1000)} gr'; } if (unit == 'l' && value > 0 && value < 1) { return '${formatQuantity(value * 1000)} ml'; } return '${formatQuantity(value)} $unit'; } String _normalizeUnit(String unit) { final normalized = unit.trim().toLowerCase(); if (['g', 'gr', 'gram', 'grams'].contains(normalized)) return 'gr'; if (['kg', 'kilogram', 'kilograms'].contains(normalized)) return 'kg'; if (['ml', 'mili', 'mililiter', 'milliliter'].contains(normalized)) { return 'ml'; } if (['l', 'lt', 'ltr', 'liter', 'litre'].contains(normalized)) return 'l'; if (['butir', 'pcs', 'piece', 'pieces'].contains(normalized)) { return 'butir'; } return normalized; } double _toDouble(dynamic value) { if (value is num) return value.toDouble(); return double.tryParse(value?.toString() ?? '') ?? 0; } int? _toInt(dynamic value) { if (value is int) return value; if (value is num) return value.toInt(); return int.tryParse(value?.toString() ?? ''); } double _convertToStockUnit({ required String ingredient, required double amount, required String fromUnit, }) { final stockUnit = _normalizeUnit(getStockUnit(ingredient)); final unit = _normalizeUnit(fromUnit); if (unit == stockUnit) return amount; if (unit == 'gr' && stockUnit == 'kg') return amount / 1000; if (unit == 'kg' && stockUnit == 'gr') return amount * 1000; if (unit == 'butir' && stockUnit == 'kg') { return (amount * eggGramPerButir) / 1000; } if (unit == 'butir' && stockUnit == 'gr') { return amount * eggGramPerButir; } if (unit == 'ml' && stockUnit == 'kg') return amount / 1000; if (unit == 'l' && stockUnit == 'kg') return amount; if (unit == 'kg' && stockUnit == 'ml') return amount * 1000; if (unit == 'kg' && stockUnit == 'l') return amount; if (unit == 'ml' && stockUnit == 'gr') return amount; if (unit == 'gr' && stockUnit == 'ml') return amount; if (unit == 'l' && stockUnit == 'ml') return amount * 1000; if (unit == 'ml' && stockUnit == 'l') return amount / 1000; if (unit == 'kg' && stockUnit == 'butir') { return (amount * 1000) / eggGramPerButir; } if (unit == 'gr' && stockUnit == 'butir') { return amount / eggGramPerButir; } return amount; } String _ingredientKey(String value) { return cleanIngredientName( value, ).toLowerCase().replaceAll(RegExp(r'\s+'), ' '); } String? _matchingProductName(String ingredient) { if (currentStock.containsKey(ingredient)) return ingredient; final target = _ingredientKey(ingredient); for (final name in currentStock.keys) { if (_ingredientKey(name) == target) return name; } return null; } Map get stockUsage { final required = requiredIngredients; final usage = {}; required.forEach((ingredient, neededAmount) { if (!isIngredientSelected(ingredient)) return; final unit = getIngredientUnit(ingredient); usage[ingredient] = _convertToStockUnit( ingredient: ingredient, amount: neededAmount, fromUnit: unit, ); }); return usage; } String buildEstimatedNeedsText() { final usage = stockUsage; if (usage.isEmpty) return 'Belum tersedia'; return usage.entries .where((entry) => entry.value > 0) .map((entry) { final name = cleanIngredientName(entry.key); final amount = formatStockQuantity(entry.key, entry.value); return '$name: $amount'; }) .join(', '); } Future> submitProduction() async { if (isSubmitting) { return { 'status': 'error', 'message': 'Permintaan sedang diproses, mohon tunggu', }; } isSubmitting = true; notifyListeners(); try { if (selectedRecipe == null || !isCalculated) { return { 'status': 'error', 'message': 'Hitung kebutuhan terlebih dahulu', }; } await refreshStock(); if (insufficientStock.isNotEmpty) { return { 'status': 'error', 'message': 'Stok masih kurang, silakan update stok dulu', }; } final usage = stockUsage; if (usage.isEmpty) { return {'status': 'error', 'message': 'Tidak ada bahan yang dipilih'}; } final items = usage.entries.where((entry) => entry.value > 0).map((entry) { final ingredient = entry.key; final quantity = entry.value; return { 'product_id': getProductId(ingredient), 'product_name': ingredient, 'quantity': quantity, 'unit': getStockUnit(ingredient), }; }).toList(); final result = await MLService.consumeStock( items: items, recipeName: selectedRecipe, productionQuantity: productionQuantity, ); if (result['status'] == 'success') { // Simpan hasil prediksi ke database agar laporan total prediksi ter-update. await MLService.savePrediction( productName: selectedRecipe ?? 'Produk', category: 'Produk', unitPrice: 0, predictionDate: DateTime.now().toIso8601String().split('T').first, predictedQuantity: productionQuantity, estimatedNeeds: buildEstimatedNeedsText(), ); for (final entry in usage.entries) { final ingredient = entry.key; final quantity = entry.value; final key = _matchingProductName(ingredient) ?? ingredient; final available = currentStock[key] ?? 0; currentStock[key] = (available - quantity).clamp(0, double.infinity); } await refreshStock(); } return result; } catch (e) { return {'status': 'error', 'message': 'Gagal submit: $e'}; } finally { isSubmitting = false; notifyListeners(); } } }