API Fitur transaksi dan Produk
This commit is contained in:
parent
f1fcbddfeb
commit
95a703dd4e
|
|
@ -5,12 +5,14 @@ import 'package:finalproject/models/transaction_model.dart';
|
||||||
import 'package:finalproject/services/ml_service.dart';
|
import 'package:finalproject/services/ml_service.dart';
|
||||||
|
|
||||||
class CartItem {
|
class CartItem {
|
||||||
|
final int productId;
|
||||||
final String productName;
|
final String productName;
|
||||||
final String category;
|
final String category;
|
||||||
final int unitPrice;
|
final int unitPrice;
|
||||||
int quantity;
|
int quantity;
|
||||||
|
|
||||||
CartItem({
|
CartItem({
|
||||||
|
required this.productId,
|
||||||
required this.productName,
|
required this.productName,
|
||||||
required this.category,
|
required this.category,
|
||||||
required this.unitPrice,
|
required this.unitPrice,
|
||||||
|
|
@ -35,6 +37,7 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
|
|
||||||
// Products list (mutable)
|
// Products list (mutable)
|
||||||
late List<String> products;
|
late List<String> products;
|
||||||
|
late Map<String, int> productIds;
|
||||||
late Map<String, String> productCategories;
|
late Map<String, String> productCategories;
|
||||||
late Map<String, int> productPrices;
|
late Map<String, int> productPrices;
|
||||||
late List<String> categories;
|
late List<String> categories;
|
||||||
|
|
@ -57,18 +60,21 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
products = [];
|
products = [];
|
||||||
|
productIds = {};
|
||||||
productCategories = {};
|
productCategories = {};
|
||||||
productPrices = {};
|
productPrices = {};
|
||||||
categories = [];
|
categories = [];
|
||||||
|
|
||||||
// Convert API response to local maps
|
// Convert API response to local maps
|
||||||
for (var product in fetchedProducts) {
|
for (var product in fetchedProducts) {
|
||||||
|
int id = product['id'] ?? 0;
|
||||||
String name = product['name'] ?? '';
|
String name = product['name'] ?? '';
|
||||||
String category = product['category'] ?? '';
|
String category = product['category'] ?? '';
|
||||||
int price = product['price'] ?? 0;
|
int price = product['price'] ?? 0;
|
||||||
|
|
||||||
if (name.isNotEmpty) {
|
if (name.isNotEmpty && id > 0) {
|
||||||
products.add(name);
|
products.add(name);
|
||||||
|
productIds[name] = id;
|
||||||
productCategories[name] = category;
|
productCategories[name] = category;
|
||||||
productPrices[name] = price;
|
productPrices[name] = price;
|
||||||
|
|
||||||
|
|
@ -117,8 +123,9 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final existingIndex =
|
final existingIndex = cartItems.indexWhere(
|
||||||
cartItems.indexWhere((item) => item.productName == _selectedProduct);
|
(item) => item.productName == _selectedProduct,
|
||||||
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
|
|
@ -128,6 +135,7 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
// Add new item to cart
|
// Add new item to cart
|
||||||
cartItems.add(
|
cartItems.add(
|
||||||
CartItem(
|
CartItem(
|
||||||
|
productId: productIds[_selectedProduct!] ?? 0,
|
||||||
productName: _selectedProduct!,
|
productName: _selectedProduct!,
|
||||||
category: productCategories[_selectedProduct!]!,
|
category: productCategories[_selectedProduct!]!,
|
||||||
unitPrice: productPrices[_selectedProduct!]!,
|
unitPrice: productPrices[_selectedProduct!]!,
|
||||||
|
|
@ -183,16 +191,21 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
final dateStr =
|
final dateStr =
|
||||||
'${_selectedDate!.year}-${_selectedDate!.month.toString().padLeft(2, '0')}-${_selectedDate!.day.toString().padLeft(2, '0')}';
|
'${_selectedDate!.year}-${_selectedDate!.month.toString().padLeft(2, '0')}-${_selectedDate!.day.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
int successCount = 0;
|
||||||
|
String? firstError;
|
||||||
|
|
||||||
for (final item in cartItems) {
|
for (final item in cartItems) {
|
||||||
await MLService.saveTransaction(
|
// Use new endpoint that updates stock automatically
|
||||||
productName: item.productName,
|
final result = await MLService.addTransactionWithStockUpdate(
|
||||||
category: item.category,
|
productId: item.productId,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
unitPrice: item.unitPrice,
|
unitPrice: item.unitPrice,
|
||||||
totalPrice: item.totalPrice,
|
totalPrice: item.totalPrice,
|
||||||
transactionDate: dateStr,
|
transactionDate: dateStr,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (result['status'] == 'success') {
|
||||||
|
successCount++;
|
||||||
// Add to transaction history
|
// Add to transaction history
|
||||||
final transaction = Transaction(
|
final transaction = Transaction(
|
||||||
id: transactions.length + 1,
|
id: transactions.length + 1,
|
||||||
|
|
@ -203,8 +216,11 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
totalPrice: item.totalPrice,
|
totalPrice: item.totalPrice,
|
||||||
date: _selectedDate ?? DateTime.now(),
|
date: _selectedDate ?? DateTime.now(),
|
||||||
);
|
);
|
||||||
|
|
||||||
transactions.insert(0, transaction);
|
transactions.insert(0, transaction);
|
||||||
|
} else {
|
||||||
|
firstError ??= result['message'] ?? 'Transaksi gagal';
|
||||||
|
print('Transaction failed for ${item.productName}: ${result['message']}');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -213,13 +229,23 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
_selectedDate = DateTime.now();
|
_selectedDate = DateTime.now();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (firstError != null && successCount < cartItems.length) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content:
|
content: Text(
|
||||||
Text('✅ ${transactions.length} transaksi berhasil disimpan!'),
|
'⚠️ $successCount/${cartItems.length} transaksi berhasil. Error: $firstError',
|
||||||
|
),
|
||||||
|
backgroundColor: AppColors.statusError,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('✅ $successCount transaksi berhasil disimpan dan stok terupdate!'),
|
||||||
backgroundColor: AppColors.statusSuccess,
|
backgroundColor: AppColors.statusSuccess,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|
@ -241,9 +267,13 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => StatefulBuilder(
|
builder:
|
||||||
builder: (context, setStateDialog) => Dialog(
|
(context) => StatefulBuilder(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
builder:
|
||||||
|
(context, setStateDialog) => Dialog(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.bgWhite,
|
color: AppColors.bgWhite,
|
||||||
|
|
@ -310,24 +340,31 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: _selectedCategory.isNotEmpty
|
value:
|
||||||
|
_selectedCategory.isNotEmpty
|
||||||
? _selectedCategory
|
? _selectedCategory
|
||||||
: null,
|
: null,
|
||||||
items: categories
|
items:
|
||||||
.map((cat) => DropdownMenuItem(
|
categories
|
||||||
|
.map(
|
||||||
|
(cat) => DropdownMenuItem(
|
||||||
value: cat,
|
value: cat,
|
||||||
child: Text(cat),
|
child: Text(cat),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setStateDialog(
|
setStateDialog(
|
||||||
() => _selectedCategory = value ?? '',
|
() =>
|
||||||
|
_selectedCategory = value ?? '',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Pilih kategori',
|
hintText: 'Pilih kategori',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(
|
||||||
|
10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.bgLight,
|
fillColor: AppColors.bgLight,
|
||||||
|
|
@ -337,11 +374,13 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setStateDialog(
|
setStateDialog(
|
||||||
() => _createNewCategory = true);
|
() => _createNewCategory = true,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'+ Tambah kategori baru',
|
'+ Tambah kategori baru',
|
||||||
style: AppTextStyles.labelMedium.copyWith(
|
style: AppTextStyles.labelMedium
|
||||||
|
.copyWith(
|
||||||
color: AppColors.secondaryBlue,
|
color: AppColors.secondaryBlue,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
|
|
@ -355,9 +394,12 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: newCategoryController,
|
controller: newCategoryController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Masukkan nama kategori baru',
|
hintText:
|
||||||
|
'Masukkan nama kategori baru',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(
|
||||||
|
10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.bgLight,
|
fillColor: AppColors.bgLight,
|
||||||
|
|
@ -367,12 +409,14 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setStateDialog(
|
setStateDialog(
|
||||||
() => _createNewCategory = false);
|
() => _createNewCategory = false,
|
||||||
|
);
|
||||||
newCategoryController.clear();
|
newCategoryController.clear();
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'← Kembali ke kategori existing',
|
'← Kembali ke kategori existing',
|
||||||
style: AppTextStyles.labelMedium.copyWith(
|
style: AppTextStyles.labelMedium
|
||||||
|
.copyWith(
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.textSecondary,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
|
|
@ -411,18 +455,22 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(
|
||||||
const EdgeInsets.symmetric(vertical: 12),
|
vertical: 12,
|
||||||
|
),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.textSecondary,
|
||||||
),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(
|
||||||
|
10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Batal',
|
'Batal',
|
||||||
style: AppTextStyles.labelLarge.copyWith(
|
style: AppTextStyles.labelLarge
|
||||||
|
.copyWith(
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.textSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -433,20 +481,26 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final productName =
|
final productName =
|
||||||
newProductNameController.text.trim();
|
newProductNameController.text
|
||||||
final category = _createNewCategory
|
.trim();
|
||||||
? newCategoryController.text.trim()
|
final category =
|
||||||
|
_createNewCategory
|
||||||
|
? newCategoryController.text
|
||||||
|
.trim()
|
||||||
: _selectedCategory;
|
: _selectedCategory;
|
||||||
final price = newPriceController.text.trim();
|
final price =
|
||||||
|
newPriceController.text.trim();
|
||||||
|
|
||||||
if (productName.isEmpty ||
|
if (productName.isEmpty ||
|
||||||
category.isEmpty ||
|
category.isEmpty ||
|
||||||
price.isEmpty) {
|
price.isEmpty) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(
|
||||||
.showSnackBar(
|
context,
|
||||||
|
).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: const Text(
|
content: const Text(
|
||||||
'Semua field harus diisi'),
|
'Semua field harus diisi',
|
||||||
|
),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
AppColors.statusError,
|
AppColors.statusError,
|
||||||
),
|
),
|
||||||
|
|
@ -457,11 +511,13 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
final priceInt =
|
final priceInt =
|
||||||
int.tryParse(price) ?? 0;
|
int.tryParse(price) ?? 0;
|
||||||
if (priceInt <= 0) {
|
if (priceInt <= 0) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(
|
||||||
.showSnackBar(
|
context,
|
||||||
|
).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: const Text(
|
content: const Text(
|
||||||
'Harga harus lebih dari 0'),
|
'Harga harus lebih dari 0',
|
||||||
|
),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
AppColors.statusError,
|
AppColors.statusError,
|
||||||
),
|
),
|
||||||
|
|
@ -471,11 +527,13 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
|
|
||||||
// Frontend validation: check duplicate
|
// Frontend validation: check duplicate
|
||||||
if (products.contains(productName)) {
|
if (products.contains(productName)) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(
|
||||||
.showSnackBar(
|
context,
|
||||||
|
).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
'Produk "$productName" sudah ada'),
|
'Produk "$productName" sudah ada',
|
||||||
|
),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
AppColors.statusError,
|
AppColors.statusError,
|
||||||
),
|
),
|
||||||
|
|
@ -493,7 +551,8 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
name: productName,
|
name: productName,
|
||||||
category: category,
|
category: category,
|
||||||
price: priceInt,
|
price: priceInt,
|
||||||
currentStock: 0, // Default stock 0
|
currentStock:
|
||||||
|
0, // Default stock 0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
@ -504,10 +563,13 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
products.add(productName);
|
products.add(productName);
|
||||||
productCategories[productName] =
|
productCategories[productName] =
|
||||||
category;
|
category;
|
||||||
productPrices[productName] = priceInt;
|
productPrices[productName] =
|
||||||
|
priceInt;
|
||||||
|
|
||||||
if (_createNewCategory &&
|
if (_createNewCategory &&
|
||||||
!categories.contains(category)) {
|
!categories.contains(
|
||||||
|
category,
|
||||||
|
)) {
|
||||||
categories.add(category);
|
categories.add(category);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -515,21 +577,25 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(
|
||||||
.showSnackBar(
|
context,
|
||||||
|
).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
'Produk "$productName" berhasil ditambahkan'),
|
'Produk "$productName" berhasil ditambahkan',
|
||||||
|
),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
AppColors.statusSuccess,
|
AppColors.statusSuccess,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Handle error from API
|
// Handle error from API
|
||||||
String errorMsg = result['message'] ??
|
String errorMsg =
|
||||||
|
result['message'] ??
|
||||||
'Gagal menambahkan produk';
|
'Gagal menambahkan produk';
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(
|
||||||
.showSnackBar(
|
context,
|
||||||
|
).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(errorMsg),
|
content: Text(errorMsg),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
|
|
@ -539,18 +605,21 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBrown,
|
backgroundColor:
|
||||||
|
AppColors.primaryBrown,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 12),
|
vertical: 12,
|
||||||
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(
|
||||||
|
10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Simpan',
|
'Simpan',
|
||||||
style: AppTextStyles.labelLarge.copyWith(
|
style: AppTextStyles.labelLarge
|
||||||
color: Colors.white,
|
.copyWith(color: Colors.white),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -577,9 +646,7 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: Text(
|
title: Text(
|
||||||
'Transaksi Penjualan',
|
'Transaksi Penjualan',
|
||||||
style: AppTextStyles.headlineLarge.copyWith(
|
style: AppTextStyles.headlineLarge.copyWith(color: Colors.white),
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||||
|
|
@ -629,11 +696,14 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.bgLight,
|
fillColor: AppColors.bgLight,
|
||||||
),
|
),
|
||||||
items: products
|
items:
|
||||||
.map((product) => DropdownMenuItem(
|
products
|
||||||
|
.map(
|
||||||
|
(product) => DropdownMenuItem(
|
||||||
value: product,
|
value: product,
|
||||||
child: Text(product),
|
child: Text(product),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() => _selectedProduct = value);
|
setState(() => _selectedProduct = value);
|
||||||
|
|
@ -690,9 +760,7 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
label: const Text('Tambah Produk Baru'),
|
label: const Text('Tambah Produk Baru'),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
side: BorderSide(
|
side: BorderSide(color: AppColors.secondaryBlue),
|
||||||
color: AppColors.secondaryBlue,
|
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
|
|
@ -779,7 +847,8 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
'Rp ${item.unitPrice} / unit',
|
'Rp ${item.unitPrice} / unit',
|
||||||
style: AppTextStyles.labelSmall
|
style: AppTextStyles.labelSmall
|
||||||
.copyWith(
|
.copyWith(
|
||||||
color: AppColors.textSecondary,
|
color:
|
||||||
|
AppColors.textSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -803,8 +872,11 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () =>
|
onTap:
|
||||||
_updateQuantity(index, item.quantity - 1),
|
() => _updateQuantity(
|
||||||
|
index,
|
||||||
|
item.quantity - 1,
|
||||||
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
|
|
@ -831,8 +903,11 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () =>
|
onTap:
|
||||||
_updateQuantity(index, item.quantity + 1),
|
() => _updateQuantity(
|
||||||
|
index,
|
||||||
|
item.quantity + 1,
|
||||||
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
|
|
@ -859,8 +934,9 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.statusError
|
color: AppColors.statusError
|
||||||
.withOpacity(0.1),
|
.withOpacity(0.1),
|
||||||
borderRadius:
|
borderRadius: BorderRadius.circular(
|
||||||
BorderRadius.circular(6),
|
6,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.delete,
|
Icons.delete,
|
||||||
|
|
@ -963,7 +1039,8 @@ class _TransactionScreenState extends State<TransactionScreen> {
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
icon: _isLoading
|
icon:
|
||||||
|
_isLoading
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,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.2:5000';
|
static const String baseUrl = 'http://192.168.18.30:5000';
|
||||||
|
|
||||||
static const int timeoutSeconds = 30;
|
static const int timeoutSeconds = 30;
|
||||||
|
|
||||||
|
|
@ -241,22 +241,19 @@ class MLService {
|
||||||
final result = jsonDecode(response.body);
|
final result = jsonDecode(response.body);
|
||||||
return {
|
return {
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': result['message'] ?? 'Bad request'
|
'message': result['message'] ?? 'Bad request',
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
print('Create product error - Status: ${response.statusCode}');
|
print('Create product error - Status: ${response.statusCode}');
|
||||||
print('Response body: ${response.body}');
|
print('Response body: ${response.body}');
|
||||||
return {
|
return {
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': 'Server error: ${response.statusCode}'
|
'message': 'Server error: ${response.statusCode}',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Create product exception: $e');
|
print('Create product exception: $e');
|
||||||
return {
|
return {'status': 'error', 'message': 'Connection error: $e'};
|
||||||
'status': 'error',
|
|
||||||
'message': 'Connection error: $e'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,7 +277,47 @@ class MLService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save transaction to database
|
/// Save transaction and update stock automatically
|
||||||
|
static Future<Map<String, dynamic>> addTransactionWithStockUpdate({
|
||||||
|
required int productId,
|
||||||
|
required int quantity,
|
||||||
|
required int unitPrice,
|
||||||
|
required int totalPrice,
|
||||||
|
required String transactionDate, // Format: 'YYYY-MM-DD'
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final data = {
|
||||||
|
'produk_id': productId,
|
||||||
|
'jumlah': quantity,
|
||||||
|
'unit_price': unitPrice,
|
||||||
|
'total_price': totalPrice,
|
||||||
|
'transaction_date': transactionDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
final response = await http
|
||||||
|
.post(
|
||||||
|
Uri.parse('$baseUrl/transaksi'),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode(data),
|
||||||
|
)
|
||||||
|
.timeout(Duration(seconds: timeoutSeconds));
|
||||||
|
|
||||||
|
if (response.statusCode == 201) {
|
||||||
|
final result = jsonDecode(response.body);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Server error: ${response.statusCode}',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Add transaction with stock update error: $e');
|
||||||
|
return {'status': 'error', 'message': 'Connection error: $e'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save transaction to database (legacy - without stock update)
|
||||||
static Future<bool> saveTransaction({
|
static Future<bool> saveTransaction({
|
||||||
required String productName,
|
required String productName,
|
||||||
required String category,
|
required String category,
|
||||||
|
|
|
||||||
|
|
@ -236,8 +236,9 @@ def info():
|
||||||
'POST /prediksi': 'Single prediction',
|
'POST /prediksi': 'Single prediction',
|
||||||
'POST /batch-prediksi': 'Batch prediction',
|
'POST /batch-prediksi': 'Batch prediction',
|
||||||
'GET /products': 'Get all products',
|
'GET /products': 'Get all products',
|
||||||
'POST /transactions': 'Save transaction',
|
'POST /transactions': 'Save transaction (legacy)',
|
||||||
'GET /transactions': 'Get transaction history'
|
'GET /transactions': 'Get transaction history',
|
||||||
|
'POST /transaksi': 'Save transaction and update stock automatically'
|
||||||
},
|
},
|
||||||
'required_features': feature_columns
|
'required_features': feature_columns
|
||||||
}), 200
|
}), 200
|
||||||
|
|
@ -431,6 +432,94 @@ def save_transaction():
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/transaksi', methods=['POST'])
|
||||||
|
def add_transaction_with_stock_update():
|
||||||
|
"""
|
||||||
|
Add transaction AND update stock automatically
|
||||||
|
Body: {
|
||||||
|
"produk_id": 1,
|
||||||
|
"jumlah": 5,
|
||||||
|
"unit_price": 15000,
|
||||||
|
"total_price": 75000,
|
||||||
|
"transaction_date": "2024-04-05"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
# Validate required fields
|
||||||
|
required_fields = ['produk_id', 'jumlah', 'unit_price', 'total_price', 'transaction_date']
|
||||||
|
missing_fields = [f for f in required_fields if f not in data]
|
||||||
|
|
||||||
|
if missing_fields:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Missing fields: {", ".join(missing_fields)}',
|
||||||
|
'required_fields': required_fields
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
connection = get_db_connection()
|
||||||
|
if not connection:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500
|
||||||
|
|
||||||
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
|
||||||
|
# 1. Get product info
|
||||||
|
cursor.execute("SELECT id, name, category FROM products WHERE id = %s", (data['produk_id'],))
|
||||||
|
product = cursor.fetchone()
|
||||||
|
|
||||||
|
if not product:
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Product ID {data["produk_id"]} not found'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
# 2. Save transaction
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO transactions
|
||||||
|
(product_name, category, quantity, unit_price, total_price, transaction_date)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s)
|
||||||
|
""", (
|
||||||
|
product['name'],
|
||||||
|
product['category'],
|
||||||
|
data['jumlah'],
|
||||||
|
data['unit_price'],
|
||||||
|
data['total_price'],
|
||||||
|
data['transaction_date']
|
||||||
|
))
|
||||||
|
connection.commit()
|
||||||
|
transaction_id = cursor.lastrowid
|
||||||
|
|
||||||
|
# 3. Update product stock (add quantity)
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE products
|
||||||
|
SET current_stock = current_stock + %s
|
||||||
|
WHERE id = %s
|
||||||
|
""", (data['jumlah'], data['produk_id']))
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
logger.info(f"Transaction saved: ID={transaction_id}, Stock updated for product ID={data['produk_id']} (+{data['jumlah']})")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'Stok berhasil ditambahkan',
|
||||||
|
'transaction_id': transaction_id,
|
||||||
|
'product_id': data['produk_id'],
|
||||||
|
'product_name': product['name'],
|
||||||
|
'quantity_added': data['jumlah']
|
||||||
|
}), 201
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Add transaction with stock update error: {str(e)}")
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/transactions', methods=['GET'])
|
@app.route('/transactions', methods=['GET'])
|
||||||
def get_transactions():
|
def get_transactions():
|
||||||
"""Get transaction history"""
|
"""Get transaction history"""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue