diff --git a/mobile_TA/padi_app/lib/dashboard_klasifikasi.dart b/mobile_TA/padi_app/lib/dashboard_klasifikasi.dart index 5631517..efd7098 100644 --- a/mobile_TA/padi_app/lib/dashboard_klasifikasi.dart +++ b/mobile_TA/padi_app/lib/dashboard_klasifikasi.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; // Penting untuk kIsWeb import 'package:geolocator/geolocator.dart'; import 'package:geocoding/geocoding.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'utils/image_compress.dart'; // Pastikan file result_screen.dart sudah kamu buat di folder lib/screen/ import 'screen/result_screen.dart'; @@ -25,6 +26,46 @@ class _HomeScreenState extends State { bool _isLoading = false; String? _errorMessage; + // Daftar Kabupaten/Kota di Indonesia (mapping Kecamatan -> Kabupaten) + final Map _districtToRegencyMap = { + // Jawa Timur - Jember + 'Sumbersari': 'Kabupaten Jember', + 'Ambulu': 'Kabupaten Jember', + 'Ajung': 'Kabupaten Jember', + 'Mayang': 'Kabupaten Jember', + 'Mumbulsari': 'Kabupaten Jember', + 'Kalisat': 'Kabupaten Jember', + 'Jenggawah': 'Kabupaten Jember', + 'Sukorambi': 'Kabupaten Jember', + 'Rambipuji': 'Kabupaten Jember', + 'Silo': 'Kabupaten Jember', + 'Kencong': 'Kabupaten Jember', + 'Puger': 'Kabupaten Jember', + 'Wuluhan': 'Kabupaten Jember', + 'Tempeh': 'Kabupaten Jember', + 'Ledokombo': 'Kabupaten Jember', + 'Panti': 'Kabupaten Jember', + 'Pakusari': 'Kabupaten Jember', + 'Tanggul': 'Kabupaten Jember', + + // Jawa Timur - Surabaya + 'Surabaya': 'Kota Surabaya', + 'Gubeng': 'Kota Surabaya', + 'Rungkut': 'Kota Surabaya', + + // Jawa Timur - Malang + 'Malang': 'Kota Malang', + + // Jawa Timur - Batu + 'Batu': 'Kota Batu', + + // Jawa Tengah - Semarang + 'Semarang': 'Kota Semarang', + + // Jawa Barat - Bandung + 'Bandung': 'Kota Bandung', + }; + Future _getImage(ImageSource source) async { final pickedFile = await _picker.pickImage( source: source, @@ -32,13 +73,16 @@ class _HomeScreenState extends State { maxWidth: 1280, ); if (pickedFile != null) { - setState(() { - if (kIsWeb) { + if (kIsWeb) { + setState(() { _webImage = pickedFile; - } else { - _image = File(pickedFile.path); - } - }); + }); + } else { + final compressed = await compressImageFile(File(pickedFile.path)); + setState(() { + _image = compressed; + }); + } } } @@ -193,36 +237,59 @@ class _HomeScreenState extends State { final provinsi = place.administrativeArea?.trim(); // Provinsi final negara = place.country?.trim(); // Negara - + + // Extract kabupaten - try multiple sources + if (place.subAdministrativeArea != null && + place.subAdministrativeArea!.isNotEmpty) { + kabupaten = place.subAdministrativeArea!.trim(); + } + + if (kabupaten == null && + place.thoroughfare != null && + place.thoroughfare!.isNotEmpty) { + final match = + RegExp(r'(?:Kabupaten|Kota)\s+([^,]+)').firstMatch(place.thoroughfare!); + if (match != null) { + kabupaten = match.group(1)?.trim(); + } + } + + if (kabupaten == null && kecamatan != null) { + kabupaten = _districtToRegencyMap[kecamatan]; + } + + if (kabupaten != null && + !kabupaten.contains('Kabupaten') && + !kabupaten.contains('Kota')) { + kabupaten = 'Kabupaten $kabupaten'; + } + // Build full address: kelurahan, kecamatan, kabupaten, provinsi, negara - // Note: subAdministrativeArea (kabupaten) tidak selalu tersedia di geocoding Indonesia final parts = [ kelurahan, kecamatan, + kabupaten, provinsi, negara, - ].where((part) => part != null && part!.isNotEmpty) - .toList(); + ].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) { final prefs = await SharedPreferences.getInstance(); await prefs.setString('user_location', address); + if (kabupaten != null) { + await prefs.setString('user_kabupaten', kabupaten); + } + if (kecamatan != null) { + await prefs.setString('user_kecamatan', kecamatan); + } + if (kelurahan != null) { + await prefs.setString('user_kelurahan', kelurahan); + } await prefs.setDouble('user_location_lat', position.latitude); await prefs.setDouble('user_location_lng', position.longitude); } diff --git a/mobile_TA/padi_app/lib/screen/home_screen.dart b/mobile_TA/padi_app/lib/screen/home_screen.dart index cf14c55..60d23ef 100644 --- a/mobile_TA/padi_app/lib/screen/home_screen.dart +++ b/mobile_TA/padi_app/lib/screen/home_screen.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; +import '../utils/image_compress.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -19,8 +20,9 @@ class _HomeScreenState extends State { final pickedFile = await _picker.pickImage(source: source); if (pickedFile != null) { + final compressed = await compressImageFile(File(pickedFile.path)); setState(() { - _image = File(pickedFile.path); + _image = compressed; }); } } diff --git a/mobile_TA/padi_app/lib/screen/profile_screen.dart b/mobile_TA/padi_app/lib/screen/profile_screen.dart index 622cf6a..d4a9196 100644 --- a/mobile_TA/padi_app/lib/screen/profile_screen.dart +++ b/mobile_TA/padi_app/lib/screen/profile_screen.dart @@ -40,7 +40,7 @@ class _ProfileScreenState extends State { } Future _tryAutoFillLocation() async { - if (_userLocation.isNotEmpty) { + if (_userLocation.isNotEmpty && _userKabupaten.isNotEmpty) { return; } @@ -51,13 +51,60 @@ class _ProfileScreenState extends State { final prefs = await SharedPreferences.getInstance(); await prefs.setString('user_location', address); + if (mounted) { setState(() { _userLocation = address; + // Load all location details from prefs + _userKabupaten = prefs.getString('user_kabupaten') ?? ''; + _userKecamatan = prefs.getString('user_kecamatan') ?? ''; + _userKelurahan = prefs.getString('user_kelurahan') ?? ''; }); } } + // Daftar Kabupaten/Kota di Indonesia (mapping Kecamatan -> Kabupaten) + final Map _districtToRegencyMap = { + // Jawa Timur - Jember + 'Sumbersari': 'Kabupaten Jember', + 'Ambulu': 'Kabupaten Jember', + 'Ajung': 'Kabupaten Jember', + 'Mayang': 'Kabupaten Jember', + 'Mumbulsari': 'Kabupaten Jember', + 'Kalisat': 'Kabupaten Jember', + 'Jenggawah': 'Kabupaten Jember', + 'Sukorambi': 'Kabupaten Jember', + 'Rambipuji': 'Kabupaten Jember', + 'Silo': 'Kabupaten Jember', + 'Kencong': 'Kabupaten Jember', + 'Puger': 'Kabupaten Jember', + 'Wuluhan': 'Kabupaten Jember', + 'Tempeh': 'Kabupaten Jember', + 'Ledokombo': 'Kabupaten Jember', + 'Panti': 'Kabupaten Jember', + 'Pakusari': 'Kabupaten Jember', + 'Tanggul': 'Kabupaten Jember', + + // Jawa Timur - Surabaya + 'Surabaya': 'Kota Surabaya', + 'Gubeng': 'Kota Surabaya', + 'Rungkut': 'Kota Surabaya', + + // Jawa Timur - Malang + 'Malang': 'Kota Malang', + + // Jawa Timur - Batu + 'Batu': 'Kota Batu', + + // Jawa Tengah - Semarang + 'Semarang': 'Kota Semarang', + + // Jawa Barat - Bandung + 'Bandung': 'Kota Bandung', + + // Tambahkan mapping untuk kabupaten/kota lain sesuai kebutuhan + }; + Future _fetchCurrentAddress() async { final serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { @@ -91,40 +138,64 @@ class _ProfileScreenState extends State { // Extract components dengan urutan yang sesuai untuk Indonesia final kelurahan = place.subLocality?.trim(); // Kelurahan/Desa - final kecamatan = place.locality?.trim(); // Kecamatan + final kecamatan = place.locality?.trim(); // Kecamatan/Kota final provinsi = place.administrativeArea?.trim(); // Provinsi final negara = place.country?.trim(); // Negara - // Build full address: kelurahan, kecamatan, kabupaten, provinsi, negara - final parts = [ + // Extract kabupaten - try multiple sources + String? kabupaten; + + // Method 1: Try subAdministrativeArea first (most reliable) + if (place.subAdministrativeArea != null && place.subAdministrativeArea!.isNotEmpty) { + kabupaten = place.subAdministrativeArea!.trim(); + } + + // Method 2: Try to find in thoroughfare + if (kabupaten == null && place.thoroughfare != null && place.thoroughfare!.isNotEmpty) { + final match = RegExp(r'(?:Kabupaten|Kota)\s+([^,]+)').firstMatch(place.thoroughfare!); + if (match != null) { + kabupaten = match.group(1)?.trim(); + } + } + + // Method 3: Use mapping dictionary based on kecamatan + if (kabupaten == null && kecamatan != null) { + kabupaten = _districtToRegencyMap[kecamatan]; + } + + if (kabupaten != null && + !kabupaten.contains('Kabupaten') && + !kabupaten.contains('Kota')) { + kabupaten = 'Kabupaten $kabupaten'; + } + + // Build address + final addressParts = [ kelurahan, kecamatan, + kabupaten, provinsi, negara, ].where((part) => part != null && part!.isNotEmpty) .toList(); - if (parts.isEmpty) { + if (addressParts.isEmpty) { return null; } - 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(); - } + final address = addressParts.join(', '); // 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); + if (kabupaten != null) { + await prefs.setString('user_kabupaten', kabupaten); + } else if (provinsi != null) { + // Default: use provinsi jika kabupaten tidak ditemukan + await prefs.setString('user_kabupaten', provinsi); + } return address; } diff --git a/mobile_TA/padi_app/lib/utils/image_compress.dart b/mobile_TA/padi_app/lib/utils/image_compress.dart new file mode 100644 index 0000000..299e23a --- /dev/null +++ b/mobile_TA/padi_app/lib/utils/image_compress.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:path_provider/path_provider.dart'; + +Future compressImageFile(File input, { + int minWidth = 1280, + int minHeight = 1280, + int quality = 75, + int maxBytes = 500 * 1024, +}) async { + if (kIsWeb) { + return input; + } + + final inputSize = await input.length(); + if (inputSize <= maxBytes) { + return input; + } + + final tempDir = await getTemporaryDirectory(); + final targetPath = + '${tempDir.path}/img_${DateTime.now().millisecondsSinceEpoch}.jpg'; + + final compressed = await FlutterImageCompress.compressAndGetFile( + input.path, + targetPath, + quality: quality, + minWidth: minWidth, + minHeight: minHeight, + format: CompressFormat.jpeg, + ); + + if (compressed == null) { + return input; + } + + return File(compressed.path); +} diff --git a/mobile_TA/padi_app/macos/Flutter/GeneratedPluginRegistrant.swift b/mobile_TA/padi_app/macos/Flutter/GeneratedPluginRegistrant.swift index e6cd7c1..445f5d4 100644 --- a/mobile_TA/padi_app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/mobile_TA/padi_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,13 +6,17 @@ import FlutterMacOS import Foundation import file_selector_macos +import flutter_image_compress_macos import geolocator_apple +import path_provider_foundation import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/mobile_TA/padi_app/pubspec.lock b/mobile_TA/padi_app/pubspec.lock index 22eeb8d..36a8707 100644 --- a/mobile_TA/padi_app/pubspec.lock +++ b/mobile_TA/padi_app/pubspec.lock @@ -150,6 +150,54 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_image_compress: + dependency: "direct main" + description: + name: flutter_image_compress + sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + flutter_image_compress_common: + dependency: transitive + description: + name: flutter_image_compress_common + sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb + url: "https://pub.dev" + source: hosted + version: "1.0.6" + flutter_image_compress_macos: + dependency: transitive + description: + name: flutter_image_compress_macos + sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_image_compress_ohos: + dependency: transitive + description: + name: flutter_image_compress_ohos + sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 + url: "https://pub.dev" + source: hosted + version: "0.0.3" + flutter_image_compress_platform_interface: + dependency: transitive + description: + name: flutter_image_compress_platform_interface + sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + flutter_image_compress_web: + dependency: transitive + description: + name: flutter_image_compress_web + sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96 + url: "https://pub.dev" + source: hosted + version: "0.1.5" flutter_lints: dependency: "direct dev" description: @@ -408,6 +456,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" path_provider_linux: dependency: transitive description: diff --git a/mobile_TA/padi_app/pubspec.yaml b/mobile_TA/padi_app/pubspec.yaml index 6ed9513..90f6d7b 100644 --- a/mobile_TA/padi_app/pubspec.yaml +++ b/mobile_TA/padi_app/pubspec.yaml @@ -43,6 +43,8 @@ dependencies: shared_preferences: ^2.5.3 url_launcher: ^6.3.2 fl_chart: ^0.65.0 + flutter_image_compress: ^2.3.0 + path_provider: ^2.1.4 dev_dependencies: flutter_test: diff --git a/web_TA/app/Http/Controllers/DashboardController.php b/web_TA/app/Http/Controllers/DashboardController.php index 532cabd..7b1295b 100644 --- a/web_TA/app/Http/Controllers/DashboardController.php +++ b/web_TA/app/Http/Controllers/DashboardController.php @@ -33,6 +33,10 @@ public function index() // Statistik penyakit per kabupaten $diseaseByKabupaten = \App\Models\Classification::selectRaw('kabupaten, predicted_class, count(*) as total') ->whereNotNull('kabupaten') + ->where(function ($query) { + $query->where('kabupaten', 'like', 'Kabupaten %') + ->orWhere('kabupaten', 'like', 'Kota %'); + }) ->groupBy('kabupaten', 'predicted_class') ->orderBy('kabupaten') ->orderByDesc('total') @@ -47,6 +51,18 @@ public function index() $kabupatenDiseaseData[$record->kabupaten][$record->predicted_class] = $record->total; } + // Statistik user terbanyak per kabupaten (berdasarkan klasifikasi) + $usersByKabupaten = \App\Models\Classification::selectRaw('kabupaten, count(distinct user_id) as total_users') + ->whereNotNull('kabupaten') + ->whereNotNull('user_id') + ->where(function ($query) { + $query->where('kabupaten', 'like', 'Kabupaten %') + ->orWhere('kabupaten', 'like', 'Kota %'); + }) + ->groupBy('kabupaten') + ->orderByDesc('total_users') + ->get(); + return view('dashboard', compact( 'totalDataset', 'diseaseStats', @@ -54,7 +70,8 @@ public function index() 'totalProducts', 'totalUsers', 'recentUsers', - 'kabupatenDiseaseData' + 'kabupatenDiseaseData', + 'usersByKabupaten' )); } } diff --git a/web_TA/app/Http/Controllers/ProductController.php b/web_TA/app/Http/Controllers/ProductController.php index 7012d8d..6741379 100644 --- a/web_TA/app/Http/Controllers/ProductController.php +++ b/web_TA/app/Http/Controllers/ProductController.php @@ -3,6 +3,7 @@ use App\Models\Product; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; class ProductController extends Controller { @@ -21,7 +22,8 @@ public function store(Request $request) { $imagePath = null; if ($request->hasFile('image')) { - $imagePath = $request->file('image')->store('products', 'public'); + $imagePath = $this->compressAndStoreProductImage($request->file('image')) + ?? $request->file('image')->store('products', 'public'); } Product::create([ @@ -61,4 +63,73 @@ public function apiIndex(Request $request) { 'data' => $products, ]); } + + private function compressAndStoreProductImage($file): ?string + { + if (!function_exists('imagecreatefromjpeg')) { + return null; + } + + $mime = $file->getMimeType(); + $sourcePath = $file->getRealPath(); + + if (!$sourcePath) { + return null; + } + + switch ($mime) { + case 'image/jpeg': + $image = imagecreatefromjpeg($sourcePath); + break; + case 'image/png': + $image = imagecreatefrompng($sourcePath); + break; + case 'image/webp': + if (!function_exists('imagecreatefromwebp')) { + return null; + } + $image = imagecreatefromwebp($sourcePath); + break; + default: + return null; + } + + if (!$image) { + return null; + } + + $maxWidth = 1280; + $quality = 75; + $width = imagesx($image); + $height = imagesy($image); + + if ($width > $maxWidth) { + $newHeight = (int) round($height * ($maxWidth / $width)); + $resized = imagecreatetruecolor($maxWidth, $newHeight); + $white = imagecolorallocate($resized, 255, 255, 255); + imagefill($resized, 0, 0, $white); + imagecopyresampled( + $resized, + $image, + 0, + 0, + 0, + 0, + $maxWidth, + $newHeight, + $width, + $height + ); + imagedestroy($image); + $image = $resized; + } + + $filename = 'products/' . Str::uuid() . '.jpg'; + $fullPath = storage_path('app/public/' . $filename); + + imagejpeg($image, $fullPath, $quality); + imagedestroy($image); + + return $filename; + } } \ No newline at end of file diff --git a/web_TA/resources/views/dashboard.blade.php b/web_TA/resources/views/dashboard.blade.php index 4276440..1dd6f48 100644 --- a/web_TA/resources/views/dashboard.blade.php +++ b/web_TA/resources/views/dashboard.blade.php @@ -106,7 +106,7 @@ -
+
@@ -117,6 +117,19 @@

Diagram menunjukkan jumlah citra setiap jenis penyakit yang tersimpan di database untuk training AI

+ +
+

👥 User Terbanyak per Kabupaten

+ @if($usersByKabupaten->count() > 0) +
+ +
+

Persentase user berdasarkan kabupaten (dari data klasifikasi)

+ @else +

Belum ada data user per kabupaten.

+ @endif +
+

📋 Detail Data Penyakit

@@ -220,6 +233,57 @@ function toggleUserList() { } } }); + + @if($usersByKabupaten->count() > 0) + const pieCtx = document.getElementById('userKabupatenPie').getContext('2d'); + const userKabupatenData = [ + @foreach($usersByKabupaten as $row) + {{ $row->total_users }}, + @endforeach + ]; + const userKabupatenLabels = [ + @foreach($usersByKabupaten as $row) + '{{ $row->kabupaten }}', + @endforeach + ]; + + new Chart(pieCtx, { + type: 'pie', + data: { + labels: userKabupatenLabels, + datasets: [{ + data: userKabupatenData, + backgroundColor: [ + '#10B981', + '#3B82F6', + '#F59E0B', + '#EF4444', + '#8B5CF6', + '#22C55E', + '#14B8A6', + '#F97316' + ] + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'bottom' }, + tooltip: { + callbacks: { + label: function(context) { + const total = userKabupatenData.reduce((a, b) => a + b, 0); + const value = context.parsed; + const percentage = total ? ((value / total) * 100).toFixed(1) : 0; + return context.label + ': ' + value + ' (' + percentage + '%)'; + } + } + } + } + } + }); + @endif