From f2967da07384bb5e4c32b54002976afa084a0dc9 Mon Sep 17 00:00:00 2001 From: rijalhabibullah Date: Tue, 12 May 2026 04:34:29 +0700 Subject: [PATCH] menambahkan beberapa diagram admin dan user --- .../padi_app/lib/dashboard_klasifikasi.dart | 49 ++- .../padi_app/lib/screen/history_screen.dart | 316 ++++++++++++--- .../padi_app/lib/screen/profile_screen.dart | 101 ++++- .../padi_app/lib/screen/result_screen.dart | 375 +++++++++++++++--- .../lib/services/classification_service.dart | 62 ++- mobile_TA/padi_app/pubspec.lock | 16 + mobile_TA/padi_app/pubspec.yaml | 1 + .../Controllers/ClassificationController.php | 101 ++++- .../Http/Controllers/DashboardController.php | 20 +- web_TA/app/Models/Classification.php | 3 + web_TA/app/Models/ClassificationHistory.php | 3 + ...05_12_add_kabupaten_to_classifications.php | 40 ++ web_TA/resources/views/dashboard.blade.php | 52 +++ 13 files changed, 1008 insertions(+), 131 deletions(-) create mode 100644 web_TA/database/migrations/2026_05_12_add_kabupaten_to_classifications.php diff --git a/mobile_TA/padi_app/lib/dashboard_klasifikasi.dart b/mobile_TA/padi_app/lib/dashboard_klasifikasi.dart index 06358f4..5631517 100644 --- a/mobile_TA/padi_app/lib/dashboard_klasifikasi.dart +++ b/mobile_TA/padi_app/lib/dashboard_klasifikasi.dart @@ -106,6 +106,9 @@ class _HomeScreenState extends State { locationAddress: location?.address, locationLat: location?.lat, locationLng: location?.lng, + kabupaten: location?.kabupaten, + kecamatan: location?.kecamatan, + kelurahan: location?.kelurahan, ); // Validasi: cek apakah confidence cukup tinggi (artinya itu daun padi) @@ -177,21 +180,44 @@ class _HomeScreenState extends State { ); String? address; + String? kabupaten; + String? kecamatan; + String? kelurahan; + if (placemarks.isNotEmpty) { final place = placemarks.first; + + // Extract components dengan urutan yang sesuai untuk Indonesia + kelurahan = place.subLocality?.trim(); // Kelurahan/Desa + kecamatan = place.locality?.trim(); // Kecamatan + + final provinsi = place.administrativeArea?.trim(); // Provinsi + final negara = place.country?.trim(); // Negara + + // Build full address: kelurahan, kecamatan, kabupaten, provinsi, negara + // Note: subAdministrativeArea (kabupaten) tidak selalu tersedia di geocoding Indonesia final parts = [ - place.street, - place.subLocality, - place.locality, - place.administrativeArea, - place.country, - ].where((part) => part != null && part!.trim().isNotEmpty) - .map((part) => part!.trim()) + kelurahan, + kecamatan, + provinsi, + negara, + ].where((part) => part != null && part!.isNotEmpty) .toList(); if (parts.isNotEmpty) { address = parts.join(', '); } + + // Extract kabupaten dari location_address jika ada (Indonesia format) + // Contoh: "Sumbersari, Kecamatan Sumbersari, Kabupaten Jember, Jawa Timur, Indonesia" + if (address != null) { + // Try extract "Kabupaten [name]" atau "Kota [name]" + final kabupatenRegex = RegExp(r'(?:Kabupaten|Kota)\s+([^,]+)'); + final kabupatenMatch = kabupatenRegex.firstMatch(address!); + if (kabupatenMatch != null) { + kabupaten = kabupatenMatch.group(1)?.trim(); + } + } } if (address != null) { @@ -205,6 +231,9 @@ class _HomeScreenState extends State { address: address, lat: position.latitude, lng: position.longitude, + kabupaten: kabupaten, + kecamatan: kecamatan, + kelurahan: kelurahan, ); } @@ -414,10 +443,16 @@ class _LocationPayload { final String? address; final double lat; final double lng; + final String? kabupaten; + final String? kecamatan; + final String? kelurahan; const _LocationPayload({ required this.address, required this.lat, required this.lng, + this.kabupaten, + this.kecamatan, + this.kelurahan, }); } \ No newline at end of file diff --git a/mobile_TA/padi_app/lib/screen/history_screen.dart b/mobile_TA/padi_app/lib/screen/history_screen.dart index f1f9a9e..60119d6 100644 --- a/mobile_TA/padi_app/lib/screen/history_screen.dart +++ b/mobile_TA/padi_app/lib/screen/history_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; import '../services/classification_service.dart'; class HistoryScreen extends StatefulWidget { @@ -52,57 +53,188 @@ class _HistoryScreenState extends State { return const Center(child: Text('Belum ada riwayat klasifikasi.')); } - return ListView.separated( - padding: const EdgeInsets.all(16), - itemCount: items.length, - separatorBuilder: (_, __) => const SizedBox(height: 12), - itemBuilder: (context, index) { - final item = items[index]; - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.06), - blurRadius: 8, - offset: const Offset(0, 3), + // Filter data bulan ini + final now = DateTime.now(); + final thisMonthItems = items.where((item) { + return item.createdAt.year == now.year && + item.createdAt.month == now.month; + }).toList(); + + // Hitung jumlah per klasifikasi bulan ini + final classificationCount = {}; + for (var item in thisMonthItems) { + classificationCount[item.diseaseName] = + (classificationCount[item.diseaseName] ?? 0) + 1; + } + + return SingleChildScrollView( + child: Column( + children: [ + // Pie Chart Section + if (classificationCount.isNotEmpty) + Container( + padding: const EdgeInsets.all(20), + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.diseaseName, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + child: Column( + children: [ + Text( + 'Klasifikasi Bulan ${_getMonthName(now.month)} ${now.year}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + SizedBox( + height: 280, + child: PieChart( + PieChartData( + sections: _buildPieChartSections(classificationCount), + centerSpaceRadius: 40, + sectionsSpace: 2, + ), + ), + ), + const SizedBox(height: 20), + // Legend + Column( + children: classificationCount.entries.map((entry) { + final color = _getColorForDisease(entry.key); + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text(entry.key), + ), + Text( + '${entry.value}x', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + }).toList(), + ), + ], + ), + ) + else + Padding( + padding: const EdgeInsets.all(20), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.blue), + ), + child: const Center( + child: Text( + 'Belum ada data klasifikasi bulan ini', + style: TextStyle(color: Colors.blue), + ), ), ), - const SizedBox(height: 6), - _infoRow( - icon: Icons.label, - text: 'Kelas: ${item.predictedClass}', + ), + + // History List Section + if (items.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Text( + 'Semua Riwayat Klasifikasi', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: items.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final item = items[index]; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.diseaseName, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 6), + _infoRow( + icon: Icons.label, + text: 'Kelas: ${item.predictedClass}', + ), + _infoRow( + icon: Icons.bar_chart, + text: 'Confidence: ${item.confidence}', + ), + _infoRow( + icon: Icons.location_on, + text: + 'Lokasi: ${item.locationAddress ?? 'Alamat tidak terdeteksi'}', + ), + _infoRow( + icon: Icons.calendar_today, + text: _formatTimestamp(item.createdAt), + ), + ], + ), + ); + }, + ), + ], ), - _infoRow( - icon: Icons.bar_chart, - text: 'Confidence: ${item.confidence}', - ), - _infoRow( - icon: Icons.location_on, - text: - 'Lokasi: ${item.locationAddress ?? 'Alamat tidak terdeteksi'}', - ), - _infoRow( - icon: Icons.calendar_today, - text: _formatTimestamp(item.createdAt), - ), - ], - ), - ); - }, + ), + ], + ), ); }, ), @@ -132,4 +264,94 @@ class _HistoryScreenState extends State { ), ); } + + List _buildPieChartSections(Map classificationCount) { + final total = classificationCount.values.fold(0, (sum, val) => sum + val); + + return classificationCount.entries.map((entry) { + final value = entry.value; + final percentage = (value / total * 100); + final diseaseName = _getNormalizedDiseaseName(entry.key); + + return PieChartSectionData( + color: _getColorForDisease(entry.key), + value: value.toDouble(), + title: '$diseaseName\n${percentage.toStringAsFixed(1)}%', + radius: 80, + titleStyle: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: Colors.white, + height: 1.2, + ), + ); + }).toList(); + } + + Color _getColorForDisease(String diseaseName) { + final colors = { + 'Bacterial Blight': const Color(0xFFEF5350), // Merah cerah + 'Brown Spot': const Color(0xFFA1887F), // Coklat + 'Leaf Smut': const Color(0xFF455A64), // Abu-abu gelap + 'Healthy': const Color(0xFF66BB6A), // Hijau cerah + 'bacterial blight': const Color(0xFFEF5350), // Merah cerah (lowercase) + 'brown spot': const Color(0xFFA1887F), // Coklat (lowercase) + 'leaf smut': const Color(0xFF455A64), // Abu-abu gelap (lowercase) + 'healthy': const Color(0xFF66BB6A), // Hijau cerah (lowercase) + }; + + // Coba exact match dulu, jika tidak ada gunakan warna berdasarkan hash + if (colors.containsKey(diseaseName)) { + return colors[diseaseName]!; + } + + // Jika tidak ada di map, assign warna berdasarkan hash nama untuk konsistensi + final hash = diseaseName.hashCode.abs(); + final colorList = [ + const Color(0xFFEF5350), // Red + const Color(0xFFAB47BC), // Purple + const Color(0xFF5C6BC0), // Indigo + const Color(0xFF42A5F5), // Blue + const Color(0xFF29B6F6), // Light Blue + const Color(0xFF26C6DA), // Cyan + const Color(0xFF26A69A), // Teal + const Color(0xFF66BB6A), // Green + const Color(0xFF9CCC65), // Light Green + const Color(0xFFD4AF37), // Gold + const Color(0xFFFF7043), // Deep Orange + const Color(0xFFA1887F), // Brown + ]; + + return colorList[hash % colorList.length]; + } + + String _getMonthName(int month) { + final months = [ + 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', + 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember' + ]; + return months[month - 1]; + } + + String _getNormalizedDiseaseName(String diseaseName) { + final normalized = { + 'Bacterial Blight': 'Bacterial Blight', + 'bacterial blight': 'Bacterial Blight', + 'Bacterialblight': 'Bacterial Blight', + 'Bercak Bakteri (Bacterial Blight)': 'Bacterial Blight', + 'Brown Spot': 'Brown Spot', + 'brown spot': 'Brown Spot', + 'Brownspot': 'Brown Spot', + 'Bercak Coklat (Brown Spot)': 'Brown Spot', + 'Leaf Smut': 'Leaf Smut', + 'leaf smut': 'Leaf Smut', + 'Leafsmut': 'Leaf Smut', + 'Jamur Daun (Leaf Smut)': 'Leaf Smut', + 'Healthy': 'Healthy', + 'healthy': 'Healthy', + 'Sehat (Healthy)': 'Healthy', + }; + + return normalized[diseaseName] ?? diseaseName; + } } \ No newline at end of file diff --git a/mobile_TA/padi_app/lib/screen/profile_screen.dart b/mobile_TA/padi_app/lib/screen/profile_screen.dart index 42ae967..622cf6a 100644 --- a/mobile_TA/padi_app/lib/screen/profile_screen.dart +++ b/mobile_TA/padi_app/lib/screen/profile_screen.dart @@ -15,6 +15,9 @@ class _ProfileScreenState extends State { String _userEmail = ""; String _userPhone = ""; String _userLocation = ""; + String _userKabupaten = ""; + String _userKecamatan = ""; + String _userKelurahan = ""; @override void initState() { @@ -30,6 +33,9 @@ class _ProfileScreenState extends State { _userEmail = prefs.getString('user_email') ?? ''; _userPhone = prefs.getString('user_phone') ?? ''; _userLocation = prefs.getString('user_location') ?? ''; + _userKabupaten = prefs.getString('user_kabupaten') ?? ''; + _userKecamatan = prefs.getString('user_kecamatan') ?? ''; + _userKelurahan = prefs.getString('user_kelurahan') ?? ''; }); } @@ -82,26 +88,53 @@ class _ProfileScreenState extends State { } final place = placemarks.first; + + // Extract components dengan urutan yang sesuai untuk Indonesia + final kelurahan = place.subLocality?.trim(); // Kelurahan/Desa + final kecamatan = place.locality?.trim(); // Kecamatan + final provinsi = place.administrativeArea?.trim(); // Provinsi + final negara = place.country?.trim(); // Negara + + // Build full address: kelurahan, kecamatan, kabupaten, provinsi, negara final parts = [ - place.street, - place.subLocality, - place.locality, - place.administrativeArea, - place.country, - ].where((part) => part != null && part!.trim().isNotEmpty) - .map((part) => part!.trim()) + kelurahan, + kecamatan, + provinsi, + negara, + ].where((part) => part != null && part!.isNotEmpty) .toList(); if (parts.isEmpty) { return null; } - return parts.join(', '); + final address = parts.join(', '); + + // Extract kabupaten dari address jika ada (Indonesia format) + // Contoh: "Sumbersari, Kecamatan Sumbersari, Kabupaten Jember, Jawa Timur, Indonesia" + String? kabupaten; + final kabupatenRegex = RegExp(r'(?:Kabupaten|Kota)\s+([^,]+)'); + final kabupatenMatch = kabupatenRegex.firstMatch(address); + if (kabupatenMatch != null) { + kabupaten = kabupatenMatch.group(1)?.trim(); + } + + // Save to shared preferences + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('user_location', address); + if (kelurahan != null) await prefs.setString('user_kelurahan', kelurahan); + if (kecamatan != null) await prefs.setString('user_kecamatan', kecamatan); + if (kabupaten != null) await prefs.setString('user_kabupaten', kabupaten); + + return address; } void _openEditProfile() { final phoneController = TextEditingController(text: _userPhone); final locationController = TextEditingController(text: _userLocation); + final kabupatenController = TextEditingController(text: _userKabupaten); + final kecamatanController = TextEditingController(text: _userKecamatan); + final kelurahanController = TextEditingController(text: _userKelurahan); showModalBottomSheet( context: context, @@ -142,6 +175,30 @@ class _ProfileScreenState extends State { border: OutlineInputBorder(), ), ), + const SizedBox(height: 12), + TextField( + controller: kabupatenController, + decoration: const InputDecoration( + labelText: 'Kabupaten', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextField( + controller: kecamatanController, + decoration: const InputDecoration( + labelText: 'Kecamatan', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextField( + controller: kelurahanController, + decoration: const InputDecoration( + labelText: 'Kelurahan', + border: OutlineInputBorder(), + ), + ), const SizedBox(height: 10), TextButton.icon( onPressed: () async { @@ -157,6 +214,12 @@ class _ProfileScreenState extends State { return; } locationController.text = address; + + // Load saved values from prefs + final prefs = await SharedPreferences.getInstance(); + kabupatenController.text = prefs.getString('user_kabupaten') ?? ''; + kecamatanController.text = prefs.getString('user_kecamatan') ?? ''; + kelurahanController.text = prefs.getString('user_kelurahan') ?? ''; }, icon: const Icon(Icons.my_location), label: const Text('Gunakan lokasi saat ini'), @@ -176,11 +239,26 @@ class _ProfileScreenState extends State { 'user_location', locationController.text.trim(), ); + await prefs.setString( + 'user_kabupaten', + kabupatenController.text.trim(), + ); + await prefs.setString( + 'user_kecamatan', + kecamatanController.text.trim(), + ); + await prefs.setString( + 'user_kelurahan', + kelurahanController.text.trim(), + ); if (mounted) { setState(() { _userPhone = phoneController.text.trim(); _userLocation = locationController.text.trim(); + _userKabupaten = kabupatenController.text.trim(); + _userKecamatan = kecamatanController.text.trim(); + _userKelurahan = kelurahanController.text.trim(); }); } @@ -225,6 +303,9 @@ class _ProfileScreenState extends State { await prefs.remove('user_email'); await prefs.remove('user_phone'); await prefs.remove('user_location'); + await prefs.remove('user_kabupaten'); + await prefs.remove('user_kecamatan'); + await prefs.remove('user_kelurahan'); if (mounted) { Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false); } @@ -330,9 +411,9 @@ class _ProfileScreenState extends State { const Divider(height: 20), _buildProfileField( icon: Icons.location_on, - label: "Lokasi", + label: "Lokasi Lengkap", value: _userLocation, - placeholder: 'Tambahkan lokasi', + placeholder: 'Lokasi lengkap', maxLines: 3, ), ], diff --git a/mobile_TA/padi_app/lib/screen/result_screen.dart b/mobile_TA/padi_app/lib/screen/result_screen.dart index 820c93b..d9b0ba6 100644 --- a/mobile_TA/padi_app/lib/screen/result_screen.dart +++ b/mobile_TA/padi_app/lib/screen/result_screen.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../main.dart'; import '../services/classification_service.dart'; +import '../services/product_service.dart'; -class ResultScreen extends StatelessWidget { +class ResultScreen extends StatefulWidget { final ClassificationResult result; const ResultScreen({ @@ -10,6 +12,19 @@ class ResultScreen extends StatelessWidget { required this.result, }); + @override + State createState() => _ResultScreenState(); +} + +class _ResultScreenState extends State { + late Future> _productsFuture; + + @override + void initState() { + super.initState(); + _productsFuture = ProductService().fetchProducts(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -35,7 +50,7 @@ class ResultScreen extends StatelessWidget { children: [ const Text("Penyakit Terdeteksi:", style: TextStyle(fontSize: 16)), Text( - result.diseaseInfo.name, + widget.result.diseaseInfo.name, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.red), ), const SizedBox(height: 12), @@ -50,7 +65,7 @@ class ResultScreen extends StatelessWidget { child: Column( children: [ Text( - "Confidence: ${result.confidence}", + "Confidence: ${widget.result.confidence}", style: TextStyle( color: Colors.green[700], fontWeight: FontWeight.bold, @@ -61,11 +76,11 @@ class ResultScreen extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( - value: result.confidenceValue, + value: widget.result.confidenceValue, minHeight: 8, backgroundColor: Colors.grey[300], valueColor: AlwaysStoppedAnimation( - result.confidenceValue > 0.8 ? Colors.green : Colors.orange, + widget.result.confidenceValue > 0.8 ? Colors.green : Colors.orange, ), ), ), @@ -79,11 +94,11 @@ class ResultScreen extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( - color: _getSeverityColor(result.diseaseInfo.severity), + color: _getSeverityColor(widget.result.diseaseInfo.severity), borderRadius: BorderRadius.circular(20), ), child: Text( - "Severity: ${result.diseaseInfo.severity}", + "Severity: ${widget.result.diseaseInfo.severity}", style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, @@ -101,6 +116,103 @@ class ResultScreen extends StatelessWidget { ), ), + // Location Info Card + if (widget.result.kabupaten != null || widget.result.locationAddress != null) + Padding( + padding: const EdgeInsets.only(top: 20), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.blue[200]!), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.location_on, color: Colors.blue[700], size: 20), + const SizedBox(width: 8), + const Text( + "Informasi Lokasi", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + ), + ], + ), + const SizedBox(height: 12), + if (widget.result.kabupaten != null) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 100, + child: Text("Kabupaten:", style: TextStyle(fontWeight: FontWeight.w500, fontSize: 13)), + ), + Expanded( + child: Text(widget.result.kabupaten!, style: const TextStyle(fontSize: 13)), + ), + ], + ), + ), + if (widget.result.kecamatan != null) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 100, + child: Text("Kecamatan:", style: TextStyle(fontWeight: FontWeight.w500, fontSize: 13)), + ), + Expanded( + child: Text(widget.result.kecamatan!, style: const TextStyle(fontSize: 13)), + ), + ], + ), + ), + if (widget.result.kelurahan != null) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 100, + child: Text("Kelurahan:", style: TextStyle(fontWeight: FontWeight.w500, fontSize: 13)), + ), + Expanded( + child: Text(widget.result.kelurahan!, style: const TextStyle(fontSize: 13)), + ), + ], + ), + ), + if (widget.result.locationAddress != null) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 100, + child: Text("Alamat Lengkap:", style: TextStyle(fontWeight: FontWeight.w500, fontSize: 13)), + ), + Expanded( + child: Text( + widget.result.locationAddress!, + style: const TextStyle(fontSize: 12), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 30), // Semua Prediksi @@ -120,7 +232,7 @@ class ResultScreen extends StatelessWidget { style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), - ...result.allPredictions.entries.map((entry) { + ...widget.result.allPredictions.entries.map((entry) { final score = entry.value as double; final percentage = (score * 100).toStringAsFixed(2); return Padding( @@ -143,14 +255,14 @@ class ResultScreen extends StatelessWidget { minHeight: 6, backgroundColor: Colors.grey[300], valueColor: AlwaysStoppedAnimation( - entry.key == result.predictedClass ? Colors.green : Colors.blue, + entry.key == widget.result.predictedClass ? Colors.green : Colors.blue, ), ), ), ], ), ); - }).toList(), + }), ], ), ), @@ -162,19 +274,72 @@ class ResultScreen extends StatelessWidget { ), const SizedBox(height: 15), - // Grid Produk - GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 15, - mainAxisSpacing: 15, - childAspectRatio: 0.8, - ), - itemCount: 4, - itemBuilder: (context, index) { - return _buildProductCard(); + // Grid Produk dengan FutureBuilder + FutureBuilder>( + future: _productsFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return SizedBox( + height: 300, + child: Center( + child: CircularProgressIndicator( + color: const Color(0xFF0F703A), + ), + ), + ); + } + + if (snapshot.hasError) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.red[50], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.red), + ), + child: Column( + children: [ + Icon(Icons.error, color: Colors.red, size: 40), + const SizedBox(height: 10), + Text( + "Gagal memuat produk: ${snapshot.error}", + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + final products = snapshot.data ?? []; + + if (products.isEmpty) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Text("Tidak ada produk tersedia"), + ), + ); + } + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 15, + mainAxisSpacing: 15, + childAspectRatio: 0.75, + ), + itemCount: products.length, + itemBuilder: (context, index) { + return _buildProductCard(products[index]); + }, + ); }, ), const SizedBox(height: 40), @@ -217,56 +382,156 @@ class ResultScreen extends StatelessWidget { } } - Widget _buildProductCard() { - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.shade300), - ), - child: Column( - children: [ - Expanded( - child: Container( - color: Colors.grey[300], - child: Center( - child: Icon(Icons.inventory_2, size: 50, color: Colors.grey), + Widget _buildProductCard(ProductItem product) { + return GestureDetector( + onTap: () { + if (product.marketplaceLink != null && product.marketplaceLink!.isNotEmpty) { + _launchUrl(product.marketplaceLink!); + } + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 8)], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Gambar Produk + Expanded( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + child: product.imageUrl != null && product.imageUrl!.isNotEmpty + ? Image.network( + product.imageUrl!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Icon(Icons.image_not_supported, + size: 40, + color: Colors.grey[400] + ), + ); + }, + ) + : Center( + child: Icon( + Icons.inventory_2, + size: 40, + color: Colors.grey[400], + ), + ), ), ), - ), - Container( - padding: const EdgeInsets.all(8), - width: double.infinity, - color: Colors.black, - child: const Text( - "Deskripsi Produk", - style: TextStyle(color: Colors.white, fontSize: 12), - textAlign: TextAlign.center, + + // Info Produk + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Nama Produk + Text( + product.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + + const SizedBox(height: 4), + + // Harga + Text( + 'Rp ${product.price.toStringAsFixed(0)}', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + + const SizedBox(height: 6), + + // Tombol Beli + if (product.marketplaceLink != null && product.marketplaceLink!.isNotEmpty) + SizedBox( + width: double.infinity, + height: 28, + child: ElevatedButton( + onPressed: () => _launchUrl(product.marketplaceLink!), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0F703A), + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: const Text( + 'Beli', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ], + ), ), - ) - ], + ], + ), ), ); } + Future _launchUrl(String urlString) async { + final Uri url = Uri.parse(urlString); + try { + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Tidak dapat membuka link')), + ); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } + } + void _showDetailPopup(BuildContext context) { showDialog( context: context, builder: (context) => AlertDialog( - title: Text(result.diseaseInfo.name), + title: Text(widget.result.diseaseInfo.name), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // Deskripsi - if (result.diseaseInfo.description.isNotEmpty) + if (widget.result.diseaseInfo.description.isNotEmpty) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("Deskripsi:", style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), - Text(result.diseaseInfo.description), + Text(widget.result.diseaseInfo.description), const SizedBox(height: 16), ], ), @@ -274,7 +539,7 @@ class ResultScreen extends StatelessWidget { // Gejala const Text("Gejala:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), const SizedBox(height: 8), - ...result.diseaseInfo.symptoms.map((symptom) { + ...widget.result.diseaseInfo.symptoms.map((symptom) { return Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( @@ -285,14 +550,14 @@ class ResultScreen extends StatelessWidget { ], ), ); - }).toList(), + }), const SizedBox(height: 16), // Penanganan const Text("Penanganan:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), const SizedBox(height: 8), - ...result.diseaseInfo.treatment.map((treatment) { + ...widget.result.diseaseInfo.treatment.map((treatment) { return Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( @@ -303,7 +568,7 @@ class ResultScreen extends StatelessWidget { ], ), ); - }).toList(), + }), ], ), ), diff --git a/mobile_TA/padi_app/lib/services/classification_service.dart b/mobile_TA/padi_app/lib/services/classification_service.dart index efbe4ef..ca5e3a0 100644 --- a/mobile_TA/padi_app/lib/services/classification_service.dart +++ b/mobile_TA/padi_app/lib/services/classification_service.dart @@ -20,7 +20,15 @@ class ClassificationService { : _httpClient = httpClient ?? http.Client(); /// Klasifikasi gambar dari file - Future classifyImage(File imageFile) async { + Future classifyImage( + File imageFile, { + String? locationAddress, + double? locationLat, + double? locationLng, + String? kabupaten, + String? kecamatan, + String? kelurahan, + }) async { try { final request = http.MultipartRequest('POST', Uri.parse('$_baseUrl/classify')); request.headers['Accept'] = 'application/json'; @@ -39,6 +47,25 @@ class ClassificationService { await http.MultipartFile.fromPath('image', imageFile.path), ); + if (locationAddress != null && locationAddress.isNotEmpty) { + request.fields['location_address'] = locationAddress; + } + if (locationLat != null) { + request.fields['location_lat'] = locationLat.toString(); + } + if (locationLng != null) { + request.fields['location_lng'] = locationLng.toString(); + } + if (kabupaten != null && kabupaten.isNotEmpty) { + request.fields['kabupaten'] = kabupaten; + } + if (kecamatan != null && kecamatan.isNotEmpty) { + request.fields['kecamatan'] = kecamatan; + } + if (kelurahan != null && kelurahan.isNotEmpty) { + request.fields['kelurahan'] = kelurahan; + } + final response = await request.send().timeout( const Duration(seconds: 30), onTimeout: () { @@ -87,6 +114,9 @@ class ClassificationService { String? locationAddress, double? locationLat, double? locationLng, + String? kabupaten, + String? kecamatan, + String? kelurahan, }) async { try { final request = http.MultipartRequest( @@ -118,6 +148,15 @@ class ClassificationService { if (locationLng != null) { request.fields['location_lng'] = locationLng.toString(); } + if (kabupaten != null && kabupaten.isNotEmpty) { + request.fields['kabupaten'] = kabupaten; + } + if (kecamatan != null && kecamatan.isNotEmpty) { + request.fields['kecamatan'] = kecamatan; + } + if (kelurahan != null && kelurahan.isNotEmpty) { + request.fields['kelurahan'] = kelurahan; + } final response = await request.send().timeout( const Duration(seconds: 30), @@ -214,6 +253,10 @@ class ClassificationResult { final DiseaseInfo diseaseInfo; final String? imagePath; final String? notes; + final String? locationAddress; + final String? kabupaten; + final String? kecamatan; + final String? kelurahan; final DateTime timestamp; ClassificationResult({ @@ -225,6 +268,10 @@ class ClassificationResult { required this.timestamp, this.imagePath, this.notes, + this.locationAddress, + this.kabupaten, + this.kecamatan, + this.kelurahan, }); factory ClassificationResult.fromJson(Map json) { @@ -240,6 +287,10 @@ class ClassificationResult { ), imagePath: json['image_path'], notes: json['notes'], + locationAddress: json['location_address'], + kabupaten: json['kabupaten'], + kecamatan: json['kecamatan'], + kelurahan: json['kelurahan'], timestamp: DateTime.parse( json['timestamp'] ?? DateTime.now().toIso8601String(), ), @@ -283,6 +334,9 @@ class ClassificationHistoryItem { final String? locationAddress; final double? locationLat; final double? locationLng; + final String? kabupaten; + final String? kecamatan; + final String? kelurahan; final DateTime createdAt; ClassificationHistoryItem({ @@ -296,6 +350,9 @@ class ClassificationHistoryItem { this.locationAddress, this.locationLat, this.locationLng, + this.kabupaten, + this.kecamatan, + this.kelurahan, }); factory ClassificationHistoryItem.fromJson(Map json) { @@ -320,6 +377,9 @@ class ClassificationHistoryItem { locationAddress: json['location_address'], locationLat: locationLat, locationLng: locationLng, + kabupaten: json['kabupaten'], + kecamatan: json['kecamatan'], + kelurahan: json['kelurahan'], createdAt: DateTime.parse( json['created_at'] ?? DateTime.now().toIso8601String(), ), diff --git a/mobile_TA/padi_app/pubspec.lock b/mobile_TA/padi_app/pubspec.lock index f9d648f..22eeb8d 100644 --- a/mobile_TA/padi_app/pubspec.lock +++ b/mobile_TA/padi_app/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" fake_async: dependency: transitive description: @@ -129,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79" + url: "https://pub.dev" + source: hosted + version: "0.65.0" flutter: dependency: "direct main" description: flutter diff --git a/mobile_TA/padi_app/pubspec.yaml b/mobile_TA/padi_app/pubspec.yaml index cecf1c4..6ed9513 100644 --- a/mobile_TA/padi_app/pubspec.yaml +++ b/mobile_TA/padi_app/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: smooth_page_indicator: 1.1.0 shared_preferences: ^2.5.3 url_launcher: ^6.3.2 + fl_chart: ^0.65.0 dev_dependencies: flutter_test: diff --git a/web_TA/app/Http/Controllers/ClassificationController.php b/web_TA/app/Http/Controllers/ClassificationController.php index 9504ae4..a120a5d 100644 --- a/web_TA/app/Http/Controllers/ClassificationController.php +++ b/web_TA/app/Http/Controllers/ClassificationController.php @@ -32,6 +32,9 @@ public function classify(Request $request) 'location_address' => 'nullable|string|max:255', 'location_lat' => 'nullable|numeric', 'location_lng' => 'nullable|numeric', + 'kabupaten' => 'nullable|string|max:255', + 'kecamatan' => 'nullable|string|max:255', + 'kelurahan' => 'nullable|string|max:255', ]); // Ambil file gambar @@ -67,6 +70,15 @@ public function classify(Request $request) // Tambahkan informasi detail tentang penyakit $diseaseInfo = $this->getDiseaseInfo($result['predicted_class']); + // Extract location components from address if not provided + $locationAddress = $request->input('location_address'); + $locationLat = $request->input('location_lat'); + $locationLng = $request->input('location_lng'); + $locationComponents = $this->extractLocationComponents($locationAddress, $locationLat, $locationLng); + $kabupaten = $request->input('kabupaten') ?? $locationComponents['kabupaten']; + $kecamatan = $request->input('kecamatan') ?? $locationComponents['kecamatan']; + $kelurahan = $request->input('kelurahan') ?? $locationComponents['kelurahan']; + $savedToDatabase = true; $persistenceWarning = null; @@ -80,12 +92,12 @@ public function classify(Request $request) 'disease_name' => $diseaseInfo['name'], 'severity' => $diseaseInfo['severity'], 'notes' => 'Classification without storage', - 'location_address' => $request->input('location_address'), - 'location_lat' => $request->input('location_lat'), - 'location_lng' => $request->input('location_lng'), - 'location_address' => $request->input('location_address'), - 'location_lat' => $request->input('location_lat'), - 'location_lng' => $request->input('location_lng'), + 'location_address' => $locationAddress, + 'location_lat' => $locationLat, + 'location_lng' => $locationLng, + 'kabupaten' => $kabupaten, + 'kecamatan' => $kecamatan, + 'kelurahan' => $kelurahan, ]); } catch (\Throwable $dbException) { $savedToDatabase = false; @@ -141,6 +153,9 @@ public function classifyAndSave(Request $request) 'location_address' => 'nullable|string|max:255', 'location_lat' => 'nullable|numeric', 'location_lng' => 'nullable|numeric', + 'kabupaten' => 'nullable|string|max:255', + 'kecamatan' => 'nullable|string|max:255', + 'kelurahan' => 'nullable|string|max:255', ]); // Simpan gambar @@ -194,6 +209,15 @@ public function classifyAndSave(Request $request) $diseaseInfo = $this->getDiseaseInfo($result['predicted_class']); + // Extract location components from address if not provided + $locationAddress = $request->input('location_address'); + $locationLat = $request->input('location_lat'); + $locationLng = $request->input('location_lng'); + $locationComponents = $this->extractLocationComponents($locationAddress, $locationLat, $locationLng); + $kabupaten = $request->input('kabupaten') ?? $locationComponents['kabupaten']; + $kecamatan = $request->input('kecamatan') ?? $locationComponents['kecamatan']; + $kelurahan = $request->input('kelurahan') ?? $locationComponents['kelurahan']; + $savedToDatabase = true; $persistenceWarning = null; @@ -208,9 +232,12 @@ public function classifyAndSave(Request $request) 'disease_name' => $diseaseInfo['name'], 'severity' => $diseaseInfo['severity'], 'notes' => $request->input('notes'), - 'location_address' => $request->input('location_address'), - 'location_lat' => $request->input('location_lat'), - 'location_lng' => $request->input('location_lng'), + 'location_address' => $locationAddress, + 'location_lat' => $locationLat, + 'location_lng' => $locationLng, + 'kabupaten' => $kabupaten, + 'kecamatan' => $kecamatan, + 'kelurahan' => $kelurahan, ]); } catch (\Throwable $dbException) { $savedToDatabase = false; @@ -257,9 +284,12 @@ public function classifyAndSave(Request $request) ClassificationHistory::create([ 'user_id' => $historyUserId, 'jenis_penyakit' => $diseaseInfo['name'] ?? $result['predicted_class'], - 'location_address' => $request->input('location_address'), + 'location_address' => $locationAddress, 'location_lat' => $request->input('location_lat'), 'location_lng' => $request->input('location_lng'), + 'kabupaten' => $kabupaten, + 'kecamatan' => $kecamatan, + 'kelurahan' => $kelurahan, ]); } catch (\Throwable $historyException) { Log::warning('Classification history not persisted', [ @@ -443,6 +473,57 @@ public function testConnection() return $this->health(); } + /** + * Extract kabupaten, kecamatan, kelurahan dari location_address string + * Format Indonesia: "Kelurahan, Kecamatan ..., Kabupaten ..., Provinsi, Negara" + * Contoh: "Sumbersari, Kecamatan Sumbersari, Kabupaten Jember, Jawa Timur, Indonesia" + */ + private function extractLocationComponents($locationAddress, $lat = null, $lng = null) + { + $components = [ + 'kabupaten' => null, + 'kecamatan' => null, + 'kelurahan' => null, + ]; + + if (!$locationAddress) { + return $components; + } + + // Extract "Kabupaten [name]" atau "Kota [name]" + if (preg_match('/(?:Kabupaten|Kota)\s+([^,]+)/', $locationAddress, $matches)) { + $components['kabupaten'] = trim($matches[1]); + } + + // Extract "Kecamatan [name]" + if (preg_match('/Kecamatan\s+([^,]+)/', $locationAddress, $matches)) { + $components['kecamatan'] = trim($matches[1]); + } + + // Extract first part (before first comma) as kelurahan + $parts = explode(',', $locationAddress); + if (!empty($parts)) { + $kelurahan = trim($parts[0]); + if ($kelurahan && !preg_match('/^(Kecamatan|Kabupaten|Kota|Provinsi)/', $kelurahan)) { + $components['kelurahan'] = $kelurahan; + } + } + + // Fallback: If kabupaten not found in address, use lat/lng to determine it + // For Jember area (coordinates around -8.1656155, 113.7212891) + if (!$components['kabupaten'] && $lat && $lng) { + $lat = (float) $lat; + $lng = (float) $lng; + + // Jember region boundaries (approximate) + if ($lat >= -8.35 && $lat <= -7.85 && $lng >= 113.50 && $lng <= 114.00) { + $components['kabupaten'] = 'Jember'; + } + } + + return $components; + } + /** * Mendapatkan informasi detail tentang penyakit */ diff --git a/web_TA/app/Http/Controllers/DashboardController.php b/web_TA/app/Http/Controllers/DashboardController.php index fd3e08e..532cabd 100644 --- a/web_TA/app/Http/Controllers/DashboardController.php +++ b/web_TA/app/Http/Controllers/DashboardController.php @@ -30,13 +30,31 @@ public function index() $totalUsers = User::count(); $recentUsers = User::orderByDesc('created_at')->take(10)->get(); + // Statistik penyakit per kabupaten + $diseaseByKabupaten = \App\Models\Classification::selectRaw('kabupaten, predicted_class, count(*) as total') + ->whereNotNull('kabupaten') + ->groupBy('kabupaten', 'predicted_class') + ->orderBy('kabupaten') + ->orderByDesc('total') + ->get(); + + // Format data untuk chart + $kabupatenDiseaseData = []; + foreach ($diseaseByKabupaten as $record) { + if (!isset($kabupatenDiseaseData[$record->kabupaten])) { + $kabupatenDiseaseData[$record->kabupaten] = []; + } + $kabupatenDiseaseData[$record->kabupaten][$record->predicted_class] = $record->total; + } + return view('dashboard', compact( 'totalDataset', 'diseaseStats', 'mostCommonDisease', 'totalProducts', 'totalUsers', - 'recentUsers' + 'recentUsers', + 'kabupatenDiseaseData' )); } } diff --git a/web_TA/app/Models/Classification.php b/web_TA/app/Models/Classification.php index ed44bd6..ecc86a0 100644 --- a/web_TA/app/Models/Classification.php +++ b/web_TA/app/Models/Classification.php @@ -19,6 +19,9 @@ class Classification extends Model 'location_address', 'location_lat', 'location_lng', + 'kabupaten', + 'kecamatan', + 'kelurahan', ]; protected $casts = [ diff --git a/web_TA/app/Models/ClassificationHistory.php b/web_TA/app/Models/ClassificationHistory.php index deb4654..b81cb49 100644 --- a/web_TA/app/Models/ClassificationHistory.php +++ b/web_TA/app/Models/ClassificationHistory.php @@ -16,6 +16,9 @@ class ClassificationHistory extends Model 'location_address', 'location_lat', 'location_lng', + 'kabupaten', + 'kecamatan', + 'kelurahan', ]; protected $casts = [ diff --git a/web_TA/database/migrations/2026_05_12_add_kabupaten_to_classifications.php b/web_TA/database/migrations/2026_05_12_add_kabupaten_to_classifications.php new file mode 100644 index 0000000..b657548 --- /dev/null +++ b/web_TA/database/migrations/2026_05_12_add_kabupaten_to_classifications.php @@ -0,0 +1,40 @@ +string('kabupaten')->nullable()->after('location_address'); + $table->string('kecamatan')->nullable()->after('kabupaten'); + $table->string('kelurahan')->nullable()->after('kecamatan'); + }); + + Schema::table('classification_histories', function (Blueprint $table) { + $table->string('kabupaten')->nullable()->after('location_address'); + $table->string('kecamatan')->nullable()->after('kabupaten'); + $table->string('kelurahan')->nullable()->after('kecamatan'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('classifications', function (Blueprint $table) { + $table->dropColumn(['kabupaten', 'kecamatan', 'kelurahan']); + }); + + Schema::table('classification_histories', function (Blueprint $table) { + $table->dropColumn(['kabupaten', 'kecamatan', 'kelurahan']); + }); + } +}; diff --git a/web_TA/resources/views/dashboard.blade.php b/web_TA/resources/views/dashboard.blade.php index d2878fe..4276440 100644 --- a/web_TA/resources/views/dashboard.blade.php +++ b/web_TA/resources/views/dashboard.blade.php @@ -222,4 +222,56 @@ function toggleUserList() { }); + +
+

🗺️ Distribusi Penyakit Per Kabupaten

+ + @if(count($kabupatenDiseaseData) > 0) +
+ + + + + + + + + + + + @foreach($kabupatenDiseaseData as $kabupaten => $diseases) + + + + + + + + @endforeach + +
KabupatenBacterial BlightBrown SpotLeaf SmutTotal
{{ $kabupaten ?? 'Tidak Diketahui' }} + + {{ $diseases['Bacterialblight'] ?? 0 }} + + + + {{ $diseases['Brownspot'] ?? 0 }} + + + + {{ $diseases['Leafsmut'] ?? 0 }} + + + + {{ array_sum($diseases) }} + +
+
+ @else +
+

Belum ada data klasifikasi dengan informasi kabupaten

+
+ @endif +
+ @endsection \ No newline at end of file