fix submit fitur prediksi dan pengurangan stock
This commit is contained in:
parent
ac2bb063bd
commit
6ad544fb75
|
|
@ -8,6 +8,7 @@ import 'package:finalproject/screens/transaction/transaction_page.dart';
|
|||
import 'package:finalproject/screens/products/product_list_page.dart';
|
||||
import 'package:finalproject/screens/reports/report_page.dart';
|
||||
import 'package:finalproject/screens/settings/settings_page.dart';
|
||||
import 'package:finalproject/utils/route_observer.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
|
|
@ -22,7 +23,8 @@ class MyApp extends StatelessWidget {
|
|||
title: 'Prediksi Stok Bahan Kue',
|
||||
theme: AppTheme.lightTheme(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
initialRoute: '/login',
|
||||
navigatorObservers: [routeObserver],
|
||||
initialRoute: '/splash',
|
||||
routes: {
|
||||
'/splash': (context) => const SplashScreen(),
|
||||
'/login': (context) => const LoginScreen(),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class PredictionScreen extends StatefulWidget {
|
|||
|
||||
class _PredictionScreenState extends State<PredictionScreen> {
|
||||
late final PredictionController _controller;
|
||||
final TextEditingController _productionController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -32,9 +33,94 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
|||
if (!mounted) return;
|
||||
|
||||
if (result['status'] == 'success') {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('✅ Stok berhasil diperbarui')),
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.12),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF10B981).withOpacity(0.15),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.check_circle,
|
||||
color: Color(0xFF10B981),
|
||||
size: 36,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Text(
|
||||
'Produksi Berhasil',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'Stok bahan sudah diperbarui sesuai produksi.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.4,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFA89080),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Sukses',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
await _controller.refreshStock();
|
||||
_controller.reset();
|
||||
_productionController.clear();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(result['message'] ?? 'Gagal submit')),
|
||||
|
|
@ -341,6 +427,7 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _productionController,
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged:
|
||||
_controller.setProductionQuantity,
|
||||
|
|
@ -419,7 +506,10 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
|||
),
|
||||
const SizedBox(width: 10),
|
||||
OutlinedButton(
|
||||
onPressed: _controller.reset,
|
||||
onPressed: () {
|
||||
_controller.reset();
|
||||
_productionController.clear();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(
|
||||
color: Color(0xFFE5E7EB),
|
||||
|
|
@ -1151,6 +1241,7 @@ class _PredictionScreenState extends State<PredictionScreen> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_productionController.dispose();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:finalproject/models/product_model.dart';
|
||||
import 'package:finalproject/theme/colors.dart';
|
||||
import 'package:finalproject/theme/text_styles.dart';
|
||||
import 'package:finalproject/utils/route_observer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'product_list_controller.dart';
|
||||
|
|
@ -12,7 +13,7 @@ class ProductListScreen extends StatefulWidget {
|
|||
State<ProductListScreen> createState() => _ProductListScreenState();
|
||||
}
|
||||
|
||||
class _ProductListScreenState extends State<ProductListScreen> {
|
||||
class _ProductListScreenState extends State<ProductListScreen> with RouteAware {
|
||||
late final ProductListController _controller;
|
||||
|
||||
@override
|
||||
|
|
@ -30,6 +31,20 @@ class _ProductListScreenState extends State<ProductListScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final route = ModalRoute.of(context);
|
||||
if (route != null) {
|
||||
routeObserver.subscribe(this, route);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
_controller.loadProducts();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
|
|
@ -546,6 +561,7 @@ class _ProductListScreenState extends State<ProductListScreen> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
routeObserver.unsubscribe(this);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,16 +64,16 @@ class _SplashScreenState extends State<SplashScreen>
|
|||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
if (mounted) {
|
||||
// Navigate ke Dashboard
|
||||
Navigator.of(context).pushReplacementNamed('/dashboard');
|
||||
// Navigate ke Login
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _statusMessage = 'Error: ${e.toString()}');
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
if (mounted) {
|
||||
// Navigate ke Dashboard even on error
|
||||
Navigator.of(context).pushReplacementNamed('/dashboard');
|
||||
// Navigate ke Login even on error
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -166,8 +166,10 @@ class _SplashScreenState extends State<SplashScreen>
|
|||
|
||||
// App name with fade-in
|
||||
FadeTransition(
|
||||
opacity: Tween<double>(begin: 0.0, end: 1.0)
|
||||
.animate(
|
||||
opacity: Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _fadeController,
|
||||
curve: Curves.easeInOut,
|
||||
|
|
@ -237,8 +239,10 @@ class _SplashScreenState extends State<SplashScreen>
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ScaleTransition(
|
||||
scale: Tween<double>(begin: 0.4, end: 1.0)
|
||||
.animate(
|
||||
scale: Tween<double>(
|
||||
begin: 0.4,
|
||||
end: 1.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _rotateController,
|
||||
curve: Curves.easeInOut,
|
||||
|
|
@ -255,8 +259,10 @@ class _SplashScreenState extends State<SplashScreen>
|
|||
),
|
||||
const SizedBox(width: 6),
|
||||
ScaleTransition(
|
||||
scale: Tween<double>(begin: 0.4, end: 1.0)
|
||||
.animate(
|
||||
scale: Tween<double>(
|
||||
begin: 0.4,
|
||||
end: 1.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _rotateController,
|
||||
curve: Curves.easeInOut,
|
||||
|
|
@ -273,8 +279,10 @@ class _SplashScreenState extends State<SplashScreen>
|
|||
),
|
||||
const SizedBox(width: 6),
|
||||
ScaleTransition(
|
||||
scale: Tween<double>(begin: 0.4, end: 1.0)
|
||||
.animate(
|
||||
scale: Tween<double>(
|
||||
begin: 0.4,
|
||||
end: 1.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _rotateController,
|
||||
curve: Curves.easeInOut,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class MLService {
|
|||
// API URL - Change based on environment
|
||||
// Untuk emulator Android: 10.0.2.2
|
||||
// Untuk device fisik: 192.168.x.x atau 127.0.0.1 kalau local
|
||||
static const String baseUrl = 'http://192.168.1.12:5000';
|
||||
static const String baseUrl = 'http://10.103.65.129:5000';
|
||||
|
||||
static const int timeoutSeconds = 30;
|
||||
|
||||
|
|
@ -229,14 +229,23 @@ class MLService {
|
|||
print('[consumeStock] Response status: ${response.statusCode}');
|
||||
print('[consumeStock] Response body: ${response.body}');
|
||||
|
||||
final parsedBody =
|
||||
response.body.isNotEmpty ? jsonDecode(response.body) : null;
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
} else {
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Server error: ${response.statusCode}',
|
||||
};
|
||||
return parsedBody is Map<String, dynamic>
|
||||
? parsedBody
|
||||
: {'status': 'success'};
|
||||
}
|
||||
|
||||
if (parsedBody is Map<String, dynamic>) {
|
||||
return parsedBody;
|
||||
}
|
||||
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Server error: ${response.statusCode}',
|
||||
};
|
||||
} catch (e) {
|
||||
print('Consume stock error: $e');
|
||||
return {'status': 'error', 'message': 'Connection error: $e'};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
final RouteObserver<ModalRoute<void>> routeObserver =
|
||||
RouteObserver<ModalRoute<void>>();
|
||||
|
|
@ -21,9 +21,6 @@ logger = logging.getLogger(__name__)
|
|||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# Prevent overlapping stock consumption transactions
|
||||
transaction_in_progress = False
|
||||
|
||||
# ============================================================================
|
||||
# DATABASE CONFIGURATION
|
||||
# ============================================================================
|
||||
|
|
@ -63,6 +60,13 @@ def get_product_name_column(connection) -> str:
|
|||
cursor.close()
|
||||
return 'name' if has_name else 'product_name'
|
||||
|
||||
def has_product_unit_column(connection) -> bool:
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("SHOW COLUMNS FROM products LIKE 'unit'")
|
||||
has_unit = cursor.fetchone() is not None
|
||||
cursor.close()
|
||||
return has_unit
|
||||
|
||||
def grams_to_kg_rounded(grams: float) -> float:
|
||||
"""
|
||||
Convert grams to kilograms with rounding rules:
|
||||
|
|
@ -468,17 +472,14 @@ def consume_stock():
|
|||
"allow_partial": false
|
||||
}
|
||||
"""
|
||||
global transaction_in_progress
|
||||
if transaction_in_progress:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Transaction already in progress'
|
||||
}), 429
|
||||
|
||||
transaction_in_progress = True
|
||||
print("FLASK TERBARU AKTIF")
|
||||
print("REQUEST RECEIVED")
|
||||
connection = None
|
||||
cursor = None
|
||||
started_transaction = False
|
||||
try:
|
||||
data = request.json or {}
|
||||
logger.info(f"[stock/consume] Incoming payload: {data}")
|
||||
print("REQUEST DATA:", data)
|
||||
items = data.get('items', [])
|
||||
allow_partial = bool(data.get('allow_partial', False))
|
||||
|
||||
|
|
@ -493,10 +494,11 @@ def consume_stock():
|
|||
return jsonify({'status': 'error', 'message': 'Database connection failed'}), 500
|
||||
|
||||
name_column = get_product_name_column(connection)
|
||||
unit_column_exists = has_product_unit_column(connection)
|
||||
history_enabled = table_exists(connection, 'stock_usage_history')
|
||||
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
connection.start_transaction()
|
||||
started_transaction = True
|
||||
|
||||
errors = []
|
||||
valid_items = []
|
||||
|
|
@ -514,14 +516,19 @@ def consume_stock():
|
|||
})
|
||||
continue
|
||||
|
||||
if unit_column_exists:
|
||||
select_fields = f"id, {name_column} as name, current_stock, unit"
|
||||
else:
|
||||
select_fields = f"id, {name_column} as name, current_stock"
|
||||
|
||||
if product_id:
|
||||
cursor.execute(
|
||||
f"SELECT id, {name_column} as name, current_stock, unit FROM products WHERE id = %s",
|
||||
f"SELECT {select_fields} FROM products WHERE id = %s",
|
||||
(product_id,)
|
||||
)
|
||||
else:
|
||||
cursor.execute(
|
||||
f"SELECT id, {name_column} as name, current_stock, unit FROM products WHERE {name_column} = %s",
|
||||
f"SELECT {select_fields} FROM products WHERE {name_column} = %s",
|
||||
(product_name,)
|
||||
)
|
||||
|
||||
|
|
@ -573,14 +580,13 @@ def consume_stock():
|
|||
|
||||
if errors and not allow_partial:
|
||||
connection.rollback()
|
||||
cursor.close()
|
||||
connection.close()
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Ada item gagal diproses',
|
||||
'errors': errors
|
||||
}), 400
|
||||
|
||||
print("UPDATING STOCK")
|
||||
results = []
|
||||
for entry in valid_items:
|
||||
product = entry['product']
|
||||
|
|
@ -618,8 +624,6 @@ def consume_stock():
|
|||
})
|
||||
|
||||
connection.commit()
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
|
|
@ -629,10 +633,25 @@ def consume_stock():
|
|||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
if connection and started_transaction:
|
||||
try:
|
||||
connection.rollback()
|
||||
except Exception:
|
||||
logger.exception("[stock/consume] Failed to rollback transaction")
|
||||
logger.error(f"Consume stock error: {str(e)}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
finally:
|
||||
transaction_in_progress = False
|
||||
if cursor is not None:
|
||||
try:
|
||||
cursor.close()
|
||||
except Exception:
|
||||
logger.exception("[stock/consume] Failed to close cursor")
|
||||
if connection is not None:
|
||||
try:
|
||||
connection.close()
|
||||
except Exception:
|
||||
logger.exception("[stock/consume] Failed to close connection")
|
||||
print("REQUEST FINISHED")
|
||||
|
||||
|
||||
@app.route('/transactions', methods=['POST'])
|
||||
|
|
@ -998,6 +1017,11 @@ def internal_error(error):
|
|||
return jsonify({'status': 'error', 'message': 'Internal server error'}), 500
|
||||
|
||||
|
||||
@app.route('/test')
|
||||
def test():
|
||||
return {"message": "FLASK BARU AKTIF"}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MAIN
|
||||
# ============================================================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue