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: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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 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"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 -->
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue