memperbaiki lokasi dan beberapa fitur
This commit is contained in:
parent
f2967da073
commit
fd6ee27287
|
|
@ -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<HomeScreen> {
|
|||
bool _isLoading = false;
|
||||
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 {
|
||||
final pickedFile = await _picker.pickImage(
|
||||
source: source,
|
||||
|
|
@ -32,13 +73,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
maxWidth: 1280,
|
||||
);
|
||||
if (pickedFile != null) {
|
||||
setState(() {
|
||||
if (kIsWeb) {
|
||||
setState(() {
|
||||
_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 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HomeScreen> {
|
|||
final pickedFile = await _picker.pickImage(source: source);
|
||||
|
||||
if (pickedFile != null) {
|
||||
final compressed = await compressImageFile(File(pickedFile.path));
|
||||
setState(() {
|
||||
_image = File(pickedFile.path);
|
||||
_image = compressed;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
}
|
||||
|
||||
Future<void> _tryAutoFillLocation() async {
|
||||
if (_userLocation.isNotEmpty) {
|
||||
if (_userLocation.isNotEmpty && _userKabupaten.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -51,13 +51,60 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
|
||||
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<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 {
|
||||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
|
|
@ -91,40 +138,64 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
|
||||
// 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
</div>
|
||||
|
||||
<!-- 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 -->
|
||||
<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>
|
||||
</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 -->
|
||||
<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>
|
||||
|
|
@ -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>
|
||||
|
||||
<!-- Statistik Penyakit Per Kabupaten -->
|
||||
|
|
|
|||
Loading…
Reference in New Issue