From 437f97396354c991cbfaf3284311bd482fbef5bd Mon Sep 17 00:00:00 2001 From: rhanarmt Date: Sun, 24 May 2026 21:52:52 +0700 Subject: [PATCH] fix final1 --- database/recipes_schema.sql | 6 +- lib/models/product_model.dart | 20 ++- .../dashboard/dashboard_controller.dart | 6 +- .../prediction/prediction_controller.dart | 20 ++- .../products/product_list_controller.dart | 27 ++- lib/screens/products/product_list_page.dart | 15 +- lib/screens/reports/report_controller.dart | 6 +- lib/screens/reports/report_page.dart | 33 +++- .../transaction/transaction_controller.dart | 2 + lib/screens/transaction/transaction_page.dart | 69 +++++++- lib/services/ml_service.dart | 4 +- lib/utils/stock_status.dart | 9 +- ml_model/app.py | 165 ++++++++++++------ ml_model/setup_database.sql | 21 +-- 14 files changed, 307 insertions(+), 96 deletions(-) diff --git a/database/recipes_schema.sql b/database/recipes_schema.sql index 9106693..7b6509b 100644 --- a/database/recipes_schema.sql +++ b/database/recipes_schema.sql @@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS recipe_ingredients ( id INT PRIMARY KEY AUTO_INCREMENT, recipe_id INT NOT NULL, product_name VARCHAR(100) NOT NULL, - quantity_needed INT NOT NULL, + quantity_needed FLOAT NOT NULL, unit VARCHAR(20) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 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, category VARCHAR(50) 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, - min_stock INT DEFAULT 0, + min_stock DECIMAL(10,3) NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, KEY idx_product_name (product_name), KEY idx_category (category) diff --git a/lib/models/product_model.dart b/lib/models/product_model.dart index 55433f5..b538f72 100644 --- a/lib/models/product_model.dart +++ b/lib/models/product_model.dart @@ -3,7 +3,8 @@ class Product { final String name; final String category; final int price; - final int stock; + final double stock; + final double minStock; final String unit; final String status; // 'tersedia', 'sedang', 'kritis' @@ -13,17 +14,27 @@ class Product { required this.category, required this.price, required this.stock, + this.minStock = 0, this.unit = 'kg', this.status = 'tersedia', }); factory Product.fromJson(Map json) { + final stockRaw = json['stock'] ?? json['current_stock'] ?? 0; + final minStockRaw = json['min_stock'] ?? json['stok_minimum'] ?? 0; return Product( id: json['id'] as int, name: json['name'] as String, category: json['category'] as String, 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', status: json['status'] as String? ?? 'tersedia', ); @@ -36,6 +47,7 @@ class Product { 'category': category, 'price': price, 'stock': stock, + 'min_stock': minStock, 'unit': unit, 'status': status, }; @@ -46,7 +58,8 @@ class Product { String? name, String? category, int? price, - int? stock, + double? stock, + double? minStock, String? unit, String? status, }) { @@ -56,6 +69,7 @@ class Product { category: category ?? this.category, price: price ?? this.price, stock: stock ?? this.stock, + minStock: minStock ?? this.minStock, unit: unit ?? this.unit, status: status ?? this.status, ); diff --git a/lib/screens/dashboard/dashboard_controller.dart b/lib/screens/dashboard/dashboard_controller.dart index 4edb137..b1db091 100644 --- a/lib/screens/dashboard/dashboard_controller.dart +++ b/lib/screens/dashboard/dashboard_controller.dart @@ -109,7 +109,11 @@ class DashboardController extends ChangeNotifier { if (item is! Map) continue; 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; final category = item['category']?.toString().toLowerCase() ?? ''; diff --git a/lib/screens/prediction/prediction_controller.dart b/lib/screens/prediction/prediction_controller.dart index d6d560b..6d3d2ff 100644 --- a/lib/screens/prediction/prediction_controller.dart +++ b/lib/screens/prediction/prediction_controller.dart @@ -88,13 +88,12 @@ class PredictionController extends ChangeNotifier { (product['name'] ?? product['product_name'] ?? '').toString(); if (name.isEmpty) continue; - final id = product['id'] ?? 0; + final id = _toInt(product['id']); final unit = (product['unit'] ?? '').toString(); - final stockRaw = product['current_stock'] ?? 0; - final stock = stockRaw is num ? stockRaw.toDouble() : 0.0; + final stock = _toDouble(product['current_stock'] ?? product['stock']); currentStock[name] = stock; - if (id is int) { + if (id != null) { productIds[name] = id; } if (unit.isNotEmpty) { @@ -162,7 +161,7 @@ class PredictionController extends ChangeNotifier { ingredients.forEach((productName, details) { if (!isIngredientSelected(productName)) return; - final quantity = (details['quantity'] as num).toDouble(); + final quantity = _toDouble(details['quantity']); required[productName] = quantity * productionQuantity; }); @@ -294,6 +293,17 @@ class PredictionController extends ChangeNotifier { 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, diff --git a/lib/screens/products/product_list_controller.dart b/lib/screens/products/product_list_controller.dart index d69d1c1..582b7d1 100644 --- a/lib/screens/products/product_list_controller.dart +++ b/lib/screens/products/product_list_controller.dart @@ -19,7 +19,8 @@ class ProductListController extends ChangeNotifier { products = 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 unit = p['unit'] ?? @@ -30,8 +31,12 @@ class ProductListController extends ChangeNotifier { category: category, price: p['price'] ?? 0, stock: stock, + minStock: minStock, unit: unit, - status: StockStatusUtils.statusFromStock(stock), + status: StockStatusUtils.statusFromStock( + stock, + minStock: minStock, + ), ); }).toList(); } finally { @@ -74,8 +79,16 @@ class ProductListController extends ChangeNotifier { } int get maxStock { + return maxStockValue.ceil().clamp(1, double.infinity).toInt(); + } + + double get maxStockValue { if (products.isEmpty) return 1; - return products.fold(0, (max, p) => p.stock > max ? p.stock : max); + final maxValue = products.fold( + 0, + (max, p) => p.stock > max ? p.stock : max, + ); + return maxValue <= 0 ? 1 : maxValue; } Color getStatusColor(String status) { @@ -107,6 +120,14 @@ class ProductListController extends ChangeNotifier { 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) { return StockStatusUtils.label(status, withIcon: true); } diff --git a/lib/screens/products/product_list_page.dart b/lib/screens/products/product_list_page.dart index 4b089f7..2be1345 100644 --- a/lib/screens/products/product_list_page.dart +++ b/lib/screens/products/product_list_page.dart @@ -360,7 +360,7 @@ class _ProductListScreenState extends State with RouteAware { } Widget _buildProductCard(Product product) { - final maxStock = _controller.maxStock; + final maxStock = _controller.maxStockValue; final stockPercentage = (product.stock / maxStock * 100).toInt(); return InkWell( @@ -488,7 +488,7 @@ class _ProductListScreenState extends State with RouteAware { ), const SizedBox(height: 4), Text( - '${product.stock} ${product.unit}', + '${_controller.formatStock(product.stock)} ${product.unit}', style: AppTextStyles.labelLarge.copyWith( color: AppColors.textPrimary, fontWeight: FontWeight.w700, @@ -560,7 +560,7 @@ class _ProductListScreenState extends State with RouteAware { } void _showProductDetail(Product product) { - final maxStock = _controller.maxStock; + final maxStock = _controller.maxStockValue; final stockPercentage = maxStock == 0 ? 0 : (product.stock / maxStock * 100).round(); final statusColor = _controller.getStatusColor(product.status); @@ -672,7 +672,14 @@ class _ProductListScreenState extends State with RouteAware { _buildDetailTile( icon: Icons.inventory_2_outlined, 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( icon: Icons.straighten_outlined, diff --git a/lib/screens/reports/report_controller.dart b/lib/screens/reports/report_controller.dart index b14adc0..f544bb3 100644 --- a/lib/screens/reports/report_controller.dart +++ b/lib/screens/reports/report_controller.dart @@ -176,7 +176,11 @@ class ReportController extends ChangeNotifier { if (item is! Map) continue; 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; final category = item['category']?.toString().toLowerCase() ?? ''; diff --git a/lib/screens/reports/report_page.dart b/lib/screens/reports/report_page.dart index 213cd8a..89ad1f8 100644 --- a/lib/screens/reports/report_page.dart +++ b/lib/screens/reports/report_page.dart @@ -371,11 +371,14 @@ class _ReportScreenState extends State { itemBuilder: (context, index) { final item = sortedItems[index]; 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 statusLabel = StockStatusUtils.label(statusKey); final unit = item['unit']?.toString() ?? 'kg'; - final minimumStock = StockStatusUtils.parseStock(item['min_stock']); return _buildReportItemCard( icon: Icons.inventory_2_outlined, @@ -1468,7 +1471,13 @@ class _ReportScreenState extends State { ...stockItems.asMap().entries.map((entry) { final item = entry.value; 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 [ '${entry.key + 1}', item['name']?.toString() ?? '-', @@ -1620,7 +1629,11 @@ class _ReportScreenState extends State { for (var index = 0; index < stockItems.length; index++) { final item = stockItems[index]; 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); buffer.writeln( '${index + 1},${_escapeCsv(item['name'])},' @@ -1709,8 +1722,16 @@ class _ReportScreenState extends State { return items.toList()..sort((a, b) { final aStock = StockStatusUtils.parseStock(a['stock']); final bStock = StockStatusUtils.parseStock(b['stock']); - final aStatus = StockStatusUtils.statusFromStock(aStock); - final bStatus = StockStatusUtils.statusFromStock(bStock); + final aMinStock = StockStatusUtils.parseStock(a['min_stock']); + final bMinStock = StockStatusUtils.parseStock(b['min_stock']); + final aStatus = StockStatusUtils.statusFromStock( + aStock, + minStock: aMinStock, + ); + final bStatus = StockStatusUtils.statusFromStock( + bStock, + minStock: bMinStock, + ); final statusComparison = _statusOrder( aStatus, ).compareTo(_statusOrder(bStatus)); diff --git a/lib/screens/transaction/transaction_controller.dart b/lib/screens/transaction/transaction_controller.dart index e40bea3..ecc9ac5 100644 --- a/lib/screens/transaction/transaction_controller.dart +++ b/lib/screens/transaction/transaction_controller.dart @@ -209,6 +209,7 @@ class TransactionController extends ChangeNotifier { required String category, required int price, required int initialStock, + required double minStock, required String unit, required String productType, }) async { @@ -221,6 +222,7 @@ class TransactionController extends ChangeNotifier { category: category, price: price, currentStock: initialStock, + minStock: minStock, unit: unit, productType: productType, ); diff --git a/lib/screens/transaction/transaction_page.dart b/lib/screens/transaction/transaction_page.dart index 91f2191..a032d57 100644 --- a/lib/screens/transaction/transaction_page.dart +++ b/lib/screens/transaction/transaction_page.dart @@ -92,6 +92,7 @@ class _TransactionScreenState extends State { final newProductNameController = TextEditingController(); final newPriceController = TextEditingController(); final newStockController = TextEditingController(); + final newMinStockController = TextEditingController(); const unitOptionsByType = { 'Bahan': ['kg'], 'Barang': ['pcs'], @@ -268,6 +269,30 @@ class _TransactionScreenState extends State { 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), Row( children: [ @@ -308,11 +333,16 @@ class _TransactionScreenState extends State { newPriceController.text.trim(); final stockText = newStockController.text.trim(); + final minStockText = + newMinStockController.text + .trim() + .replaceAll(',', '.'); if (productName.isEmpty || category.isEmpty || priceText.isEmpty || - stockText.isEmpty) { + stockText.isEmpty || + minStockText.isEmpty) { ScaffoldMessenger.of( context, ).showSnackBar( @@ -361,12 +391,37 @@ class _TransactionScreenState extends State { 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 .createProduct( productName: productName, category: category, price: priceInt, initialStock: stockInt, + minStock: minStock, unit: selectedUnit, productType: selectedProductType, @@ -375,10 +430,8 @@ class _TransactionScreenState extends State { if (!mounted) return; if (result['status'] == 'success') { - Navigator.pop(context); - ScaffoldMessenger.of( - context, - ).showSnackBar( + navigator.pop(); + messenger.showSnackBar( SnackBar( content: Text( 'Produk "$productName" berhasil ditambahkan', @@ -388,9 +441,7 @@ class _TransactionScreenState extends State { ), ); } else { - ScaffoldMessenger.of( - context, - ).showSnackBar( + messenger.showSnackBar( SnackBar( content: Text( result['message'] ?? @@ -724,7 +775,7 @@ class _TransactionScreenState extends State { height: 32, decoration: BoxDecoration( color: AppColors.statusError - .withOpacity(0.1), + .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), diff --git a/lib/services/ml_service.dart b/lib/services/ml_service.dart index 2940831..d83598d 100644 --- a/lib/services/ml_service.dart +++ b/lib/services/ml_service.dart @@ -6,7 +6,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.1.16:5000'; + static const String baseUrl = 'http://192.168.1.2:5000'; static const int timeoutSeconds = 30; @@ -501,6 +501,7 @@ class MLService { required String category, required int price, required int currentStock, + double? minStock, String? unit, String? productType, }) async { @@ -510,6 +511,7 @@ class MLService { 'category': category, 'price': price, 'current_stock': currentStock, + if (minStock != null) 'min_stock': minStock, if (unit != null) 'unit': unit, if (productType != null) 'product_type': productType, }; diff --git a/lib/utils/stock_status.dart b/lib/utils/stock_status.dart index d349d90..eec7e8a 100644 --- a/lib/utils/stock_status.dart +++ b/lib/utils/stock_status.dart @@ -16,8 +16,15 @@ class StockStatusUtils { return double.tryParse(value?.toString() ?? '') ?? 0.0; } - static String statusFromStock(num stockKg) { + static String statusFromStock(num stockKg, {num? minStock}) { 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 < warningStockLimitKg) return statusSedang; return statusTersedia; diff --git a/ml_model/app.py b/ml_model/app.py index 26ba6c4..2027202 100644 --- a/ml_model/app.py +++ b/ml_model/app.py @@ -108,6 +108,87 @@ def has_product_unit_column(connection) -> bool: cursor.close() 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: return f"`{table_name}`" @@ -253,6 +334,11 @@ def fetch_stock_report(connection): if not table_name: 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']) 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']) @@ -1109,6 +1195,10 @@ def get_products(): if not connection: 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.execute("SELECT * FROM products ORDER BY name") products = cursor.fetchall() @@ -1184,6 +1274,9 @@ def create_product(): if not connection: return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500 + ensure_product_stock_precision(connection) + ensure_product_min_stock_column(connection) + cursor = connection.cursor() # Optional columns compatibility (works for old/new schemas) @@ -1191,9 +1284,12 @@ def create_product(): has_unit_column = cursor.fetchone() is not None cursor.execute("SHOW COLUMNS FROM products LIKE 'product_type'") 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') product_type_value = data.get('product_type') + min_stock_value = data.get('min_stock', 0) # Check for duplicate product 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' }), 409 - # Insert new product - if has_unit_column and has_product_type_column: - cursor.execute(""" - INSERT INTO products - (name, category, price, current_stock, unit, product_type) - VALUES (%s, %s, %s, %s, %s, %s) - """, ( - data['name'], - data['category'], - data['price'], - data['current_stock'], - unit_value, - product_type_value - )) - elif has_unit_column: - cursor.execute(""" - INSERT INTO products - (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'] - )) + columns = ['name', 'category', 'price', 'current_stock'] + values = [data['name'], data['category'], data['price'], data['current_stock']] + if has_unit_column: + columns.append('unit') + values.append(unit_value) + if has_product_type_column: + columns.append('product_type') + values.append(product_type_value) + if has_min_stock_column: + columns.append('min_stock') + values.append(min_stock_value) + + column_sql = ', '.join(columns) + placeholders = ', '.join(['%s'] * len(columns)) + cursor.execute( + f"INSERT INTO products ({column_sql}) VALUES ({placeholders})", + tuple(values) + ) connection.commit() product_id = cursor.lastrowid @@ -1305,6 +1370,8 @@ def consume_stock(): if not connection: return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500 + ensure_product_stock_precision(connection) + ensure_stock_usage_tables(connection) name_column = get_product_name_column(connection) diff --git a/ml_model/setup_database.sql b/ml_model/setup_database.sql index 615a933..a06ed01 100644 --- a/ml_model/setup_database.sql +++ b/ml_model/setup_database.sql @@ -13,7 +13,8 @@ CREATE TABLE IF NOT EXISTS products ( product_type VARCHAR(20) DEFAULT 'Bahan', unit VARCHAR(20) DEFAULT 'kg', 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, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -100,15 +101,15 @@ CREATE TABLE IF NOT EXISTS recipe_ingredients ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Insert Initial Products (8 items) -INSERT INTO products (name, category, product_type, unit, price, current_stock) VALUES -('Tepung Terigu 1kg', 'Tepung', 'Bahan', 'kg', 15000, 50), -('Telur 1kg', 'Telur', 'Bahan', 'kg', 25000, 30), -('Gula Pasir 1kg', 'Gula', 'Bahan', 'kg', 12000, 40), -('Susu Bubuk', 'Susu', 'Bahan', 'kg', 20000, 20), -('Cokelat Bubuk 250gr', 'Cokelat', 'Bahan', 'kg', 18000, 15), -('Mentega 500gr', 'Mentega', 'Bahan', 'kg', 22000, 25), -('Keju Parut 250gr', 'Keju', 'Bahan', 'kg', 28000, 10), -('Baking Powder', 'Bahan Tambahan', 'Bahan', 'kg', 8000, 35); +INSERT INTO products (name, category, product_type, unit, price, current_stock, min_stock) VALUES +('Tepung Terigu 1kg', 'Tepung', 'Bahan', 'kg', 15000, 50, 10), +('Telur 1kg', 'Telur', 'Bahan', 'kg', 25000, 30, 5), +('Gula Pasir 1kg', 'Gula', 'Bahan', 'kg', 12000, 40, 8), +('Susu Bubuk', 'Susu', 'Bahan', 'kg', 20000, 20, 4), +('Cokelat Bubuk 250gr', 'Cokelat', 'Bahan', 'kg', 18000, 15, 3), +('Mentega 500gr', 'Mentega', 'Bahan', 'kg', 22000, 25, 5), +('Keju Parut 250gr', 'Keju', 'Bahan', 'kg', 28000, 10, 2), +('Baking Powder', 'Bahan Tambahan', 'Bahan', 'kg', 8000, 35, 2); -- Insert Default Login Account INSERT INTO login (name, email, username, password) VALUES