diff --git a/lib/screens/prediction/prediction_controller.dart b/lib/screens/prediction/prediction_controller.dart index 2393c44..2889364 100644 --- a/lib/screens/prediction/prediction_controller.dart +++ b/lib/screens/prediction/prediction_controller.dart @@ -9,6 +9,7 @@ class PredictionController extends ChangeNotifier { List> recipes = []; Map> recipeIngredients = {}; + Map ingredientSelections = {}; final Map currentStock = { 'Tepung Terigu 1kg': 45000, @@ -46,6 +47,8 @@ class PredictionController extends ChangeNotifier { } } + _initializeIngredientSelections(); + return null; } catch (e) { return 'Gagal memuat resep: $e'; @@ -58,6 +61,7 @@ class PredictionController extends ChangeNotifier { void setSelectedRecipe(String? value) { selectedRecipe = value; isCalculated = false; + _initializeIngredientSelections(); notifyListeners(); } @@ -78,9 +82,31 @@ class PredictionController extends ChangeNotifier { 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 {}; @@ -90,6 +116,7 @@ class PredictionController extends ChangeNotifier { final required = {}; ingredients.forEach((productName, details) { + if (!isIngredientSelected(productName)) return; final quantity = (details['quantity'] as num).toInt(); required[productName] = quantity * productionQuantity; }); diff --git a/lib/screens/prediction/prediction_page.dart b/lib/screens/prediction/prediction_page.dart index 0dbce16..524ea1f 100644 --- a/lib/screens/prediction/prediction_page.dart +++ b/lib/screens/prediction/prediction_page.dart @@ -31,6 +31,12 @@ class _PredictionScreenState extends State { return AnimatedBuilder( animation: _controller, builder: (context, _) { + final selectedRecipe = _controller.selectedRecipe; + final displayIngredients = + selectedRecipe == null + ? >{} + : (_controller.recipeIngredients[selectedRecipe] ?? + >{}); return Scaffold( backgroundColor: const Color(0xFFF5F5F5), appBar: PreferredSize( @@ -526,212 +532,272 @@ class _PredictionScreenState extends State { ), ), const SizedBox(height: 12), - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), + if (displayIngredients.isEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: const Text( + 'Belum ada bahan untuk produk ini.', + style: TextStyle( + fontSize: 12, + color: Color(0xFF6B7280), ), - ], - ), - child: Column( - children: - _controller.requiredIngredients.entries.toList().asMap().entries.map(( - entry, - ) { - final isLast = - entry.key == - _controller - .requiredIngredients - .length - - 1; - final ingredient = entry.value.key; - final neededAmount = - entry.value.value; - final availableAmount = - _controller - .currentStock[ingredient] ?? - 0; - final unit = _controller - .getIngredientUnit(ingredient); - final isSufficient = - availableAmount >= neededAmount; + ), + ) + else + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: + displayIngredients.entries.toList().asMap().entries.map(( + entry, + ) { + final isLast = + entry.key == + displayIngredients.length - 1; + final ingredient = entry.value.key; + final details = entry.value.value; + final isSelected = _controller + .isIngredientSelected( + ingredient, + ); + final unit = + (details['unit'] as String?) ?? + _controller.getIngredientUnit( + ingredient, + ); + final quantityPerUnit = + (details['quantity'] as num) + .toInt(); + final neededAmount = + isSelected + ? quantityPerUnit * + _controller + .productionQuantity + : 0; + final availableAmount = + _controller + .currentStock[ingredient] ?? + 0; + final isSufficient = + availableAmount >= neededAmount; + final statusColor = + isSelected + ? (isSufficient + ? const Color( + 0xFF10B981, + ) + : const Color( + 0xFFDC2626, + )) + : const Color(0xFF9CA3AF); + final statusLabel = + isSelected + ? (isSufficient + ? 'Aman' + : 'Kurang') + : 'Diabaikan'; - return Column( - children: [ - Padding( - padding: const EdgeInsets.all( - 14, - ), - child: Column( - children: [ - Row( - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - Text( - _controller - .cleanIngredientName( - ingredient, - ), - style: const TextStyle( - fontSize: 13, - fontWeight: - FontWeight - .w600, - color: Color( - 0xFF1F2937, - ), - ), - ), - const SizedBox( - height: 6, - ), - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - const Text( - 'Kebutuhan', - style: TextStyle( - fontSize: - 11, - color: Color( - 0xFF9CA3AF, - ), - ), - ), - Text( - '$neededAmount $unit', - style: const TextStyle( - fontSize: - 12, - fontWeight: - FontWeight.w700, - color: Color( - 0xFF1F2937, - ), - ), - ), - ], - ), - ), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - const Text( - 'Stok Saat Ini', - style: TextStyle( - fontSize: - 11, - color: Color( - 0xFF9CA3AF, - ), - ), - ), - Text( - '$availableAmount $unit', - style: const TextStyle( - fontSize: - 12, - fontWeight: - FontWeight.w700, - color: Color( - 0xFF1F2937, - ), - ), - ), - ], - ), - ), - ], - ), - ], - ), - ), - const SizedBox( - width: 10, - ), - Container( - padding: - const EdgeInsets.symmetric( - horizontal: 10, - vertical: 6, - ), - decoration: BoxDecoration( - color: _controller - .getStatusColor( - ingredient, - ) - .withOpacity( - 0.15, - ), - borderRadius: - BorderRadius.circular( - 8, - ), - border: Border.all( - color: _controller - .getStatusColor( + return Column( + children: [ + Padding( + padding: const EdgeInsets.all( + 14, + ), + child: Column( + children: [ + Row( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Checkbox( + value: isSelected, + onChanged: (value) { + _controller + .setIngredientSelection( ingredient, - ) + value ?? + false, + ); + }, + activeColor: + const Color( + 0xFFA89080, + ), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + _controller + .cleanIngredientName( + ingredient, + ), + style: const TextStyle( + fontSize: + 13, + fontWeight: + FontWeight + .w600, + color: Color( + 0xFF1F2937, + ), + ), + ), + const SizedBox( + height: 6, + ), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Kebutuhan', + style: TextStyle( + fontSize: + 11, + color: Color( + 0xFF9CA3AF, + ), + ), + ), + Text( + '$neededAmount $unit', + style: const TextStyle( + fontSize: + 12, + fontWeight: + FontWeight.w700, + color: Color( + 0xFF1F2937, + ), + ), + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Stok Saat Ini', + style: TextStyle( + fontSize: + 11, + color: Color( + 0xFF9CA3AF, + ), + ), + ), + Text( + '$availableAmount $unit', + style: const TextStyle( + fontSize: + 12, + fontWeight: + FontWeight.w700, + color: Color( + 0xFF1F2937, + ), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + const SizedBox( + width: 10, + ), + Container( + padding: + const EdgeInsets.symmetric( + horizontal: + 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: statusColor .withOpacity( - 0.3, + 0.15, ), - width: 1, + borderRadius: + BorderRadius.circular( + 8, + ), + border: Border.all( + color: statusColor + .withOpacity( + 0.3, + ), + width: 1, + ), + ), + child: Text( + statusLabel, + style: TextStyle( + fontSize: 11, + fontWeight: + FontWeight + .w700, + color: + statusColor, + ), ), ), - child: Text( - isSufficient - ? 'Aman' - : 'Kurang', - style: TextStyle( - fontSize: 11, - fontWeight: - FontWeight - .w700, - color: _controller - .getStatusColor( - ingredient, - ), - ), - ), - ), - ], - ), - ], - ), - ), - if (!isLast) - Container( - height: 1, - color: const Color( - 0xFFF3F4F6, + ], + ), + ], ), ), - ], - ); - }).toList(), + if (!isLast) + Container( + height: 1, + color: const Color( + 0xFFF3F4F6, + ), + ), + ], + ); + }).toList(), + ), ), - ), const SizedBox(height: 16), if (!_controller.isStockSufficient) ...[ const Text( diff --git a/lib/services/ml_service.dart b/lib/services/ml_service.dart index 2ef9423..b89f9f1 100644 --- a/lib/services/ml_service.dart +++ b/lib/services/ml_service.dart @@ -5,7 +5,7 @@ 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.110.16:5000'; + static const String baseUrl = 'http://192.168.1.62:5000 '; static const int timeoutSeconds = 30;