From 634edd078a15725963af36dfaea28c8bd38f5fb8 Mon Sep 17 00:00:00 2001 From: rhanarmt Date: Sun, 17 May 2026 22:30:10 +0700 Subject: [PATCH] memperbarui beberapa fitur dilaporan seperti hasil export dan grafik tren permintaan produk --- android/app/src/main/AndroidManifest.xml | 5 +- docs/use_case_aplikasi.svg | 124 ++++++++++++++ lib/screens/reports/report_controller.dart | 15 +- lib/screens/reports/report_page.dart | 187 ++++++++++++++------- lib/services/ml_service.dart | 4 +- ml_model/app.py | 6 +- 6 files changed, 278 insertions(+), 63 deletions(-) create mode 100644 docs/use_case_aplikasi.svg diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 310cc19..7dd78b6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,11 @@ + + + android:icon="@mipmap/ic_launcher" + android:usesCleartextTraffic="true"> + + + + + + + + + Use Case Diagram Aplikasi Prediksi dan Pengelolaan Stok Bahan Kue + + + + + + + + Admin / + Pemilik Toko + + + + Sistem Aplikasi Mobile + + + + Login + + + Melihat + Dashboard + + + Mengelola Produk + dan Stok Bahan + + + Mencatat + Stok Masuk + + + Melakukan Prediksi + Kebutuhan Produksi + + + Submit Produksi + dan Update Stok + + + Melihat Laporan + dan Analitik + + + Mengatur + Aplikasi + + + + Mengambil Data Resep + + + Menghitung + Kebutuhan Bahan + + + Mengecek + Ketersediaan Stok + + + Mengurangi + Stok Otomatis + + + Menyimpan + Hasil Prediksi + + + Mengekspor + Laporan + + + + + + + + + + + + + + <<include>> + + + <<include>> + + + <<include>> + + + <<include>> + + + <<include>> + + + <<include>> + + + + Model Random Forest digunakan melalui Flask API + untuk mendukung proses prediksi permintaan. + diff --git a/lib/screens/reports/report_controller.dart b/lib/screens/reports/report_controller.dart index 20445b7..b14adc0 100644 --- a/lib/screens/reports/report_controller.dart +++ b/lib/screens/reports/report_controller.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:finalproject/services/ml_service.dart'; import 'package:finalproject/utils/stock_status.dart'; @@ -401,9 +402,19 @@ class ReportController extends ChangeNotifier { DateTime _parseDate(dynamic value) { if (value is DateTime) return value; if (value is String) { - return DateTime.tryParse(value) ?? DateTime.now(); + final text = value.trim(); + if (text.isEmpty) return DateTime.fromMillisecondsSinceEpoch(0); + + final parsed = DateTime.tryParse(text); + if (parsed != null) return parsed; + + try { + return HttpDate.parse(text).toLocal(); + } catch (_) { + return DateTime.fromMillisecondsSinceEpoch(0); + } } - return DateTime.now(); + return DateTime.fromMillisecondsSinceEpoch(0); } @override diff --git a/lib/screens/reports/report_page.dart b/lib/screens/reports/report_page.dart index 7bdad3e..213cd8a 100644 --- a/lib/screens/reports/report_page.dart +++ b/lib/screens/reports/report_page.dart @@ -691,63 +691,7 @@ class _ReportScreenState extends State { message: 'Belum ada data penggunaan bahan. Jalankan prediksi dan simpan hasil produksi agar grafik terisi.', ) - : SizedBox( - height: 180, - child: BarChart( - BarChartData( - borderData: FlBorderData(show: false), - titlesData: FlTitlesData( - leftTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - rightTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - topTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - interval: 1, - getTitlesWidget: (value, meta) { - if (value % 1 != 0) { - return const SizedBox.shrink(); - } - if (value < 0 || value >= usageBars.length) { - return const SizedBox.shrink(); - } - - return Padding( - padding: const EdgeInsets.only(top: 6), - child: Text( - usageBars[value.toInt()]['label'] as String, - style: AppTextStyles.labelSmall.copyWith( - color: AppColors.textSecondary, - ), - ), - ); - }, - ), - ), - ), - barGroups: - usageBars.asMap().entries.map((entry) { - return BarChartGroupData( - x: entry.key, - barRods: [ - BarChartRodData( - toY: entry.value['value'] as double, - color: entry.value['color'] as Color, - width: 18, - borderRadius: BorderRadius.circular(6), - ), - ], - ); - }).toList(), - ), - ), - ), + : _buildUsageBarChart(usageBars), ), const SizedBox(height: 16), _buildChartCard( @@ -794,6 +738,135 @@ class _ReportScreenState extends State { ); } + Widget _buildUsageBarChart(List> usageBars) { + return LayoutBuilder( + builder: (context, constraints) { + final minChartWidth = usageBars.length * 76.0; + final chartWidth = + minChartWidth > constraints.maxWidth + ? minChartWidth + : constraints.maxWidth; + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: chartWidth, + height: 250, + child: BarChart( + BarChartData( + borderData: FlBorderData(show: false), + gridData: FlGridData( + getDrawingHorizontalLine: + (_) => FlLine( + color: AppColors.grey200.withValues(alpha: 0.7), + strokeWidth: 1, + ), + getDrawingVerticalLine: + (_) => FlLine( + color: AppColors.grey200.withValues(alpha: 0.7), + strokeWidth: 1, + ), + ), + barTouchData: BarTouchData( + touchTooltipData: BarTouchTooltipData( + tooltipBgColor: AppColors.textPrimary, + getTooltipItem: (group, groupIndex, rod, rodIndex) { + final item = usageBars[group.x.toInt()]; + return BarTooltipItem( + '${item['label']}\n${_formatQuantity(rod.toY)} ${item['unit']}', + AppTextStyles.labelSmall.copyWith( + color: Colors.white, + fontWeight: FontWeight.w700, + ), + ); + }, + ), + ), + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: 1, + reservedSize: 72, + getTitlesWidget: (value, meta) { + if (value % 1 != 0) { + return const SizedBox.shrink(); + } + if (value < 0 || value >= usageBars.length) { + return const SizedBox.shrink(); + } + + return SideTitleWidget( + axisSide: meta.axisSide, + space: 10, + child: SizedBox( + width: 68, + child: Text( + _compactUsageLabel( + usageBars[value.toInt()]['label'] as String, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: AppTextStyles.labelSmall.copyWith( + color: AppColors.textSecondary, + fontWeight: FontWeight.w700, + height: 1.15, + ), + ), + ), + ); + }, + ), + ), + ), + groupsSpace: 22, + barGroups: + usageBars.asMap().entries.map((entry) { + return BarChartGroupData( + x: entry.key, + barRods: [ + BarChartRodData( + toY: entry.value['value'] as double, + color: entry.value['color'] as Color, + width: 22, + borderRadius: BorderRadius.circular(6), + ), + ], + ); + }).toList(), + ), + ), + ), + ); + }, + ); + } + + String _compactUsageLabel(String label) { + final words = label.trim().split(RegExp(r'\s+')); + if (words.length <= 2) return label; + + final lastWord = words.last; + final quantityPattern = RegExp(r'^\d+([,.]\d+)?[a-zA-Z]+$'); + if (quantityPattern.hasMatch(lastWord)) { + return '${words.take(words.length - 1).join(' ')}\n$lastWord'; + } + + final splitIndex = (words.length / 2).ceil(); + return '${words.take(splitIndex).join(' ')}\n' + '${words.skip(splitIndex).join(' ')}'; + } + Widget _buildChartCard({required String title, required Widget child}) { return Container( padding: const EdgeInsets.all(16), diff --git a/lib/services/ml_service.dart b/lib/services/ml_service.dart index a94b5ef..8596b71 100644 --- a/lib/services/ml_service.dart +++ b/lib/services/ml_service.dart @@ -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://10.112.239.23:5000'; + static const String baseUrl = 'http://192.168.1.72:5000'; static const int timeoutSeconds = 30; @@ -270,12 +270,14 @@ class MLService { return jsonDecode(response.body); } + print('Report request failed [$path]: ${response.statusCode}'); return { 'status': false, 'message': 'Server error: ${response.statusCode}', 'data': [], }; } catch (e) { + print('Report request error [$path]: $e'); return {'status': false, 'message': 'Connection error: $e', 'data': []}; } } diff --git a/ml_model/app.py b/ml_model/app.py index 5567f14..863be05 100644 --- a/ml_model/app.py +++ b/ml_model/app.py @@ -1442,10 +1442,11 @@ def laporan_stok_masuk(): data = [] for row in rows: + tanggal = row.get('tanggal') data.append({ 'nama_bahan': row.get('nama_bahan'), 'jumlah': float(row.get('jumlah') or 0), - 'tanggal': row.get('tanggal'), + 'tanggal': tanggal.isoformat() if hasattr(tanggal, 'isoformat') else tanggal, 'unit': row.get('unit') or 'kg' }) @@ -1499,11 +1500,12 @@ def laporan_prediksi(): data = [] for row in rows: + tanggal_prediksi = row.get('tanggal_prediksi') data.append({ 'nama_produk': row.get('nama_produk'), 'hasil_prediksi': float(row.get('hasil_prediksi') or 0), 'estimasi_kebutuhan_bahan': row.get('estimasi_kebutuhan_bahan'), - 'tanggal_prediksi': row.get('tanggal_prediksi') + 'tanggal_prediksi': tanggal_prediksi.isoformat() if hasattr(tanggal_prediksi, 'isoformat') else tanggal_prediksi }) return build_report_response(True, 'Data berhasil diambil', data, 200)