memperbaiki lokasi dan beberapa fitur

This commit is contained in:
rijalhabibullah 2026-05-13 00:20:14 +07:00
parent f2967da073
commit fd6ee27287
10 changed files with 451 additions and 41 deletions

View File

@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; // Penting untuk kIsWeb
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:geocoding/geocoding.dart'; import 'package:geocoding/geocoding.dart';
import 'package:shared_preferences/shared_preferences.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/ // Pastikan file result_screen.dart sudah kamu buat di folder lib/screen/
import 'screen/result_screen.dart'; import 'screen/result_screen.dart';
@ -25,6 +26,46 @@ class _HomeScreenState extends State<HomeScreen> {
bool _isLoading = false; bool _isLoading = false;
String? _errorMessage; String? _errorMessage;
// Daftar Kabupaten/Kota di Indonesia (mapping Kecamatan -> Kabupaten)
final Map<String, String> _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<void> _getImage(ImageSource source) async { Future<void> _getImage(ImageSource source) async {
final pickedFile = await _picker.pickImage( final pickedFile = await _picker.pickImage(
source: source, source: source,
@ -32,13 +73,16 @@ class _HomeScreenState extends State<HomeScreen> {
maxWidth: 1280, maxWidth: 1280,
); );
if (pickedFile != null) { if (pickedFile != null) {
setState(() {
if (kIsWeb) { if (kIsWeb) {
setState(() {
_webImage = pickedFile; _webImage = pickedFile;
} else {
_image = File(pickedFile.path);
}
}); });
} else {
final compressed = await compressImageFile(File(pickedFile.path));
setState(() {
_image = compressed;
});
}
} }
} }
@ -194,35 +238,58 @@ class _HomeScreenState extends State<HomeScreen> {
final provinsi = place.administrativeArea?.trim(); // Provinsi final provinsi = place.administrativeArea?.trim(); // Provinsi
final negara = place.country?.trim(); // Negara 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 // Build full address: kelurahan, kecamatan, kabupaten, provinsi, negara
// Note: subAdministrativeArea (kabupaten) tidak selalu tersedia di geocoding Indonesia
final parts = [ final parts = [
kelurahan, kelurahan,
kecamatan, kecamatan,
kabupaten,
provinsi, provinsi,
negara, negara,
].where((part) => part != null && part!.isNotEmpty) ].where((part) => part != null && part!.isNotEmpty).toList();
.toList();
if (parts.isNotEmpty) { if (parts.isNotEmpty) {
address = parts.join(', '); 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) { if (address != null) {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setString('user_location', address); 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_lat', position.latitude);
await prefs.setDouble('user_location_lng', position.longitude); await prefs.setDouble('user_location_lng', position.longitude);
} }

View File

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import '../utils/image_compress.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@ -19,8 +20,9 @@ class _HomeScreenState extends State<HomeScreen> {
final pickedFile = await _picker.pickImage(source: source); final pickedFile = await _picker.pickImage(source: source);
if (pickedFile != null) { if (pickedFile != null) {
final compressed = await compressImageFile(File(pickedFile.path));
setState(() { setState(() {
_image = File(pickedFile.path); _image = compressed;
}); });
} }
} }

View File

@ -40,7 +40,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
} }
Future<void> _tryAutoFillLocation() async { Future<void> _tryAutoFillLocation() async {
if (_userLocation.isNotEmpty) { if (_userLocation.isNotEmpty && _userKabupaten.isNotEmpty) {
return; return;
} }
@ -51,13 +51,60 @@ class _ProfileScreenState extends State<ProfileScreen> {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setString('user_location', address); await prefs.setString('user_location', address);
if (mounted) { if (mounted) {
setState(() { setState(() {
_userLocation = address; _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<String, String> _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<String?> _fetchCurrentAddress() async { Future<String?> _fetchCurrentAddress() async {
final serviceEnabled = await Geolocator.isLocationServiceEnabled(); final serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) { if (!serviceEnabled) {
@ -91,40 +138,64 @@ class _ProfileScreenState extends State<ProfileScreen> {
// Extract components dengan urutan yang sesuai untuk Indonesia // Extract components dengan urutan yang sesuai untuk Indonesia
final kelurahan = place.subLocality?.trim(); // Kelurahan/Desa 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 provinsi = place.administrativeArea?.trim(); // Provinsi
final negara = place.country?.trim(); // Negara final negara = place.country?.trim(); // Negara
// Build full address: kelurahan, kecamatan, kabupaten, provinsi, negara // Extract kabupaten - try multiple sources
final parts = [ 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, kelurahan,
kecamatan, kecamatan,
kabupaten,
provinsi, provinsi,
negara, negara,
].where((part) => part != null && part!.isNotEmpty) ].where((part) => part != null && part!.isNotEmpty)
.toList(); .toList();
if (parts.isEmpty) { if (addressParts.isEmpty) {
return null; return null;
} }
final address = parts.join(', '); final address = addressParts.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 // Save to shared preferences
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setString('user_location', address); await prefs.setString('user_location', address);
if (kelurahan != null) await prefs.setString('user_kelurahan', kelurahan); if (kelurahan != null) await prefs.setString('user_kelurahan', kelurahan);
if (kecamatan != null) await prefs.setString('user_kecamatan', kecamatan); 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; return address;
} }

View File

@ -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<File> 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);
}

View File

@ -6,13 +6,17 @@ import FlutterMacOS
import Foundation import Foundation
import file_selector_macos import file_selector_macos
import flutter_image_compress_macos
import geolocator_apple import geolocator_apple
import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
import url_launcher_macos import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@ -150,6 +150,54 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -408,6 +456,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" 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: path_provider_linux:
dependency: transitive dependency: transitive
description: description:

View File

@ -43,6 +43,8 @@ dependencies:
shared_preferences: ^2.5.3 shared_preferences: ^2.5.3
url_launcher: ^6.3.2 url_launcher: ^6.3.2
fl_chart: ^0.65.0 fl_chart: ^0.65.0
flutter_image_compress: ^2.3.0
path_provider: ^2.1.4
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -33,6 +33,10 @@ public function index()
// Statistik penyakit per kabupaten // Statistik penyakit per kabupaten
$diseaseByKabupaten = \App\Models\Classification::selectRaw('kabupaten, predicted_class, count(*) as total') $diseaseByKabupaten = \App\Models\Classification::selectRaw('kabupaten, predicted_class, count(*) as total')
->whereNotNull('kabupaten') ->whereNotNull('kabupaten')
->where(function ($query) {
$query->where('kabupaten', 'like', 'Kabupaten %')
->orWhere('kabupaten', 'like', 'Kota %');
})
->groupBy('kabupaten', 'predicted_class') ->groupBy('kabupaten', 'predicted_class')
->orderBy('kabupaten') ->orderBy('kabupaten')
->orderByDesc('total') ->orderByDesc('total')
@ -47,6 +51,18 @@ public function index()
$kabupatenDiseaseData[$record->kabupaten][$record->predicted_class] = $record->total; $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( return view('dashboard', compact(
'totalDataset', 'totalDataset',
'diseaseStats', 'diseaseStats',
@ -54,7 +70,8 @@ public function index()
'totalProducts', 'totalProducts',
'totalUsers', 'totalUsers',
'recentUsers', 'recentUsers',
'kabupatenDiseaseData' 'kabupatenDiseaseData',
'usersByKabupaten'
)); ));
} }
} }

View File

@ -3,6 +3,7 @@
use App\Models\Product; use App\Models\Product;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class ProductController extends Controller class ProductController extends Controller
{ {
@ -21,7 +22,8 @@ public function store(Request $request) {
$imagePath = null; $imagePath = null;
if ($request->hasFile('image')) { 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([ Product::create([
@ -61,4 +63,73 @@ public function apiIndex(Request $request) {
'data' => $products, '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;
}
} }

View File

@ -106,7 +106,7 @@
</div> </div>
<!-- Grafik dan Detail Penyakit --> <!-- Grafik dan Detail Penyakit -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Grafik Distribusi Penyakit --> <!-- Grafik Distribusi Penyakit -->
<div class="lg:col-span-2 bg-white p-6 rounded-2xl shadow-sm border border-gray-100"> <div class="lg:col-span-2 bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
@ -117,6 +117,19 @@
<p class="text-xs text-gray-500 mt-4">Diagram menunjukkan jumlah citra setiap jenis penyakit yang tersimpan di database untuk training AI</p> <p class="text-xs text-gray-500 mt-4">Diagram menunjukkan jumlah citra setiap jenis penyakit yang tersimpan di database untuk training AI</p>
</div> </div>
<!-- Pie Chart User Terbanyak Per Kabupaten -->
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
<h3 class="text-lg font-bold text-gray-800 mb-4">👥 User Terbanyak per Kabupaten</h3>
@if($usersByKabupaten->count() > 0)
<div class="h-56 md:h-64">
<canvas id="userKabupatenPie"></canvas>
</div>
<p class="text-xs text-gray-500 mt-4">Persentase user berdasarkan kabupaten (dari data klasifikasi)</p>
@else
<p class="text-gray-500 text-sm">Belum ada data user per kabupaten.</p>
@endif
</div>
<!-- Daftar Detail Penyakit --> <!-- Daftar Detail Penyakit -->
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100"> <div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
<h3 class="text-lg font-bold text-gray-800 mb-4">📋 Detail Data Penyakit</h3> <h3 class="text-lg font-bold text-gray-800 mb-4">📋 Detail Data Penyakit</h3>
@ -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
</script> </script>
<!-- Statistik Penyakit Per Kabupaten --> <!-- Statistik Penyakit Per Kabupaten -->