Fitur Checklist pada kebutuhan bahan
This commit is contained in:
parent
eae95fa4eb
commit
e08e9c91e9
|
|
@ -9,6 +9,7 @@ class PredictionController extends ChangeNotifier {
|
|||
|
||||
List<Map<String, dynamic>> recipes = [];
|
||||
Map<String, Map<String, dynamic>> recipeIngredients = {};
|
||||
Map<String, bool> ingredientSelections = {};
|
||||
|
||||
final Map<String, int> 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<String, int> get requiredIngredients {
|
||||
if (selectedRecipe == null || productionQuantity == 0) {
|
||||
return {};
|
||||
|
|
@ -90,6 +116,7 @@ class PredictionController extends ChangeNotifier {
|
|||
final required = <String, int>{};
|
||||
|
||||
ingredients.forEach((productName, details) {
|
||||
if (!isIngredientSelected(productName)) return;
|
||||
final quantity = (details['quantity'] as num).toInt();
|
||||
required[productName] = quantity * productionQuantity;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
|||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
final selectedRecipe = _controller.selectedRecipe;
|
||||
final displayIngredients =
|
||||
selectedRecipe == null
|
||||
? <String, Map<String, dynamic>>{}
|
||||
: (_controller.recipeIngredients[selectedRecipe] ??
|
||||
<String, Map<String, dynamic>>{});
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: PreferredSize(
|
||||
|
|
@ -526,212 +532,272 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
|||
),
|
||||
),
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue