memperbarui beberapa fitur dilaporan seperti hasil export dan grafik tren permintaan produk

This commit is contained in:
rhanarmt 2026-05-17 22:30:10 +07:00
parent 5f2f97d693
commit 634edd078a
6 changed files with 278 additions and 63 deletions

View File

@ -1,8 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="finalproject"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"

124
docs/use_case_aplikasi.svg Normal file
View File

@ -0,0 +1,124 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="980" viewBox="0 0 1400 980">
<defs>
<style>
.title { font-family: Arial, sans-serif; font-size: 30px; font-weight: 700; fill: #2b211c; }
.label { font-family: Arial, sans-serif; font-size: 18px; fill: #2b211c; }
.small { font-family: Arial, sans-serif; font-size: 15px; fill: #5b4b43; }
.system { fill: #fffaf4; stroke: #8b5e3c; stroke-width: 3; rx: 18; }
.usecase { fill: #ffffff; stroke: #8b5e3c; stroke-width: 2.4; }
.actor-line { stroke: #2b211c; stroke-width: 3; stroke-linecap: round; fill: none; }
.assoc { stroke: #4b3a31; stroke-width: 2.1; fill: none; }
.include { stroke: #6b7280; stroke-width: 1.8; stroke-dasharray: 8 6; fill: none; marker-end: url(#arrow); }
.note { fill: #eef7ff; stroke: #7eb6d9; stroke-width: 1.8; rx: 12; }
</style>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#6b7280" />
</marker>
</defs>
<rect width="1400" height="980" fill="#f6efe8" />
<text x="700" y="54" text-anchor="middle" class="title">Use Case Diagram Aplikasi Prediksi dan Pengelolaan Stok Bahan Kue</text>
<!-- Actor -->
<circle cx="128" cy="235" r="30" fill="#ffffff" stroke="#2b211c" stroke-width="3" />
<path d="M128 265 L128 365" class="actor-line" />
<path d="M70 305 L186 305" class="actor-line" />
<path d="M128 365 L80 445" class="actor-line" />
<path d="M128 365 L176 445" class="actor-line" />
<text x="128" y="486" text-anchor="middle" class="label">Admin /</text>
<text x="128" y="510" text-anchor="middle" class="label">Pemilik Toko</text>
<!-- System boundary -->
<rect x="270" y="100" width="1040" height="810" class="system" />
<text x="790" y="137" text-anchor="middle" class="label" font-weight="700">Sistem Aplikasi Mobile</text>
<!-- Main use cases -->
<ellipse cx="520" cy="210" rx="155" ry="45" class="usecase" />
<text x="520" y="216" text-anchor="middle" class="label">Login</text>
<ellipse cx="520" cy="325" rx="155" ry="48" class="usecase" />
<text x="520" y="320" text-anchor="middle" class="label">Melihat</text>
<text x="520" y="344" text-anchor="middle" class="label">Dashboard</text>
<ellipse cx="520" cy="450" rx="155" ry="52" class="usecase" />
<text x="520" y="444" text-anchor="middle" class="label">Mengelola Produk</text>
<text x="520" y="468" text-anchor="middle" class="label">dan Stok Bahan</text>
<ellipse cx="520" cy="580" rx="155" ry="48" class="usecase" />
<text x="520" y="575" text-anchor="middle" class="label">Mencatat</text>
<text x="520" y="599" text-anchor="middle" class="label">Stok Masuk</text>
<ellipse cx="790" cy="250" rx="170" ry="54" class="usecase" />
<text x="790" y="244" text-anchor="middle" class="label">Melakukan Prediksi</text>
<text x="790" y="268" text-anchor="middle" class="label">Kebutuhan Produksi</text>
<ellipse cx="790" cy="410" rx="170" ry="54" class="usecase" />
<text x="790" y="404" text-anchor="middle" class="label">Submit Produksi</text>
<text x="790" y="428" text-anchor="middle" class="label">dan Update Stok</text>
<ellipse cx="790" cy="610" rx="170" ry="54" class="usecase" />
<text x="790" y="604" text-anchor="middle" class="label">Melihat Laporan</text>
<text x="790" y="628" text-anchor="middle" class="label">dan Analitik</text>
<ellipse cx="790" cy="760" rx="155" ry="48" class="usecase" />
<text x="790" y="755" text-anchor="middle" class="label">Mengatur</text>
<text x="790" y="779" text-anchor="middle" class="label">Aplikasi</text>
<!-- Included use cases -->
<ellipse cx="1095" cy="195" rx="150" ry="42" class="usecase" />
<text x="1095" y="201" text-anchor="middle" class="label">Mengambil Data Resep</text>
<ellipse cx="1095" cy="295" rx="150" ry="42" class="usecase" />
<text x="1095" y="290" text-anchor="middle" class="label">Menghitung</text>
<text x="1095" y="314" text-anchor="middle" class="label">Kebutuhan Bahan</text>
<ellipse cx="1095" cy="395" rx="150" ry="42" class="usecase" />
<text x="1095" y="390" text-anchor="middle" class="label">Mengecek</text>
<text x="1095" y="414" text-anchor="middle" class="label">Ketersediaan Stok</text>
<ellipse cx="1095" cy="500" rx="150" ry="42" class="usecase" />
<text x="1095" y="495" text-anchor="middle" class="label">Mengurangi</text>
<text x="1095" y="519" text-anchor="middle" class="label">Stok Otomatis</text>
<ellipse cx="1095" cy="605" rx="150" ry="42" class="usecase" />
<text x="1095" y="600" text-anchor="middle" class="label">Menyimpan</text>
<text x="1095" y="624" text-anchor="middle" class="label">Hasil Prediksi</text>
<ellipse cx="1095" cy="710" rx="150" ry="42" class="usecase" />
<text x="1095" y="705" text-anchor="middle" class="label">Mengekspor</text>
<text x="1095" y="729" text-anchor="middle" class="label">Laporan</text>
<!-- Actor associations -->
<path d="M188 235 C250 225, 300 215, 365 210" class="assoc" />
<path d="M188 270 C250 288, 300 310, 365 325" class="assoc" />
<path d="M188 310 C250 350, 300 410, 365 450" class="assoc" />
<path d="M188 350 C250 425, 300 525, 365 580" class="assoc" />
<path d="M188 250 C360 230, 520 235, 620 250" class="assoc" />
<path d="M188 305 C360 345, 520 380, 620 410" class="assoc" />
<path d="M188 395 C360 500, 540 585, 620 610" class="assoc" />
<path d="M188 445 C360 610, 560 735, 635 760" class="assoc" />
<!-- Include relationships -->
<path d="M945 235 C985 225, 1015 215, 1048 203" class="include" />
<text x="1005" y="222" class="small">&lt;&lt;include&gt;&gt;</text>
<path d="M953 270 C990 278, 1018 286, 1050 294" class="include" />
<text x="996" y="278" class="small">&lt;&lt;include&gt;&gt;</text>
<path d="M943 390 C986 390, 1015 392, 1045 394" class="include" />
<text x="975" y="382" class="small">&lt;&lt;include&gt;&gt;</text>
<path d="M940 430 C990 455, 1022 474, 1050 491" class="include" />
<text x="990" y="465" class="small">&lt;&lt;include&gt;&gt;</text>
<path d="M920 450 C985 510, 1025 560, 1054 592" class="include" />
<text x="970" y="542" class="small">&lt;&lt;include&gt;&gt;</text>
<path d="M947 640 C990 660, 1020 680, 1050 698" class="include" />
<text x="990" y="675" class="small">&lt;&lt;include&gt;&gt;</text>
<!-- Note -->
<rect x="362" y="820" width="436" height="58" class="note" />
<text x="580" y="844" text-anchor="middle" class="small">Model Random Forest digunakan melalui Flask API</text>
<text x="580" y="865" text-anchor="middle" class="small">untuk mendukung proses prediksi permintaan.</text>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

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

View File

@ -691,63 +691,7 @@ class _ReportScreenState extends State<ReportScreen> {
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<ReportScreen> {
);
}
Widget _buildUsageBarChart(List<Map<String, dynamic>> 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),

View File

@ -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': []};
}
}

View File

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