fix final1

This commit is contained in:
rhanarmt 2026-05-24 21:52:52 +07:00
parent cc37039155
commit 437f973963
14 changed files with 307 additions and 96 deletions

View File

@ -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)

View File

@ -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<String, dynamic> 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,
);

View File

@ -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() ?? '';

View File

@ -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,

View File

@ -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<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) {
@ -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);
}

View File

@ -360,7 +360,7 @@ class _ProductListScreenState extends State<ProductListScreen> 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<ProductListScreen> 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<ProductListScreen> 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<ProductListScreen> 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,

View File

@ -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() ?? '';

View File

@ -371,11 +371,14 @@ class _ReportScreenState extends State<ReportScreen> {
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<ReportScreen> {
...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<ReportScreen> {
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<ReportScreen> {
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));

View File

@ -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,
);

View File

@ -92,6 +92,7 @@ class _TransactionScreenState extends State<TransactionScreen> {
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<TransactionScreen> {
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<TransactionScreen> {
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<TransactionScreen> {
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<TransactionScreen> {
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<TransactionScreen> {
),
);
} else {
ScaffoldMessenger.of(
context,
).showSnackBar(
messenger.showSnackBar(
SnackBar(
content: Text(
result['message'] ??
@ -724,7 +775,7 @@ class _TransactionScreenState extends State<TransactionScreen> {
height: 32,
decoration: BoxDecoration(
color: AppColors.statusError
.withOpacity(0.1),
.withValues(alpha: 0.1),
borderRadius:
BorderRadius.circular(6),
),

View File

@ -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,
};

View File

@ -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;

View File

@ -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)

View File

@ -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