fix final1
This commit is contained in:
parent
cc37039155
commit
437f973963
|
|
@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS recipe_ingredients (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
recipe_id INT NOT NULL,
|
recipe_id INT NOT NULL,
|
||||||
product_name VARCHAR(100) NOT NULL,
|
product_name VARCHAR(100) NOT NULL,
|
||||||
quantity_needed INT NOT NULL,
|
quantity_needed FLOAT NOT NULL,
|
||||||
unit VARCHAR(20) NOT NULL,
|
unit VARCHAR(20) NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE,
|
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE,
|
||||||
|
|
@ -34,9 +34,9 @@ CREATE TABLE IF NOT EXISTS products (
|
||||||
product_name VARCHAR(100) NOT NULL UNIQUE,
|
product_name VARCHAR(100) NOT NULL UNIQUE,
|
||||||
category VARCHAR(50) NOT NULL,
|
category VARCHAR(50) NOT NULL,
|
||||||
price INT NOT NULL,
|
price INT NOT NULL,
|
||||||
current_stock INT NOT NULL DEFAULT 0,
|
current_stock DECIMAL(10,3) NOT NULL DEFAULT 0,
|
||||||
unit VARCHAR(20) NOT NULL,
|
unit VARCHAR(20) NOT NULL,
|
||||||
min_stock INT DEFAULT 0,
|
min_stock DECIMAL(10,3) NOT NULL DEFAULT 0,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
KEY idx_product_name (product_name),
|
KEY idx_product_name (product_name),
|
||||||
KEY idx_category (category)
|
KEY idx_category (category)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ class Product {
|
||||||
final String name;
|
final String name;
|
||||||
final String category;
|
final String category;
|
||||||
final int price;
|
final int price;
|
||||||
final int stock;
|
final double stock;
|
||||||
|
final double minStock;
|
||||||
final String unit;
|
final String unit;
|
||||||
final String status; // 'tersedia', 'sedang', 'kritis'
|
final String status; // 'tersedia', 'sedang', 'kritis'
|
||||||
|
|
||||||
|
|
@ -13,17 +14,27 @@ class Product {
|
||||||
required this.category,
|
required this.category,
|
||||||
required this.price,
|
required this.price,
|
||||||
required this.stock,
|
required this.stock,
|
||||||
|
this.minStock = 0,
|
||||||
this.unit = 'kg',
|
this.unit = 'kg',
|
||||||
this.status = 'tersedia',
|
this.status = 'tersedia',
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Product.fromJson(Map<String, dynamic> json) {
|
factory Product.fromJson(Map<String, dynamic> json) {
|
||||||
|
final stockRaw = json['stock'] ?? json['current_stock'] ?? 0;
|
||||||
|
final minStockRaw = json['min_stock'] ?? json['stok_minimum'] ?? 0;
|
||||||
return Product(
|
return Product(
|
||||||
id: json['id'] as int,
|
id: json['id'] as int,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
category: json['category'] as String,
|
category: json['category'] as String,
|
||||||
price: json['price'] as int,
|
price: json['price'] as int,
|
||||||
stock: json['stock'] as int,
|
stock:
|
||||||
|
stockRaw is num
|
||||||
|
? stockRaw.toDouble()
|
||||||
|
: double.tryParse(stockRaw.toString()) ?? 0,
|
||||||
|
minStock:
|
||||||
|
minStockRaw is num
|
||||||
|
? minStockRaw.toDouble()
|
||||||
|
: double.tryParse(minStockRaw.toString()) ?? 0,
|
||||||
unit: (json['unit'] as String?) ?? 'kg',
|
unit: (json['unit'] as String?) ?? 'kg',
|
||||||
status: json['status'] as String? ?? 'tersedia',
|
status: json['status'] as String? ?? 'tersedia',
|
||||||
);
|
);
|
||||||
|
|
@ -36,6 +47,7 @@ class Product {
|
||||||
'category': category,
|
'category': category,
|
||||||
'price': price,
|
'price': price,
|
||||||
'stock': stock,
|
'stock': stock,
|
||||||
|
'min_stock': minStock,
|
||||||
'unit': unit,
|
'unit': unit,
|
||||||
'status': status,
|
'status': status,
|
||||||
};
|
};
|
||||||
|
|
@ -46,7 +58,8 @@ class Product {
|
||||||
String? name,
|
String? name,
|
||||||
String? category,
|
String? category,
|
||||||
int? price,
|
int? price,
|
||||||
int? stock,
|
double? stock,
|
||||||
|
double? minStock,
|
||||||
String? unit,
|
String? unit,
|
||||||
String? status,
|
String? status,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -56,6 +69,7 @@ class Product {
|
||||||
category: category ?? this.category,
|
category: category ?? this.category,
|
||||||
price: price ?? this.price,
|
price: price ?? this.price,
|
||||||
stock: stock ?? this.stock,
|
stock: stock ?? this.stock,
|
||||||
|
minStock: minStock ?? this.minStock,
|
||||||
unit: unit ?? this.unit,
|
unit: unit ?? this.unit,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,11 @@ class DashboardController extends ChangeNotifier {
|
||||||
if (item is! Map) continue;
|
if (item is! Map) continue;
|
||||||
|
|
||||||
final stockValue = StockStatusUtils.parseStock(item['current_stock']);
|
final stockValue = StockStatusUtils.parseStock(item['current_stock']);
|
||||||
final statusKey = StockStatusUtils.statusFromStock(stockValue);
|
final minStock = StockStatusUtils.parseStock(item['min_stock']);
|
||||||
|
final statusKey = StockStatusUtils.statusFromStock(
|
||||||
|
stockValue,
|
||||||
|
minStock: minStock,
|
||||||
|
);
|
||||||
if (statusKey != StockStatusUtils.statusKritis) continue;
|
if (statusKey != StockStatusUtils.statusKritis) continue;
|
||||||
|
|
||||||
final category = item['category']?.toString().toLowerCase() ?? '';
|
final category = item['category']?.toString().toLowerCase() ?? '';
|
||||||
|
|
|
||||||
|
|
@ -88,13 +88,12 @@ class PredictionController extends ChangeNotifier {
|
||||||
(product['name'] ?? product['product_name'] ?? '').toString();
|
(product['name'] ?? product['product_name'] ?? '').toString();
|
||||||
if (name.isEmpty) continue;
|
if (name.isEmpty) continue;
|
||||||
|
|
||||||
final id = product['id'] ?? 0;
|
final id = _toInt(product['id']);
|
||||||
final unit = (product['unit'] ?? '').toString();
|
final unit = (product['unit'] ?? '').toString();
|
||||||
final stockRaw = product['current_stock'] ?? 0;
|
final stock = _toDouble(product['current_stock'] ?? product['stock']);
|
||||||
final stock = stockRaw is num ? stockRaw.toDouble() : 0.0;
|
|
||||||
|
|
||||||
currentStock[name] = stock;
|
currentStock[name] = stock;
|
||||||
if (id is int) {
|
if (id != null) {
|
||||||
productIds[name] = id;
|
productIds[name] = id;
|
||||||
}
|
}
|
||||||
if (unit.isNotEmpty) {
|
if (unit.isNotEmpty) {
|
||||||
|
|
@ -162,7 +161,7 @@ class PredictionController extends ChangeNotifier {
|
||||||
|
|
||||||
ingredients.forEach((productName, details) {
|
ingredients.forEach((productName, details) {
|
||||||
if (!isIngredientSelected(productName)) return;
|
if (!isIngredientSelected(productName)) return;
|
||||||
final quantity = (details['quantity'] as num).toDouble();
|
final quantity = _toDouble(details['quantity']);
|
||||||
required[productName] = quantity * productionQuantity;
|
required[productName] = quantity * productionQuantity;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -294,6 +293,17 @@ class PredictionController extends ChangeNotifier {
|
||||||
return normalized;
|
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({
|
double _convertToStockUnit({
|
||||||
required String ingredient,
|
required String ingredient,
|
||||||
required double amount,
|
required double amount,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ class ProductListController extends ChangeNotifier {
|
||||||
|
|
||||||
products =
|
products =
|
||||||
fetchedProducts.map((p) {
|
fetchedProducts.map((p) {
|
||||||
final stock = p['current_stock'] ?? 0;
|
final stock = StockStatusUtils.parseStock(p['current_stock']);
|
||||||
|
final minStock = StockStatusUtils.parseStock(p['min_stock']);
|
||||||
final category = p['category'] ?? '';
|
final category = p['category'] ?? '';
|
||||||
final unit =
|
final unit =
|
||||||
p['unit'] ??
|
p['unit'] ??
|
||||||
|
|
@ -30,8 +31,12 @@ class ProductListController extends ChangeNotifier {
|
||||||
category: category,
|
category: category,
|
||||||
price: p['price'] ?? 0,
|
price: p['price'] ?? 0,
|
||||||
stock: stock,
|
stock: stock,
|
||||||
|
minStock: minStock,
|
||||||
unit: unit,
|
unit: unit,
|
||||||
status: StockStatusUtils.statusFromStock(stock),
|
status: StockStatusUtils.statusFromStock(
|
||||||
|
stock,
|
||||||
|
minStock: minStock,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -74,8 +79,16 @@ class ProductListController extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
int get maxStock {
|
int get maxStock {
|
||||||
|
return maxStockValue.ceil().clamp(1, double.infinity).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
double get maxStockValue {
|
||||||
if (products.isEmpty) return 1;
|
if (products.isEmpty) return 1;
|
||||||
return products.fold<int>(0, (max, p) => p.stock > max ? p.stock : max);
|
final maxValue = products.fold<double>(
|
||||||
|
0,
|
||||||
|
(max, p) => p.stock > max ? p.stock : max,
|
||||||
|
);
|
||||||
|
return maxValue <= 0 ? 1 : maxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Color getStatusColor(String status) {
|
Color getStatusColor(String status) {
|
||||||
|
|
@ -107,6 +120,14 @@ class ProductListController extends ChangeNotifier {
|
||||||
|
|
||||||
String formatPrice(int price) => 'Rp ${(price ~/ 1000)}K';
|
String formatPrice(int price) => 'Rp ${(price ~/ 1000)}K';
|
||||||
|
|
||||||
|
String formatStock(double value) {
|
||||||
|
if (value % 1 == 0) return value.toInt().toString();
|
||||||
|
return value
|
||||||
|
.toStringAsFixed(3)
|
||||||
|
.replaceAll(RegExp(r'0+$'), '')
|
||||||
|
.replaceAll(RegExp(r'\.$'), '');
|
||||||
|
}
|
||||||
|
|
||||||
String getStatusLabel(String status) {
|
String getStatusLabel(String status) {
|
||||||
return StockStatusUtils.label(status, withIcon: true);
|
return StockStatusUtils.label(status, withIcon: true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -360,7 +360,7 @@ class _ProductListScreenState extends State<ProductListScreen> with RouteAware {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProductCard(Product product) {
|
Widget _buildProductCard(Product product) {
|
||||||
final maxStock = _controller.maxStock;
|
final maxStock = _controller.maxStockValue;
|
||||||
final stockPercentage = (product.stock / maxStock * 100).toInt();
|
final stockPercentage = (product.stock / maxStock * 100).toInt();
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
|
|
@ -488,7 +488,7 @@ class _ProductListScreenState extends State<ProductListScreen> with RouteAware {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'${product.stock} ${product.unit}',
|
'${_controller.formatStock(product.stock)} ${product.unit}',
|
||||||
style: AppTextStyles.labelLarge.copyWith(
|
style: AppTextStyles.labelLarge.copyWith(
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
|
|
@ -560,7 +560,7 @@ class _ProductListScreenState extends State<ProductListScreen> with RouteAware {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showProductDetail(Product product) {
|
void _showProductDetail(Product product) {
|
||||||
final maxStock = _controller.maxStock;
|
final maxStock = _controller.maxStockValue;
|
||||||
final stockPercentage =
|
final stockPercentage =
|
||||||
maxStock == 0 ? 0 : (product.stock / maxStock * 100).round();
|
maxStock == 0 ? 0 : (product.stock / maxStock * 100).round();
|
||||||
final statusColor = _controller.getStatusColor(product.status);
|
final statusColor = _controller.getStatusColor(product.status);
|
||||||
|
|
@ -672,7 +672,14 @@ class _ProductListScreenState extends State<ProductListScreen> with RouteAware {
|
||||||
_buildDetailTile(
|
_buildDetailTile(
|
||||||
icon: Icons.inventory_2_outlined,
|
icon: Icons.inventory_2_outlined,
|
||||||
label: 'Stok',
|
label: 'Stok',
|
||||||
value: '${product.stock} ${product.unit}',
|
value:
|
||||||
|
'${_controller.formatStock(product.stock)} ${product.unit}',
|
||||||
|
),
|
||||||
|
_buildDetailTile(
|
||||||
|
icon: Icons.low_priority_outlined,
|
||||||
|
label: 'Minimum',
|
||||||
|
value:
|
||||||
|
'${_controller.formatStock(product.minStock)} ${product.unit}',
|
||||||
),
|
),
|
||||||
_buildDetailTile(
|
_buildDetailTile(
|
||||||
icon: Icons.straighten_outlined,
|
icon: Icons.straighten_outlined,
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,11 @@ class ReportController extends ChangeNotifier {
|
||||||
if (item is! Map) continue;
|
if (item is! Map) continue;
|
||||||
|
|
||||||
final stock = StockStatusUtils.parseStock(item['current_stock']);
|
final stock = StockStatusUtils.parseStock(item['current_stock']);
|
||||||
final status = StockStatusUtils.statusFromStock(stock);
|
final minStock = StockStatusUtils.parseStock(item['min_stock']);
|
||||||
|
final status = StockStatusUtils.statusFromStock(
|
||||||
|
stock,
|
||||||
|
minStock: minStock,
|
||||||
|
);
|
||||||
if (status != StockStatusUtils.statusKritis) continue;
|
if (status != StockStatusUtils.statusKritis) continue;
|
||||||
|
|
||||||
final category = item['category']?.toString().toLowerCase() ?? '';
|
final category = item['category']?.toString().toLowerCase() ?? '';
|
||||||
|
|
|
||||||
|
|
@ -371,11 +371,14 @@ class _ReportScreenState extends State<ReportScreen> {
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = sortedItems[index];
|
final item = sortedItems[index];
|
||||||
final stockValue = StockStatusUtils.parseStock(item['stock']);
|
final stockValue = StockStatusUtils.parseStock(item['stock']);
|
||||||
final statusKey = StockStatusUtils.statusFromStock(stockValue);
|
final minimumStock = StockStatusUtils.parseStock(item['min_stock']);
|
||||||
|
final statusKey = StockStatusUtils.statusFromStock(
|
||||||
|
stockValue,
|
||||||
|
minStock: minimumStock,
|
||||||
|
);
|
||||||
final statusColor = StockStatusUtils.color(statusKey);
|
final statusColor = StockStatusUtils.color(statusKey);
|
||||||
final statusLabel = StockStatusUtils.label(statusKey);
|
final statusLabel = StockStatusUtils.label(statusKey);
|
||||||
final unit = item['unit']?.toString() ?? 'kg';
|
final unit = item['unit']?.toString() ?? 'kg';
|
||||||
final minimumStock = StockStatusUtils.parseStock(item['min_stock']);
|
|
||||||
|
|
||||||
return _buildReportItemCard(
|
return _buildReportItemCard(
|
||||||
icon: Icons.inventory_2_outlined,
|
icon: Icons.inventory_2_outlined,
|
||||||
|
|
@ -1468,7 +1471,13 @@ class _ReportScreenState extends State<ReportScreen> {
|
||||||
...stockItems.asMap().entries.map((entry) {
|
...stockItems.asMap().entries.map((entry) {
|
||||||
final item = entry.value;
|
final item = entry.value;
|
||||||
final stockValue = StockStatusUtils.parseStock(item['stock']);
|
final stockValue = StockStatusUtils.parseStock(item['stock']);
|
||||||
final statusKey = StockStatusUtils.statusFromStock(stockValue);
|
final minimumStock = StockStatusUtils.parseStock(
|
||||||
|
item['min_stock'],
|
||||||
|
);
|
||||||
|
final statusKey = StockStatusUtils.statusFromStock(
|
||||||
|
stockValue,
|
||||||
|
minStock: minimumStock,
|
||||||
|
);
|
||||||
return [
|
return [
|
||||||
'${entry.key + 1}',
|
'${entry.key + 1}',
|
||||||
item['name']?.toString() ?? '-',
|
item['name']?.toString() ?? '-',
|
||||||
|
|
@ -1620,7 +1629,11 @@ class _ReportScreenState extends State<ReportScreen> {
|
||||||
for (var index = 0; index < stockItems.length; index++) {
|
for (var index = 0; index < stockItems.length; index++) {
|
||||||
final item = stockItems[index];
|
final item = stockItems[index];
|
||||||
final stockValue = StockStatusUtils.parseStock(item['stock']);
|
final stockValue = StockStatusUtils.parseStock(item['stock']);
|
||||||
final statusKey = StockStatusUtils.statusFromStock(stockValue);
|
final minimumStock = StockStatusUtils.parseStock(item['min_stock']);
|
||||||
|
final statusKey = StockStatusUtils.statusFromStock(
|
||||||
|
stockValue,
|
||||||
|
minStock: minimumStock,
|
||||||
|
);
|
||||||
final statusLabel = StockStatusUtils.label(statusKey);
|
final statusLabel = StockStatusUtils.label(statusKey);
|
||||||
buffer.writeln(
|
buffer.writeln(
|
||||||
'${index + 1},${_escapeCsv(item['name'])},'
|
'${index + 1},${_escapeCsv(item['name'])},'
|
||||||
|
|
@ -1709,8 +1722,16 @@ class _ReportScreenState extends State<ReportScreen> {
|
||||||
return items.toList()..sort((a, b) {
|
return items.toList()..sort((a, b) {
|
||||||
final aStock = StockStatusUtils.parseStock(a['stock']);
|
final aStock = StockStatusUtils.parseStock(a['stock']);
|
||||||
final bStock = StockStatusUtils.parseStock(b['stock']);
|
final bStock = StockStatusUtils.parseStock(b['stock']);
|
||||||
final aStatus = StockStatusUtils.statusFromStock(aStock);
|
final aMinStock = StockStatusUtils.parseStock(a['min_stock']);
|
||||||
final bStatus = StockStatusUtils.statusFromStock(bStock);
|
final bMinStock = StockStatusUtils.parseStock(b['min_stock']);
|
||||||
|
final aStatus = StockStatusUtils.statusFromStock(
|
||||||
|
aStock,
|
||||||
|
minStock: aMinStock,
|
||||||
|
);
|
||||||
|
final bStatus = StockStatusUtils.statusFromStock(
|
||||||
|
bStock,
|
||||||
|
minStock: bMinStock,
|
||||||
|
);
|
||||||
final statusComparison = _statusOrder(
|
final statusComparison = _statusOrder(
|
||||||
aStatus,
|
aStatus,
|
||||||
).compareTo(_statusOrder(bStatus));
|
).compareTo(_statusOrder(bStatus));
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,7 @@ class TransactionController extends ChangeNotifier {
|
||||||
required String category,
|
required String category,
|
||||||
required int price,
|
required int price,
|
||||||
required int initialStock,
|
required int initialStock,
|
||||||
|
required double minStock,
|
||||||
required String unit,
|
required String unit,
|
||||||
required String productType,
|
required String productType,
|
||||||
}) async {
|
}) async {
|
||||||
|
|
@ -221,6 +222,7 @@ class TransactionController extends ChangeNotifier {
|
||||||
category: category,
|
category: category,
|
||||||
price: price,
|
price: price,
|
||||||
currentStock: initialStock,
|
currentStock: initialStock,
|
||||||
|
minStock: minStock,
|
||||||
unit: unit,
|
unit: unit,
|
||||||
productType: productType,
|
productType: productType,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
final newProductNameController = TextEditingController();
|
final newProductNameController = TextEditingController();
|
||||||
final newPriceController = TextEditingController();
|
final newPriceController = TextEditingController();
|
||||||
final newStockController = TextEditingController();
|
final newStockController = TextEditingController();
|
||||||
|
final newMinStockController = TextEditingController();
|
||||||
const unitOptionsByType = {
|
const unitOptionsByType = {
|
||||||
'Bahan': ['kg'],
|
'Bahan': ['kg'],
|
||||||
'Barang': ['pcs'],
|
'Barang': ['pcs'],
|
||||||
|
|
@ -268,6 +269,30 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
fillColor: AppColors.bgLight,
|
fillColor: AppColors.bgLight,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Stok Minimum',
|
||||||
|
style: AppTextStyles.labelLarge.copyWith(
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
controller: newMinStockController,
|
||||||
|
keyboardType:
|
||||||
|
const TextInputType.numberWithOptions(
|
||||||
|
decimal: true,
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText:
|
||||||
|
'Batas minimum stok untuk peringatan',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: AppColors.bgLight,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -308,11 +333,16 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
newPriceController.text.trim();
|
newPriceController.text.trim();
|
||||||
final stockText =
|
final stockText =
|
||||||
newStockController.text.trim();
|
newStockController.text.trim();
|
||||||
|
final minStockText =
|
||||||
|
newMinStockController.text
|
||||||
|
.trim()
|
||||||
|
.replaceAll(',', '.');
|
||||||
|
|
||||||
if (productName.isEmpty ||
|
if (productName.isEmpty ||
|
||||||
category.isEmpty ||
|
category.isEmpty ||
|
||||||
priceText.isEmpty ||
|
priceText.isEmpty ||
|
||||||
stockText.isEmpty) {
|
stockText.isEmpty ||
|
||||||
|
minStockText.isEmpty) {
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
context,
|
context,
|
||||||
).showSnackBar(
|
).showSnackBar(
|
||||||
|
|
@ -361,12 +391,37 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final minStock =
|
||||||
|
double.tryParse(minStockText) ??
|
||||||
|
-1;
|
||||||
|
if (minStock < 0) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: const Text(
|
||||||
|
'Stok minimum tidak boleh negatif',
|
||||||
|
),
|
||||||
|
backgroundColor:
|
||||||
|
AppColors.statusError,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final navigator = Navigator.of(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
final messenger =
|
||||||
|
ScaffoldMessenger.of(context);
|
||||||
|
|
||||||
final result = await _controller
|
final result = await _controller
|
||||||
.createProduct(
|
.createProduct(
|
||||||
productName: productName,
|
productName: productName,
|
||||||
category: category,
|
category: category,
|
||||||
price: priceInt,
|
price: priceInt,
|
||||||
initialStock: stockInt,
|
initialStock: stockInt,
|
||||||
|
minStock: minStock,
|
||||||
unit: selectedUnit,
|
unit: selectedUnit,
|
||||||
productType:
|
productType:
|
||||||
selectedProductType,
|
selectedProductType,
|
||||||
|
|
@ -375,10 +430,8 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
if (result['status'] == 'success') {
|
if (result['status'] == 'success') {
|
||||||
Navigator.pop(context);
|
navigator.pop();
|
||||||
ScaffoldMessenger.of(
|
messenger.showSnackBar(
|
||||||
context,
|
|
||||||
).showSnackBar(
|
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
'Produk "$productName" berhasil ditambahkan',
|
'Produk "$productName" berhasil ditambahkan',
|
||||||
|
|
@ -388,9 +441,7 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(
|
messenger.showSnackBar(
|
||||||
context,
|
|
||||||
).showSnackBar(
|
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
result['message'] ??
|
result['message'] ??
|
||||||
|
|
@ -724,7 +775,7 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
height: 32,
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.statusError
|
color: AppColors.statusError
|
||||||
.withOpacity(0.1),
|
.withValues(alpha: 0.1),
|
||||||
borderRadius:
|
borderRadius:
|
||||||
BorderRadius.circular(6),
|
BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ class MLService {
|
||||||
// API URL - Change based on environment
|
// API URL - Change based on environment
|
||||||
// Untuk emulator Android: 10.0.2.2
|
// Untuk emulator Android: 10.0.2.2
|
||||||
// Untuk device fisik: 192.168.x.x atau 127.0.0.1 kalau local
|
// 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 String baseUrl = 'http://192.168.1.2:5000';
|
||||||
|
|
||||||
static const int timeoutSeconds = 30;
|
static const int timeoutSeconds = 30;
|
||||||
|
|
||||||
|
|
@ -501,6 +501,7 @@ class MLService {
|
||||||
required String category,
|
required String category,
|
||||||
required int price,
|
required int price,
|
||||||
required int currentStock,
|
required int currentStock,
|
||||||
|
double? minStock,
|
||||||
String? unit,
|
String? unit,
|
||||||
String? productType,
|
String? productType,
|
||||||
}) async {
|
}) async {
|
||||||
|
|
@ -510,6 +511,7 @@ class MLService {
|
||||||
'category': category,
|
'category': category,
|
||||||
'price': price,
|
'price': price,
|
||||||
'current_stock': currentStock,
|
'current_stock': currentStock,
|
||||||
|
if (minStock != null) 'min_stock': minStock,
|
||||||
if (unit != null) 'unit': unit,
|
if (unit != null) 'unit': unit,
|
||||||
if (productType != null) 'product_type': productType,
|
if (productType != null) 'product_type': productType,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,15 @@ class StockStatusUtils {
|
||||||
return double.tryParse(value?.toString() ?? '') ?? 0.0;
|
return double.tryParse(value?.toString() ?? '') ?? 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String statusFromStock(num stockKg) {
|
static String statusFromStock(num stockKg, {num? minStock}) {
|
||||||
final stock = stockKg.toDouble();
|
final stock = stockKg.toDouble();
|
||||||
|
final minimum = minStock?.toDouble() ?? 0;
|
||||||
|
if (minimum > 0) {
|
||||||
|
if (stock <= minimum) return statusKritis;
|
||||||
|
if (stock <= minimum * 2) return statusSedang;
|
||||||
|
return statusTersedia;
|
||||||
|
}
|
||||||
|
|
||||||
if (stock < criticalStockLimitKg) return statusKritis;
|
if (stock < criticalStockLimitKg) return statusKritis;
|
||||||
if (stock < warningStockLimitKg) return statusSedang;
|
if (stock < warningStockLimitKg) return statusSedang;
|
||||||
return statusTersedia;
|
return statusTersedia;
|
||||||
|
|
|
||||||
165
ml_model/app.py
165
ml_model/app.py
|
|
@ -108,6 +108,87 @@ def has_product_unit_column(connection) -> bool:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
return has_unit
|
return has_unit
|
||||||
|
|
||||||
|
def ensure_product_stock_precision(connection):
|
||||||
|
if not table_exists(connection, 'products'):
|
||||||
|
return
|
||||||
|
|
||||||
|
stock_col = get_existing_column(connection, 'products', ['current_stock'])
|
||||||
|
if not stock_col:
|
||||||
|
return
|
||||||
|
|
||||||
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
try:
|
||||||
|
cursor.execute("SHOW COLUMNS FROM products LIKE 'current_stock'")
|
||||||
|
column = cursor.fetchone()
|
||||||
|
column_type = (column or {}).get('Type', '').lower()
|
||||||
|
if any(kind in column_type for kind in ['decimal', 'float', 'double']):
|
||||||
|
return
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"ALTER TABLE products MODIFY current_stock DECIMAL(10,3) NOT NULL DEFAULT 0"
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
except Error as e:
|
||||||
|
logger.warning(f"Could not update current_stock precision: {e}")
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
def ensure_product_min_stock_column(connection):
|
||||||
|
if not table_exists(connection, 'products'):
|
||||||
|
return
|
||||||
|
|
||||||
|
min_col = get_existing_column(connection, 'products', ['min_stock'])
|
||||||
|
if min_col:
|
||||||
|
return
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"ALTER TABLE products ADD COLUMN min_stock DECIMAL(10,3) NOT NULL DEFAULT 0"
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
except Error as e:
|
||||||
|
logger.warning(f"Could not add min_stock column: {e}")
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
def backfill_default_min_stock(connection):
|
||||||
|
if not table_exists(connection, 'products'):
|
||||||
|
return
|
||||||
|
|
||||||
|
min_col = get_existing_column(connection, 'products', ['min_stock'])
|
||||||
|
if not min_col:
|
||||||
|
return
|
||||||
|
|
||||||
|
name_col = get_product_name_column(connection)
|
||||||
|
default_minimums = {
|
||||||
|
'Tepung Terigu 1kg': 10,
|
||||||
|
'Telur 1kg': 5,
|
||||||
|
'Gula Pasir 1kg': 8,
|
||||||
|
'Susu Bubuk': 4,
|
||||||
|
'Cokelat Bubuk 250gr': 3,
|
||||||
|
'Mentega 500gr': 5,
|
||||||
|
'Keju Parut 250gr': 2,
|
||||||
|
'Baking Powder': 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
try:
|
||||||
|
for product_name, min_stock in default_minimums.items():
|
||||||
|
cursor.execute(
|
||||||
|
f"""
|
||||||
|
UPDATE products
|
||||||
|
SET {min_col} = %s
|
||||||
|
WHERE {name_col} = %s AND COALESCE({min_col}, 0) = 0
|
||||||
|
""",
|
||||||
|
(min_stock, product_name)
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
except Error as e:
|
||||||
|
logger.warning(f"Could not backfill min_stock defaults: {e}")
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
def escape_table_name(table_name: str) -> str:
|
def escape_table_name(table_name: str) -> str:
|
||||||
return f"`{table_name}`"
|
return f"`{table_name}`"
|
||||||
|
|
||||||
|
|
@ -253,6 +334,11 @@ def fetch_stock_report(connection):
|
||||||
if not table_name:
|
if not table_name:
|
||||||
return None, 'Tabel bahan atau products tidak ditemukan'
|
return None, 'Tabel bahan atau products tidak ditemukan'
|
||||||
|
|
||||||
|
if table_name == 'products':
|
||||||
|
ensure_product_stock_precision(connection)
|
||||||
|
ensure_product_min_stock_column(connection)
|
||||||
|
backfill_default_min_stock(connection)
|
||||||
|
|
||||||
name_col = get_existing_column(connection, table_name, ['nama_bahan', 'product_name', 'name'])
|
name_col = get_existing_column(connection, table_name, ['nama_bahan', 'product_name', 'name'])
|
||||||
stock_col = get_existing_column(connection, table_name, ['stok', 'current_stock', 'stock'])
|
stock_col = get_existing_column(connection, table_name, ['stok', 'current_stock', 'stock'])
|
||||||
min_col = get_existing_column(connection, table_name, ['stok_minimum', 'min_stock', 'minimum_stock'])
|
min_col = get_existing_column(connection, table_name, ['stok_minimum', 'min_stock', 'minimum_stock'])
|
||||||
|
|
@ -1109,6 +1195,10 @@ def get_products():
|
||||||
if not connection:
|
if not connection:
|
||||||
return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500
|
return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500
|
||||||
|
|
||||||
|
ensure_product_stock_precision(connection)
|
||||||
|
ensure_product_min_stock_column(connection)
|
||||||
|
backfill_default_min_stock(connection)
|
||||||
|
|
||||||
cursor = connection.cursor(dictionary=True)
|
cursor = connection.cursor(dictionary=True)
|
||||||
cursor.execute("SELECT * FROM products ORDER BY name")
|
cursor.execute("SELECT * FROM products ORDER BY name")
|
||||||
products = cursor.fetchall()
|
products = cursor.fetchall()
|
||||||
|
|
@ -1184,6 +1274,9 @@ def create_product():
|
||||||
if not connection:
|
if not connection:
|
||||||
return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500
|
return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500
|
||||||
|
|
||||||
|
ensure_product_stock_precision(connection)
|
||||||
|
ensure_product_min_stock_column(connection)
|
||||||
|
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
|
||||||
# Optional columns compatibility (works for old/new schemas)
|
# Optional columns compatibility (works for old/new schemas)
|
||||||
|
|
@ -1191,9 +1284,12 @@ def create_product():
|
||||||
has_unit_column = cursor.fetchone() is not None
|
has_unit_column = cursor.fetchone() is not None
|
||||||
cursor.execute("SHOW COLUMNS FROM products LIKE 'product_type'")
|
cursor.execute("SHOW COLUMNS FROM products LIKE 'product_type'")
|
||||||
has_product_type_column = cursor.fetchone() is not None
|
has_product_type_column = cursor.fetchone() is not None
|
||||||
|
cursor.execute("SHOW COLUMNS FROM products LIKE 'min_stock'")
|
||||||
|
has_min_stock_column = cursor.fetchone() is not None
|
||||||
|
|
||||||
unit_value = data.get('unit')
|
unit_value = data.get('unit')
|
||||||
product_type_value = data.get('product_type')
|
product_type_value = data.get('product_type')
|
||||||
|
min_stock_value = data.get('min_stock', 0)
|
||||||
|
|
||||||
# Check for duplicate product name
|
# Check for duplicate product name
|
||||||
cursor.execute("SELECT id FROM products WHERE name = %s", (data['name'],))
|
cursor.execute("SELECT id FROM products WHERE name = %s", (data['name'],))
|
||||||
|
|
@ -1205,55 +1301,24 @@ def create_product():
|
||||||
'message': f'Product "{data["name"]}" already exists'
|
'message': f'Product "{data["name"]}" already exists'
|
||||||
}), 409
|
}), 409
|
||||||
|
|
||||||
# Insert new product
|
columns = ['name', 'category', 'price', 'current_stock']
|
||||||
if has_unit_column and has_product_type_column:
|
values = [data['name'], data['category'], data['price'], data['current_stock']]
|
||||||
cursor.execute("""
|
if has_unit_column:
|
||||||
INSERT INTO products
|
columns.append('unit')
|
||||||
(name, category, price, current_stock, unit, product_type)
|
values.append(unit_value)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s)
|
if has_product_type_column:
|
||||||
""", (
|
columns.append('product_type')
|
||||||
data['name'],
|
values.append(product_type_value)
|
||||||
data['category'],
|
if has_min_stock_column:
|
||||||
data['price'],
|
columns.append('min_stock')
|
||||||
data['current_stock'],
|
values.append(min_stock_value)
|
||||||
unit_value,
|
|
||||||
product_type_value
|
column_sql = ', '.join(columns)
|
||||||
))
|
placeholders = ', '.join(['%s'] * len(columns))
|
||||||
elif has_unit_column:
|
cursor.execute(
|
||||||
cursor.execute("""
|
f"INSERT INTO products ({column_sql}) VALUES ({placeholders})",
|
||||||
INSERT INTO products
|
tuple(values)
|
||||||
(name, category, price, current_stock, unit)
|
)
|
||||||
VALUES (%s, %s, %s, %s, %s)
|
|
||||||
""", (
|
|
||||||
data['name'],
|
|
||||||
data['category'],
|
|
||||||
data['price'],
|
|
||||||
data['current_stock'],
|
|
||||||
unit_value
|
|
||||||
))
|
|
||||||
elif has_product_type_column:
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO products
|
|
||||||
(name, category, price, current_stock, product_type)
|
|
||||||
VALUES (%s, %s, %s, %s, %s)
|
|
||||||
""", (
|
|
||||||
data['name'],
|
|
||||||
data['category'],
|
|
||||||
data['price'],
|
|
||||||
data['current_stock'],
|
|
||||||
product_type_value
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO products
|
|
||||||
(name, category, price, current_stock)
|
|
||||||
VALUES (%s, %s, %s, %s)
|
|
||||||
""", (
|
|
||||||
data['name'],
|
|
||||||
data['category'],
|
|
||||||
data['price'],
|
|
||||||
data['current_stock']
|
|
||||||
))
|
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
product_id = cursor.lastrowid
|
product_id = cursor.lastrowid
|
||||||
|
|
@ -1305,6 +1370,8 @@ def consume_stock():
|
||||||
if not connection:
|
if not connection:
|
||||||
return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500
|
return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500
|
||||||
|
|
||||||
|
ensure_product_stock_precision(connection)
|
||||||
|
|
||||||
ensure_stock_usage_tables(connection)
|
ensure_stock_usage_tables(connection)
|
||||||
|
|
||||||
name_column = get_product_name_column(connection)
|
name_column = get_product_name_column(connection)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ CREATE TABLE IF NOT EXISTS products (
|
||||||
product_type VARCHAR(20) DEFAULT 'Bahan',
|
product_type VARCHAR(20) DEFAULT 'Bahan',
|
||||||
unit VARCHAR(20) DEFAULT 'kg',
|
unit VARCHAR(20) DEFAULT 'kg',
|
||||||
price INT NOT NULL,
|
price INT NOT NULL,
|
||||||
current_stock INT NOT NULL DEFAULT 0,
|
current_stock DECIMAL(10,3) NOT NULL DEFAULT 0,
|
||||||
|
min_stock DECIMAL(10,3) NOT NULL DEFAULT 0,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
@ -100,15 +101,15 @@ CREATE TABLE IF NOT EXISTS recipe_ingredients (
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- Insert Initial Products (8 items)
|
-- Insert Initial Products (8 items)
|
||||||
INSERT INTO products (name, category, product_type, unit, price, current_stock) VALUES
|
INSERT INTO products (name, category, product_type, unit, price, current_stock, min_stock) VALUES
|
||||||
('Tepung Terigu 1kg', 'Tepung', 'Bahan', 'kg', 15000, 50),
|
('Tepung Terigu 1kg', 'Tepung', 'Bahan', 'kg', 15000, 50, 10),
|
||||||
('Telur 1kg', 'Telur', 'Bahan', 'kg', 25000, 30),
|
('Telur 1kg', 'Telur', 'Bahan', 'kg', 25000, 30, 5),
|
||||||
('Gula Pasir 1kg', 'Gula', 'Bahan', 'kg', 12000, 40),
|
('Gula Pasir 1kg', 'Gula', 'Bahan', 'kg', 12000, 40, 8),
|
||||||
('Susu Bubuk', 'Susu', 'Bahan', 'kg', 20000, 20),
|
('Susu Bubuk', 'Susu', 'Bahan', 'kg', 20000, 20, 4),
|
||||||
('Cokelat Bubuk 250gr', 'Cokelat', 'Bahan', 'kg', 18000, 15),
|
('Cokelat Bubuk 250gr', 'Cokelat', 'Bahan', 'kg', 18000, 15, 3),
|
||||||
('Mentega 500gr', 'Mentega', 'Bahan', 'kg', 22000, 25),
|
('Mentega 500gr', 'Mentega', 'Bahan', 'kg', 22000, 25, 5),
|
||||||
('Keju Parut 250gr', 'Keju', 'Bahan', 'kg', 28000, 10),
|
('Keju Parut 250gr', 'Keju', 'Bahan', 'kg', 28000, 10, 2),
|
||||||
('Baking Powder', 'Bahan Tambahan', 'Bahan', 'kg', 8000, 35);
|
('Baking Powder', 'Bahan Tambahan', 'Bahan', 'kg', 8000, 35, 2);
|
||||||
|
|
||||||
-- Insert Default Login Account
|
-- Insert Default Login Account
|
||||||
INSERT INTO login (name, email, username, password) VALUES
|
INSERT INTO login (name, email, username, password) VALUES
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue