Fix laundry distance display with user location and cleanup mobile laundry model
This commit is contained in:
parent
b192557bb8
commit
a8e72f1203
|
|
@ -95,8 +95,8 @@ public function store(Request $request)
|
||||||
$startDate = Carbon::parse($request->tanggal_mulai);
|
$startDate = Carbon::parse($request->tanggal_mulai);
|
||||||
$endDate = $startDate->copy()->addMonths((int)$request->durasi_bulan);
|
$endDate = $startDate->copy()->addMonths((int)$request->durasi_bulan);
|
||||||
|
|
||||||
// Calculate total biaya
|
// Calculate total biaya from annual price pro-rated to monthly duration
|
||||||
$amount = $kontrakan->harga * (int)$request->durasi_bulan;
|
$amount = $kontrakan->harga * ((int)$request->durasi_bulan / 12);
|
||||||
|
|
||||||
$bookingData = [
|
$bookingData = [
|
||||||
'user_id' => $request->user()->id,
|
'user_id' => $request->user()->id,
|
||||||
|
|
@ -211,7 +211,7 @@ public function extend(Request $request, $id)
|
||||||
// Create new booking untuk perpanjangan
|
// Create new booking untuk perpanjangan
|
||||||
$startDate = Carbon::parse($booking->end_date);
|
$startDate = Carbon::parse($booking->end_date);
|
||||||
$endDate = $startDate->copy()->addMonths($request->durasi_bulan);
|
$endDate = $startDate->copy()->addMonths($request->durasi_bulan);
|
||||||
$amount = $booking->kontrakan->harga * $request->durasi_bulan;
|
$amount = $booking->kontrakan->harga * ($request->durasi_bulan / 12);
|
||||||
|
|
||||||
$newBooking = Booking::create([
|
$newBooking = Booking::create([
|
||||||
'user_id' => $request->user()->id,
|
'user_id' => $request->user()->id,
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ public function calculateKontrakan(Request $request)
|
||||||
public function calculateLaundry(Request $request)
|
public function calculateLaundry(Request $request)
|
||||||
{
|
{
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'jenis_layanan' => 'nullable|string|in:reguler,express,kilat',
|
'jenis_layanan' => 'nullable|string|in:reguler,express',
|
||||||
'harga_min' => 'nullable|numeric',
|
'harga_min' => 'nullable|numeric',
|
||||||
'harga_max' => 'nullable|numeric',
|
'harga_max' => 'nullable|numeric',
|
||||||
'jarak_max' => 'nullable|numeric',
|
'jarak_max' => 'nullable|numeric',
|
||||||
|
|
@ -220,7 +220,8 @@ public function calculateLaundry(Request $request)
|
||||||
// Filter by jenis_layanan if provided
|
// Filter by jenis_layanan if provided
|
||||||
if ($jenisLayanan) {
|
if ($jenisLayanan) {
|
||||||
$query->whereHas('layanan', function($q) use ($jenisLayanan) {
|
$query->whereHas('layanan', function($q) use ($jenisLayanan) {
|
||||||
$q->where('jenis_layanan', $jenisLayanan);
|
$values = $this->mapJenisLayananQueryValues($jenisLayanan);
|
||||||
|
$q->whereIn('jenis_layanan', $values);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -464,12 +465,25 @@ private function prosesMetodeSAWLaundry($items, $kriteria, $customBobot = null,
|
||||||
/**
|
/**
|
||||||
* Helper: Get nilai laundry berdasarkan nama kriteria
|
* Helper: Get nilai laundry berdasarkan nama kriteria
|
||||||
*/
|
*/
|
||||||
|
private function mapJenisLayananQueryValues($jenisLayanan)
|
||||||
|
{
|
||||||
|
$map = [
|
||||||
|
'reguler' => ['reguler', 'kiloan'],
|
||||||
|
'express' => ['express', 'satuan'],
|
||||||
|
];
|
||||||
|
|
||||||
|
return $map[$jenisLayanan] ?? [$jenisLayanan];
|
||||||
|
}
|
||||||
|
|
||||||
private function getNilaiLaundry($item, $field, $jenisLayanan = null)
|
private function getNilaiLaundry($item, $field, $jenisLayanan = null)
|
||||||
{
|
{
|
||||||
// Filter layanan by jenis if specified
|
// Filter layanan by jenis if specified
|
||||||
$layananCollection = $jenisLayanan
|
if ($jenisLayanan) {
|
||||||
? $item->layanan->where('jenis_layanan', $jenisLayanan)
|
$filterValues = $this->mapJenisLayananQueryValues($jenisLayanan);
|
||||||
: $item->layanan;
|
$layananCollection = $item->layanan->whereIn('jenis_layanan', $filterValues);
|
||||||
|
} else {
|
||||||
|
$layananCollection = $item->layanan;
|
||||||
|
}
|
||||||
|
|
||||||
switch ($field) {
|
switch ($field) {
|
||||||
case 'harga':
|
case 'harga':
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ public function map(Request $request)
|
||||||
'harga_min' => 'nullable|numeric|min:0',
|
'harga_min' => 'nullable|numeric|min:0',
|
||||||
'harga_max' => 'nullable|numeric|min:0|gte:harga_min',
|
'harga_max' => 'nullable|numeric|min:0|gte:harga_min',
|
||||||
'jarak' => 'nullable|in:dekat,sedang,jauh',
|
'jarak' => 'nullable|in:dekat,sedang,jauh',
|
||||||
'jenis_layanan' => 'nullable|in:express,reguler,kilat',
|
'jenis_layanan' => 'nullable|in:express,reguler',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$query = Laundry::with('layanan');
|
$query = Laundry::with('layanan');
|
||||||
|
|
@ -308,7 +308,7 @@ public function store(Request $request)
|
||||||
|
|
||||||
// Validasi Layanan
|
// Validasi Layanan
|
||||||
'layanan' => 'required|array|min:1',
|
'layanan' => 'required|array|min:1',
|
||||||
'layanan.*.jenis_layanan' => 'required|in:express,reguler,kilat',
|
'layanan.*.jenis_layanan' => 'required|in:express,reguler',
|
||||||
'layanan.*.nama_paket' => 'required|string|max:255',
|
'layanan.*.nama_paket' => 'required|string|max:255',
|
||||||
'layanan.*.harga' => 'required|numeric|min:0',
|
'layanan.*.harga' => 'required|numeric|min:0',
|
||||||
'layanan.*.estimasi_selesai' => 'required|numeric|min:1',
|
'layanan.*.estimasi_selesai' => 'required|numeric|min:1',
|
||||||
|
|
@ -327,7 +327,7 @@ public function store(Request $request)
|
||||||
'layanan.required' => 'Minimal harus ada 1 jenis layanan',
|
'layanan.required' => 'Minimal harus ada 1 jenis layanan',
|
||||||
'layanan.min' => 'Minimal harus ada 1 jenis layanan',
|
'layanan.min' => 'Minimal harus ada 1 jenis layanan',
|
||||||
'layanan.*.jenis_layanan.required' => 'Jenis layanan harus dipilih',
|
'layanan.*.jenis_layanan.required' => 'Jenis layanan harus dipilih',
|
||||||
'layanan.*.jenis_layanan.in' => 'Jenis layanan harus express, reguler, atau kilat',
|
'layanan.*.jenis_layanan.in' => 'Jenis layanan harus express atau reguler',
|
||||||
'layanan.*.nama_paket.required' => 'Nama paket harus diisi',
|
'layanan.*.nama_paket.required' => 'Nama paket harus diisi',
|
||||||
'layanan.*.harga.required' => 'Harga layanan harus diisi',
|
'layanan.*.harga.required' => 'Harga layanan harus diisi',
|
||||||
'layanan.*.harga.min' => 'Harga tidak boleh negatif',
|
'layanan.*.harga.min' => 'Harga tidak boleh negatif',
|
||||||
|
|
@ -504,7 +504,7 @@ public function update(Request $request, Laundry $laundry)
|
||||||
|
|
||||||
// Validasi Layanan
|
// Validasi Layanan
|
||||||
'layanan' => 'required|array|min:1',
|
'layanan' => 'required|array|min:1',
|
||||||
'layanan.*.jenis_layanan' => 'required|in:express,reguler,kilat',
|
'layanan.*.jenis_layanan' => 'required|in:express,reguler',
|
||||||
'layanan.*.nama_paket' => 'required|string|max:255',
|
'layanan.*.nama_paket' => 'required|string|max:255',
|
||||||
'layanan.*.harga' => 'required|numeric|min:0',
|
'layanan.*.harga' => 'required|numeric|min:0',
|
||||||
'layanan.*.estimasi_selesai' => 'required|numeric|min:1',
|
'layanan.*.estimasi_selesai' => 'required|numeric|min:1',
|
||||||
|
|
@ -519,7 +519,7 @@ public function update(Request $request, Laundry $laundry)
|
||||||
'layanan.required' => 'Minimal harus ada 1 jenis layanan',
|
'layanan.required' => 'Minimal harus ada 1 jenis layanan',
|
||||||
'layanan.min' => 'Minimal harus ada 1 jenis layanan',
|
'layanan.min' => 'Minimal harus ada 1 jenis layanan',
|
||||||
'layanan.*.jenis_layanan.required' => 'Jenis layanan harus dipilih',
|
'layanan.*.jenis_layanan.required' => 'Jenis layanan harus dipilih',
|
||||||
'layanan.*.jenis_layanan.in' => 'Jenis layanan harus express, reguler, atau kilat',
|
'layanan.*.jenis_layanan.in' => 'Jenis layanan harus express atau reguler',
|
||||||
'layanan.*.nama_paket.required' => 'Nama paket harus diisi',
|
'layanan.*.nama_paket.required' => 'Nama paket harus diisi',
|
||||||
'layanan.*.harga.required' => 'Harga layanan harus diisi',
|
'layanan.*.harga.required' => 'Harga layanan harus diisi',
|
||||||
'layanan.*.harga.min' => 'Harga tidak boleh negatif',
|
'layanan.*.harga.min' => 'Harga tidak boleh negatif',
|
||||||
|
|
|
||||||
|
|
@ -398,7 +398,6 @@ class="form-control @error('longitude') is-invalid @enderror"
|
||||||
<option value="">-- Pilih Jenis --</option>
|
<option value="">-- Pilih Jenis --</option>
|
||||||
<option value="reguler">🕐 Reguler (Normal)</option>
|
<option value="reguler">🕐 Reguler (Normal)</option>
|
||||||
<option value="express">⚡ Express (Cepat)</option>
|
<option value="express">⚡ Express (Cepat)</option>
|
||||||
<option value="kilat">🚀 Kilat (Super Cepat)</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -546,7 +545,7 @@ class="form-control @error('foto') is-invalid @enderror"
|
||||||
<li>Koordinat akan terisi otomatis saat klik peta</li>
|
<li>Koordinat akan terisi otomatis saat klik peta</li>
|
||||||
<li>Reguler: Layanan normal dengan harga standar</li>
|
<li>Reguler: Layanan normal dengan harga standar</li>
|
||||||
<li>Express: Layanan cepat dengan harga lebih tinggi</li>
|
<li>Express: Layanan cepat dengan harga lebih tinggi</li>
|
||||||
<li>Kilat: Layanan super cepat dengan harga premium</li>
|
<li>Express: Layanan cepat dengan waktu penyelesaian lebih singkat.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -722,7 +721,6 @@ function(error) {
|
||||||
<option value="">-- Pilih Jenis --</option>
|
<option value="">-- Pilih Jenis --</option>
|
||||||
<option value="reguler">🕐 Reguler (Normal)</option>
|
<option value="reguler">🕐 Reguler (Normal)</option>
|
||||||
<option value="express">⚡ Express (Cepat)</option>
|
<option value="express">⚡ Express (Cepat)</option>
|
||||||
<option value="kilat">🚀 Kilat (Super Cepat)</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -393,7 +393,6 @@ class="form-control"
|
||||||
<option value="">-- Pilih Jenis --</option>
|
<option value="">-- Pilih Jenis --</option>
|
||||||
<option value="reguler" {{ $layanan->jenis_layanan == 'reguler' ? 'selected' : '' }}>🕐 Reguler (Normal)</option>
|
<option value="reguler" {{ $layanan->jenis_layanan == 'reguler' ? 'selected' : '' }}>🕐 Reguler (Normal)</option>
|
||||||
<option value="express" {{ $layanan->jenis_layanan == 'express' ? 'selected' : '' }}>⚡ Express (Cepat)</option>
|
<option value="express" {{ $layanan->jenis_layanan == 'express' ? 'selected' : '' }}>⚡ Express (Cepat)</option>
|
||||||
<option value="kilat" {{ $layanan->jenis_layanan == 'kilat' ? 'selected' : '' }}>🚀 Kilat (Super Cepat)</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -488,7 +487,6 @@ class="form-control"
|
||||||
<option value="">-- Pilih Jenis --</option>
|
<option value="">-- Pilih Jenis --</option>
|
||||||
<option value="reguler">🕐 Reguler (Normal)</option>
|
<option value="reguler">🕐 Reguler (Normal)</option>
|
||||||
<option value="express">⚡ Express (Cepat)</option>
|
<option value="express">⚡ Express (Cepat)</option>
|
||||||
<option value="kilat">🚀 Kilat (Super Cepat)</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -755,7 +753,6 @@ function updateJarakKampus(lat, lng) {
|
||||||
<option value="">-- Pilih Jenis --</option>
|
<option value="">-- Pilih Jenis --</option>
|
||||||
<option value="reguler">🕐 Reguler (Normal)</option>
|
<option value="reguler">🕐 Reguler (Normal)</option>
|
||||||
<option value="express">⚡ Express (Cepat)</option>
|
<option value="express">⚡ Express (Cepat)</option>
|
||||||
<option value="kilat">🚀 Kilat (Super Cepat)</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="spk_mobile"
|
android:label="Kontrak Kampus"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Spk Mobile</string>
|
<string>Kontrak Kampus</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>spk_mobile</string>
|
<string>KontrakKampus</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ class AppConfig {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Default fallback (dipakai jika auto-detect gagal)
|
// Default fallback (dipakai jika auto-detect gagal)
|
||||||
static const String _defaultServer = 'http://192.168.1.154:8000';
|
static const String _defaultServer = 'http://192.168.18.16:8000';
|
||||||
|
|
||||||
// Runtime values — diupdate otomatis oleh ServerDiscoveryService
|
// Runtime values — diupdate otomatis oleh ServerDiscoveryService
|
||||||
static String _serverUrl = _defaultServer;
|
static String _serverUrl = _defaultServer;
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,12 @@ class Environment {
|
||||||
// Pilih sesuai platform dan environment saat development
|
// Pilih sesuai platform dan environment saat development
|
||||||
static const String apiBaseUrl = String.fromEnvironment(
|
static const String apiBaseUrl = String.fromEnvironment(
|
||||||
'API_BASE_URL',
|
'API_BASE_URL',
|
||||||
defaultValue: 'http://192.168.1.154:8000', // Default IP lokal
|
defaultValue: 'http://192.168.18.16:8000', // Default IP lokal
|
||||||
);
|
);
|
||||||
|
|
||||||
static const String storageBaseUrl = String.fromEnvironment(
|
static const String storageBaseUrl = String.fromEnvironment(
|
||||||
'STORAGE_BASE_URL',
|
'STORAGE_BASE_URL',
|
||||||
defaultValue: 'http://192.168.1.154:8000/storage',
|
defaultValue: 'http://192.168.18.16:8000/storage',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mode debugging
|
// Mode debugging
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class MyApp extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'SPK Rekomendasi Kontrakan & Laundry',
|
title: 'Kontrak Kampus',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
seedColor: _primary,
|
seedColor: _primary,
|
||||||
|
|
@ -71,7 +71,10 @@ class MyApp extends StatelessWidget {
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFFF7F8FC),
|
fillColor: const Color(0xFFF7F8FC),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
|
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
|
||||||
|
|
@ -120,9 +123,10 @@ class _SplashScreenState extends State<SplashScreen>
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: const Duration(milliseconds: 1200),
|
duration: const Duration(milliseconds: 1200),
|
||||||
);
|
);
|
||||||
_fadeAnim = Tween<double>(begin: 0, end: 1).animate(
|
_fadeAnim = Tween<double>(
|
||||||
CurvedAnimation(parent: _animController, curve: Curves.easeOut),
|
begin: 0,
|
||||||
);
|
end: 1,
|
||||||
|
).animate(CurvedAnimation(parent: _animController, curve: Curves.easeOut));
|
||||||
_scaleAnim = Tween<double>(begin: 0.7, end: 1).animate(
|
_scaleAnim = Tween<double>(begin: 0.7, end: 1).animate(
|
||||||
CurvedAnimation(parent: _animController, curve: Curves.elasticOut),
|
CurvedAnimation(parent: _animController, curve: Curves.elasticOut),
|
||||||
);
|
);
|
||||||
|
|
@ -174,11 +178,7 @@ class _SplashScreenState extends State<SplashScreen>
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: [Color(0xFF1565C0), Color(0xFF0D47A1), Color(0xFF1A237E)],
|
||||||
Color(0xFF1565C0),
|
|
||||||
Color(0xFF0D47A1),
|
|
||||||
Color(0xFF1A237E),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
|
|
@ -210,7 +210,7 @@ class _SplashScreenState extends State<SplashScreen>
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
const Text(
|
const Text(
|
||||||
'SPK Rekomendasi',
|
'Kontrak Kampus',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
|
|
@ -220,7 +220,7 @@ class _SplashScreenState extends State<SplashScreen>
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Kontrakan & Laundry Terbaik untuk Mahasiswa',
|
'Rekomendasi Kontrakan & Laundry untuk Mahasiswa',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.white.withOpacity(0.8),
|
color: Colors.white.withOpacity(0.8),
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ class Laundry {
|
||||||
final String jamBuka;
|
final String jamBuka;
|
||||||
final String jamTutup;
|
final String jamTutup;
|
||||||
final double hargaPerKg;
|
final double hargaPerKg;
|
||||||
|
final double hargaKiloan;
|
||||||
final int waktuProses; // dalam jam
|
final int waktuProses; // dalam jam
|
||||||
final String? deskripsi;
|
final String? deskripsi;
|
||||||
final String? fotoUtama;
|
final String? fotoUtama;
|
||||||
|
|
@ -31,6 +32,7 @@ class Laundry {
|
||||||
required this.jamBuka,
|
required this.jamBuka,
|
||||||
required this.jamTutup,
|
required this.jamTutup,
|
||||||
required this.hargaPerKg,
|
required this.hargaPerKg,
|
||||||
|
required this.hargaKiloan,
|
||||||
required this.waktuProses,
|
required this.waktuProses,
|
||||||
this.deskripsi,
|
this.deskripsi,
|
||||||
this.fotoUtama,
|
this.fotoUtama,
|
||||||
|
|
@ -53,13 +55,31 @@ class Laundry {
|
||||||
|
|
||||||
// Get harga from layanan if harga_per_kg is not set directly
|
// Get harga from layanan if harga_per_kg is not set directly
|
||||||
double harga = 0;
|
double harga = 0;
|
||||||
|
double hargaKiloan = 0;
|
||||||
|
|
||||||
if (json['harga_per_kg'] != null) {
|
if (json['harga_per_kg'] != null) {
|
||||||
harga = double.tryParse(json['harga_per_kg'].toString()) ?? 0;
|
harga = double.tryParse(json['harga_per_kg'].toString()) ?? 0;
|
||||||
} else if (json['layanan'] != null &&
|
}
|
||||||
|
|
||||||
|
if (json['harga_kiloan'] != null) {
|
||||||
|
hargaKiloan = double.tryParse(json['harga_kiloan'].toString()) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((harga == 0 || hargaKiloan == 0) &&
|
||||||
|
json['layanan'] != null &&
|
||||||
(json['layanan'] as List).isNotEmpty) {
|
(json['layanan'] as List).isNotEmpty) {
|
||||||
final layananList = json['layanan'] as List;
|
final layananList = json['layanan'] as List;
|
||||||
harga =
|
harga = harga > 0
|
||||||
double.tryParse(layananList.first['harga']?.toString() ?? '0') ?? 0;
|
? harga
|
||||||
|
: double.tryParse(layananList.first['harga']?.toString() ?? '0') ?? 0;
|
||||||
|
for (final item in layananList) {
|
||||||
|
final jenis = item['jenis_layanan']?.toString().toLowerCase();
|
||||||
|
final itemHarga =
|
||||||
|
double.tryParse(item['harga']?.toString() ?? '0') ?? 0;
|
||||||
|
if (jenis == 'kiloan') {
|
||||||
|
hargaKiloan = itemHarga;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get foto from API and build gallery list
|
// Get foto from API and build gallery list
|
||||||
|
|
@ -68,19 +88,21 @@ class Laundry {
|
||||||
// Parse galeri array if exists
|
// Parse galeri array if exists
|
||||||
if (json['galeri'] != null && (json['galeri'] as List).isNotEmpty) {
|
if (json['galeri'] != null && (json['galeri'] as List).isNotEmpty) {
|
||||||
galleryList.addAll(
|
galleryList.addAll(
|
||||||
(json['galeri'] as List).map((g) => GaleriLaundry.fromJson(g)).toList()
|
(json['galeri'] as List).map((g) => GaleriLaundry.fromJson(g)).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If gallery is empty but foto field exists, create a galeri item from it
|
// If gallery is empty but foto field exists, create a galeri item from it
|
||||||
if (galleryList.isEmpty && json['foto'] != null && json['foto'].toString().isNotEmpty) {
|
if (galleryList.isEmpty &&
|
||||||
|
json['foto'] != null &&
|
||||||
|
json['foto'].toString().isNotEmpty) {
|
||||||
galleryList.add(
|
galleryList.add(
|
||||||
GaleriLaundry(
|
GaleriLaundry(
|
||||||
id: json['id'] ?? 0,
|
id: json['id'] ?? 0,
|
||||||
foto: json['foto'].toString(),
|
foto: json['foto'].toString(),
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
urutan: 0,
|
urutan: 0,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,6 +121,7 @@ class Laundry {
|
||||||
jamBuka: _parseTime(json['jam_buka'], '08:00'),
|
jamBuka: _parseTime(json['jam_buka'], '08:00'),
|
||||||
jamTutup: _parseTime(json['jam_tutup'], '20:00'),
|
jamTutup: _parseTime(json['jam_tutup'], '20:00'),
|
||||||
hargaPerKg: harga,
|
hargaPerKg: harga,
|
||||||
|
hargaKiloan: hargaKiloan,
|
||||||
waktuProses: json['waktu_proses'] ?? 24,
|
waktuProses: json['waktu_proses'] ?? 24,
|
||||||
deskripsi: json['deskripsi'],
|
deskripsi: json['deskripsi'],
|
||||||
fotoUtama: json['foto_utama'],
|
fotoUtama: json['foto_utama'],
|
||||||
|
|
@ -149,13 +172,23 @@ class Laundry {
|
||||||
|
|
||||||
// Format harga dengan Rupiah
|
// Format harga dengan Rupiah
|
||||||
String get formattedHarga {
|
String get formattedHarga {
|
||||||
return 'Rp ${hargaPerKg.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
|
return 'Rp ${_formatRupiah(hargaPerKg)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatRupiah(double value) {
|
||||||
|
return value
|
||||||
|
.toStringAsFixed(0)
|
||||||
|
.replaceAllMapped(
|
||||||
|
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||||
|
(Match m) => '${m[1]}.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get formattedHargaKiloan {
|
||||||
|
final value = hargaKiloan > 0 ? hargaKiloan : hargaPerKg;
|
||||||
|
return 'Rp ${_formatRupiah(value)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy getters for backward compatibility
|
|
||||||
String get formattedHargaKiloan => formattedHarga;
|
|
||||||
String get formattedHargaSatuan => formattedHarga;
|
|
||||||
double get hargaKiloan => hargaPerKg;
|
|
||||||
double get jarak => jarakKampus; // alias for jarak field
|
double get jarak => jarakKampus; // alias for jarak field
|
||||||
double get rating => avgRating ?? 0.0;
|
double get rating => avgRating ?? 0.0;
|
||||||
String get estimasiSelesai => '$waktuProses jam';
|
String get estimasiSelesai => '$waktuProses jam';
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:image_picker/image_picker.dart';
|
||||||
import '../models/kontrakan.dart';
|
import '../models/kontrakan.dart';
|
||||||
import '../services/booking_service.dart';
|
import '../services/booking_service.dart';
|
||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
|
import 'improved_home_screen.dart';
|
||||||
|
|
||||||
class BookingFormScreen extends StatefulWidget {
|
class BookingFormScreen extends StatefulWidget {
|
||||||
final Kontrakan kontrakan;
|
final Kontrakan kontrakan;
|
||||||
|
|
@ -23,7 +24,7 @@ class _BookingFormScreenState extends State<BookingFormScreen> {
|
||||||
final _imagePicker = ImagePicker();
|
final _imagePicker = ImagePicker();
|
||||||
|
|
||||||
DateTime? _tanggalMulai;
|
DateTime? _tanggalMulai;
|
||||||
int _durasiBulan = 1;
|
int _durasiBulan = 6;
|
||||||
bool _isSubmitting = false;
|
bool _isSubmitting = false;
|
||||||
File? _paymentProofImage;
|
File? _paymentProofImage;
|
||||||
|
|
||||||
|
|
@ -33,7 +34,11 @@ class _BookingFormScreenState extends State<BookingFormScreen> {
|
||||||
decimalDigits: 0,
|
decimalDigits: 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
double get _totalBiaya => widget.kontrakan.harga * _durasiBulan;
|
// Harga kontrakan disimpan sebagai harga tahunan, jadi total disesuaikan
|
||||||
|
// proporsional terhadap durasi sewa (6 bulan atau 12 bulan).
|
||||||
|
double get _totalBiaya => widget.kontrakan.harga * (_durasiBulan / 12);
|
||||||
|
|
||||||
|
String get _durasiLabel => _durasiBulan == 12 ? '1 tahun' : '6 bulan';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -194,7 +199,7 @@ class _BookingFormScreenState extends State<BookingFormScreen> {
|
||||||
intl.DateFormat('dd MMMM yyyy', 'id_ID').format(_tanggalMulai!),
|
intl.DateFormat('dd MMMM yyyy', 'id_ID').format(_tanggalMulai!),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildConfirmRow('Durasi', '$_durasiBulan bulan'),
|
_buildConfirmRow('Durasi', _durasiLabel),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildConfirmRow(
|
_buildConfirmRow(
|
||||||
'Total Biaya',
|
'Total Biaya',
|
||||||
|
|
@ -290,7 +295,13 @@ class _BookingFormScreenState extends State<BookingFormScreen> {
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(ctx); // Close dialog
|
Navigator.pop(ctx); // Close dialog
|
||||||
Navigator.pop(context, true); // Go back to detail with result
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const ImprovedHomeScreen(),
|
||||||
|
),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF667eea),
|
backgroundColor: const Color(0xFF667eea),
|
||||||
|
|
@ -413,7 +424,7 @@ class _BookingFormScreenState extends State<BookingFormScreen> {
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${widget.kontrakan.formattedHarga}/bulan',
|
'${widget.kontrakan.formattedHarga}/tahun',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -490,7 +501,7 @@ class _BookingFormScreenState extends State<BookingFormScreen> {
|
||||||
_buildFormCard(
|
_buildFormCard(
|
||||||
icon: Icons.access_time,
|
icon: Icons.access_time,
|
||||||
title: 'Durasi Sewa',
|
title: 'Durasi Sewa',
|
||||||
subtitle: 'Tentukan berapa bulan Anda ingin menyewa',
|
subtitle: 'Pilih durasi 6 bulan atau 1 tahun',
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -513,12 +524,16 @@ class _BookingFormScreenState extends State<BookingFormScreen> {
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
items: List.generate(12, (i) => i + 1).map((bulan) {
|
items: const [
|
||||||
return DropdownMenuItem<int>(
|
DropdownMenuItem<int>(
|
||||||
value: bulan,
|
value: 6,
|
||||||
child: Text('$bulan bulan'),
|
child: Text('6 bulan'),
|
||||||
);
|
),
|
||||||
}).toList(),
|
DropdownMenuItem<int>(
|
||||||
|
value: 12,
|
||||||
|
child: Text('1 tahun'),
|
||||||
|
),
|
||||||
|
],
|
||||||
onChanged: (val) =>
|
onChanged: (val) =>
|
||||||
setState(() => _durasiBulan = val!),
|
setState(() => _durasiBulan = val!),
|
||||||
),
|
),
|
||||||
|
|
@ -669,11 +684,11 @@ class _BookingFormScreenState extends State<BookingFormScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildBiayaRow(
|
_buildBiayaRow(
|
||||||
'Harga per bulan',
|
'Harga per tahun',
|
||||||
widget.kontrakan.formattedHarga,
|
widget.kontrakan.formattedHarga,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_buildBiayaRow('Durasi sewa', '$_durasiBulan bulan'),
|
_buildBiayaRow('Durasi sewa', _durasiLabel),
|
||||||
const Divider(height: 20),
|
const Divider(height: 20),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
|
|
||||||
|
|
@ -491,7 +491,7 @@ class _FavoritesScreenState extends State<FavoritesScreen>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'/bln',
|
'/thn',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|
|
||||||
|
|
@ -845,7 +845,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Rp ${_formatPrice(kontrakan.harga)}/bln',
|
'Rp ${_formatPrice(kontrakan.harga)}/thn',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
kontrakan.formattedHarga + '/bln',
|
kontrakan.formattedHarga + '/thn',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
|
||||||
|
|
@ -281,7 +281,7 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'SPK Rekomendasi',
|
'Kontrak Kampus',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
|
|
@ -1088,7 +1088,7 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'/bln',
|
'/thn',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
|
||||||
|
|
||||||
/// Hitung jarak dari Kampus Polije ke kontrakan ini secara langsung.
|
/// Hitung jarak dari Kampus Polije ke kontrakan ini secara langsung.
|
||||||
void _calcDistanceFromPolije() {
|
void _calcDistanceFromPolije() {
|
||||||
if (widget.kontrakan.latitude == null || widget.kontrakan.longitude == null) return;
|
if (widget.kontrakan.latitude == null || widget.kontrakan.longitude == null)
|
||||||
|
return;
|
||||||
final dist = LocationService.calculateDistance(
|
final dist = LocationService.calculateDistance(
|
||||||
_polije_lat,
|
_polije_lat,
|
||||||
_polije_lng,
|
_polije_lng,
|
||||||
|
|
@ -100,7 +101,9 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
|
||||||
content: Text(result['message'] ?? 'Gagal mengubah favorit'),
|
content: Text(result['message'] ?? 'Gagal mengubah favorit'),
|
||||||
backgroundColor: Colors.red[700],
|
backgroundColor: Colors.red[700],
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
margin: const EdgeInsets.all(16),
|
margin: const EdgeInsets.all(16),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -226,7 +229,7 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
' /bulan',
|
' /tahun',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|
@ -478,7 +481,11 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.school_rounded, color: Color(0xFF1565C0), size: 22),
|
const Icon(
|
||||||
|
Icons.school_rounded,
|
||||||
|
color: Color(0xFF1565C0),
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
const Text(
|
const Text(
|
||||||
'Jarak dari Kampus Polije',
|
'Jarak dari Kampus Polije',
|
||||||
|
|
@ -521,10 +528,7 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'Politeknik Negeri Jember',
|
'Politeknik Negeri Jember',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -588,5 +592,4 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -263,8 +263,8 @@ class _LaundryDetailScreenState extends State<LaundryDetailScreen> {
|
||||||
),
|
),
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
Icons.schedule,
|
Icons.schedule,
|
||||||
'Estimasi Selesai',
|
'Kecepatan Layanan',
|
||||||
'${widget.laundry.estimasiSelesai} jam',
|
widget.laundry.estimasiSelesai,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -283,13 +283,6 @@ class _LaundryDetailScreenState extends State<LaundryDetailScreen> {
|
||||||
Icons.scale,
|
Icons.scale,
|
||||||
const Color(0xFF00897B),
|
const Color(0xFF00897B),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildPriceCard(
|
|
||||||
'Laundry Satuan',
|
|
||||||
widget.laundry.formattedHargaSatuan,
|
|
||||||
Icons.checkroom,
|
|
||||||
Colors.purple,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -449,37 +449,6 @@ class _LaundryListScreenState extends State<LaundryListScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.purple.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Satuan',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
laundry.formattedHargaSatuan,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.purple,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
@ -496,7 +465,7 @@ class _LaundryListScreenState extends State<LaundryListScreen> {
|
||||||
Icon(Icons.schedule, size: 16, color: Colors.orange[700]),
|
Icon(Icons.schedule, size: 16, color: Colors.orange[700]),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'Selesai dalam ${laundry.estimasiSelesai} jam',
|
'Selesai dalam ${laundry.estimasiSelesai}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ class _MobileHomeScreenState extends State<MobileHomeScreen> {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'SPK Kontrakan',
|
'Kontrak Kampus',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
final bookings = await _bookingService.getBookingHistory();
|
final bookings = await _bookingService.getBookingHistory();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_totalFavorites = (favResult['kontrakan']?.length ?? 0) +
|
_totalFavorites =
|
||||||
|
(favResult['kontrakan']?.length ?? 0) +
|
||||||
(favResult['laundry']?.length ?? 0);
|
(favResult['laundry']?.length ?? 0);
|
||||||
_totalBookings = bookings.length;
|
_totalBookings = bookings.length;
|
||||||
});
|
});
|
||||||
|
|
@ -105,7 +106,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
'September',
|
'September',
|
||||||
'Oktober',
|
'Oktober',
|
||||||
'November',
|
'November',
|
||||||
'Desember'
|
'Desember',
|
||||||
];
|
];
|
||||||
return '${months[date.month]} ${date.year}';
|
return '${months[date.month]} ${date.year}';
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +117,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
backgroundColor: const Color(0xFFF5F6FA),
|
backgroundColor: const Color(0xFFF5F6FA),
|
||||||
body: _isLoading
|
body: _isLoading
|
||||||
? const Center(
|
? const Center(
|
||||||
child: CircularProgressIndicator(color: Color(0xFF1565C0)))
|
child: CircularProgressIndicator(color: Color(0xFF1565C0)),
|
||||||
|
)
|
||||||
: RefreshIndicator(
|
: RefreshIndicator(
|
||||||
onRefresh: _loadAll,
|
onRefresh: _loadAll,
|
||||||
color: const Color(0xFF1565C0),
|
color: const Color(0xFF1565C0),
|
||||||
|
|
@ -156,8 +158,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
final result = await Navigator.push(
|
final result = await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => EditProfileScreen(
|
builder: (_) =>
|
||||||
user: _currentUser!),
|
EditProfileScreen(user: _currentUser!),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (result == true) _loadAll();
|
if (result == true) _loadAll();
|
||||||
|
|
@ -185,7 +187,9 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
|
|
||||||
// --- Aktivitas ---
|
// --- Aktivitas ---
|
||||||
_buildSectionTitle(
|
_buildSectionTitle(
|
||||||
'Aktivitas', Icons.timeline_rounded),
|
'Aktivitas',
|
||||||
|
Icons.timeline_rounded,
|
||||||
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_buildMenuCard([
|
_buildMenuCard([
|
||||||
_buildMenuItem(
|
_buildMenuItem(
|
||||||
|
|
@ -225,7 +229,9 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
|
|
||||||
// --- Lainnya ---
|
// --- Lainnya ---
|
||||||
_buildSectionTitle(
|
_buildSectionTitle(
|
||||||
'Lainnya', Icons.more_horiz_rounded),
|
'Lainnya',
|
||||||
|
Icons.more_horiz_rounded,
|
||||||
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_buildMenuCard([
|
_buildMenuCard([
|
||||||
_buildMenuItem(
|
_buildMenuItem(
|
||||||
|
|
@ -318,8 +324,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
// Title bar
|
// Title bar
|
||||||
const Row(
|
const Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.person_rounded,
|
Icon(Icons.person_rounded, color: Colors.white, size: 24),
|
||||||
color: Colors.white, size: 24),
|
|
||||||
SizedBox(width: 10),
|
SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
'Profil Saya',
|
'Profil Saya',
|
||||||
|
|
@ -355,10 +360,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [Colors.white, Color(0xFFBBDEFB)],
|
||||||
Colors.white,
|
|
||||||
Color(0xFFBBDEFB),
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
),
|
),
|
||||||
|
|
@ -412,7 +414,9 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 10, vertical: 4),
|
horizontal: 10,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withValues(alpha: 0.15),
|
color: Colors.white.withValues(alpha: 0.15),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
|
@ -420,18 +424,20 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.calendar_month_rounded,
|
Icon(
|
||||||
|
Icons.calendar_month_rounded,
|
||||||
size: 13,
|
size: 13,
|
||||||
color: Colors.white
|
color: Colors.white.withValues(alpha: 0.85),
|
||||||
.withValues(alpha: 0.85)),
|
),
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
Text(
|
Text(
|
||||||
'Bergabung ${_getMemberSince(_currentUser?.createdAt)}',
|
'Bergabung ${_getMemberSince(_currentUser?.createdAt)}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.white
|
color: Colors.white.withValues(
|
||||||
.withValues(alpha: 0.9),
|
alpha: 0.9,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -454,8 +460,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
Widget _headerInfoRow(IconData icon, String text) {
|
Widget _headerInfoRow(IconData icon, String text) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon,
|
Icon(icon, size: 14, color: Colors.white.withValues(alpha: 0.75)),
|
||||||
size: 14, color: Colors.white.withValues(alpha: 0.75)),
|
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -559,11 +564,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatDivider() {
|
Widget _buildStatDivider() {
|
||||||
return Container(
|
return Container(width: 1, height: 45, color: Colors.grey.shade200);
|
||||||
width: 1,
|
|
||||||
height: 45,
|
|
||||||
color: Colors.grey.shade200,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== SECTION TITLE ====================
|
// ==================== SECTION TITLE ====================
|
||||||
|
|
@ -681,8 +682,11 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
color: Colors.grey.shade50,
|
color: Colors.grey.shade50,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Icon(Icons.chevron_right_rounded,
|
child: Icon(
|
||||||
color: Colors.grey.shade400, size: 20),
|
Icons.chevron_right_rounded,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -709,8 +713,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.logout_rounded,
|
Icon(Icons.logout_rounded, color: Colors.red.shade600, size: 20),
|
||||||
color: Colors.red.shade600, size: 20),
|
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
'Keluar dari Akun',
|
'Keluar dari Akun',
|
||||||
|
|
@ -749,19 +752,21 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color:
|
color: const Color(0xFF1565C0).withValues(alpha: 0.3),
|
||||||
const Color(0xFF1565C0).withValues(alpha: 0.3),
|
|
||||||
blurRadius: 12,
|
blurRadius: 12,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.home_work_rounded,
|
child: const Icon(
|
||||||
color: Colors.white, size: 40),
|
Icons.home_work_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 40,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Text(
|
const Text(
|
||||||
'SPK Kontrakan & Laundry',
|
'Kontrak Kampus',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
|
|
@ -771,8 +776,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Container(
|
Container(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1565C0).withValues(alpha: 0.1),
|
color: const Color(0xFF1565C0).withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
|
@ -805,14 +809,15 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_aboutDetailRow(
|
_aboutDetailRow(Icons.code_rounded, 'Developer', 'Tim TA'),
|
||||||
Icons.code_rounded, 'Developer', 'Tim TA'),
|
const SizedBox(height: 10),
|
||||||
|
_aboutDetailRow(Icons.school_rounded, 'Institusi', 'Polije'),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_aboutDetailRow(
|
_aboutDetailRow(
|
||||||
Icons.school_rounded, 'Institusi', 'Polije'),
|
Icons.calendar_today_rounded,
|
||||||
const SizedBox(height: 10),
|
'Tahun',
|
||||||
_aboutDetailRow(
|
'2025',
|
||||||
Icons.calendar_today_rounded, 'Tahun', '2025'),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -882,8 +887,11 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
color: Colors.red.shade50,
|
color: Colors.red.shade50,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(Icons.logout_rounded,
|
child: Icon(
|
||||||
color: Colors.red.shade600, size: 32),
|
Icons.logout_rounded,
|
||||||
|
color: Colors.red.shade600,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Text(
|
const Text(
|
||||||
|
|
@ -920,8 +928,10 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text('Batal',
|
child: const Text(
|
||||||
style: TextStyle(fontWeight: FontWeight.w600)),
|
'Batal',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|
@ -937,8 +947,10 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text('Keluar',
|
child: const Text(
|
||||||
style: TextStyle(fontWeight: FontWeight.w700)),
|
'Keluar',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -385,8 +385,8 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final categoryTitle = widget.category == 'kontrakan'
|
final categoryTitle = widget.category == 'kontrakan'
|
||||||
? 'SPK Rekomendasi Kontrakan'
|
? 'Kontrak Kampus - Rekomendasi Kontrakan'
|
||||||
: 'SPK Rekomendasi Laundry';
|
: 'Kontrak Kampus - Rekomendasi Laundry';
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: const Color(0xFFF5F5F5),
|
||||||
|
|
@ -660,12 +660,6 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
||||||
'icon': Icons.flash_on,
|
'icon': Icons.flash_on,
|
||||||
'desc': 'Layanan cepat dengan waktu lebih singkat',
|
'desc': 'Layanan cepat dengan waktu lebih singkat',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'value': 'kilat',
|
|
||||||
'label': 'Kilat',
|
|
||||||
'icon': Icons.bolt,
|
|
||||||
'desc': 'Layanan tercepat, selesai dalam hitungan jam',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
|
@ -1390,8 +1384,6 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
||||||
return 'Reguler';
|
return 'Reguler';
|
||||||
case 'express':
|
case 'express':
|
||||||
return 'Express';
|
return 'Express';
|
||||||
case 'kilat':
|
|
||||||
return 'Kilat';
|
|
||||||
default:
|
default:
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
@ -1704,7 +1696,11 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: widget.category == 'kontrakan'
|
child: widget.category == 'kontrakan'
|
||||||
? KontrakanCard(kontrakan: Kontrakan.fromJson(itemData))
|
? KontrakanCard(kontrakan: Kontrakan.fromJson(itemData))
|
||||||
: LaundryCard(laundry: Laundry.fromJson(itemData)),
|
: LaundryCard(
|
||||||
|
laundry: Laundry.fromJson(itemData),
|
||||||
|
userLatitude: _userLatitude,
|
||||||
|
userLongitude: _userLongitude,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -850,7 +850,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
'${kontrakan.formattedHarga}/bln',
|
'${kontrakan.formattedHarga}/thn',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,13 @@ class ServerDiscoveryService {
|
||||||
static const int _port = 8000;
|
static const int _port = 8000;
|
||||||
static const String _cacheKey = 'discovered_server_url';
|
static const String _cacheKey = 'discovered_server_url';
|
||||||
static const Duration _probeTimeout = Duration(seconds: 2);
|
static const Duration _probeTimeout = Duration(seconds: 2);
|
||||||
|
static const int _scanBatchSize = 24;
|
||||||
|
|
||||||
// ─── PUBLIC ENTRY POINT ─────────────────────────────────────────────────────
|
// ─── PUBLIC ENTRY POINT ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Temukan server dan update [AppConfig] secara otomatis.
|
/// Temukan server dan update [AppConfig] secara otomatis.
|
||||||
/// Kembalikan `true` jika server ditemukan, `false` jika tidak.
|
/// Kembalikan `true` jika server ditemukan, `false` jika tidak.
|
||||||
static Future<bool> discover({
|
static Future<bool> discover({void Function(String status)? onStatus}) async {
|
||||||
void Function(String status)? onStatus,
|
|
||||||
}) async {
|
|
||||||
onStatus?.call('Mencari server...');
|
onStatus?.call('Mencari server...');
|
||||||
|
|
||||||
// 1. Coba cache terlebih dahulu
|
// 1. Coba cache terlebih dahulu
|
||||||
|
|
@ -73,12 +72,12 @@ class ServerDiscoveryService {
|
||||||
try {
|
try {
|
||||||
final response = await http
|
final response = await http
|
||||||
.get(
|
.get(
|
||||||
Uri.parse('$serverUrl/api'),
|
Uri.parse('$serverUrl/api/health'),
|
||||||
headers: {'Accept': 'application/json'},
|
headers: {'Accept': 'application/json'},
|
||||||
)
|
)
|
||||||
.timeout(_probeTimeout);
|
.timeout(_probeTimeout);
|
||||||
// Respons 4xx/5xx tetap berarti server bisa dijangkau
|
// Pastikan benar-benar backend API aplikasi ini.
|
||||||
return response.statusCode > 0;
|
return response.statusCode == 200;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -110,27 +109,33 @@ class ServerDiscoveryService {
|
||||||
List<String> subnets, {
|
List<String> subnets, {
|
||||||
void Function(String)? onStatus,
|
void Function(String)? onStatus,
|
||||||
}) async {
|
}) async {
|
||||||
final completer = Completer<String?>();
|
if (subnets.isEmpty) return null;
|
||||||
int remaining = subnets.length * 254;
|
|
||||||
|
|
||||||
if (remaining == 0) return null;
|
|
||||||
|
|
||||||
|
// Scan host secara bertahap untuk menghindari "socket flood" di device.
|
||||||
for (final subnet in subnets) {
|
for (final subnet in subnets) {
|
||||||
for (int i = 1; i <= 254; i++) {
|
onStatus?.call('Memindai subnet $subnet.x ...');
|
||||||
|
|
||||||
|
for (int start = 1; start <= 254; start += _scanBatchSize) {
|
||||||
|
final end = (start + _scanBatchSize - 1) > 254
|
||||||
|
? 254
|
||||||
|
: (start + _scanBatchSize - 1);
|
||||||
|
|
||||||
|
final futures = <Future<String?>>[];
|
||||||
|
for (int i = start; i <= end; i++) {
|
||||||
final url = 'http://$subnet.$i:$_port';
|
final url = 'http://$subnet.$i:$_port';
|
||||||
_isAlive(url).then((alive) {
|
futures.add(_isAlive(url).then((alive) => alive ? url : null));
|
||||||
if (alive && !completer.isCompleted) {
|
}
|
||||||
completer.complete(url);
|
|
||||||
|
final results = await Future.wait(futures);
|
||||||
|
for (final found in results) {
|
||||||
|
if (found != null) {
|
||||||
|
return found;
|
||||||
}
|
}
|
||||||
remaining--;
|
|
||||||
if (remaining <= 0 && !completer.isCompleted) {
|
|
||||||
completer.complete(null);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return completer.future;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<String?> _getCachedUrl() async {
|
static Future<String?> _getCachedUrl() async {
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ class KontrakanCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'/bln',
|
'/thn',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.grey[500],
|
color: Colors.grey[500],
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,15 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import '../models/laundry.dart';
|
import '../models/laundry.dart';
|
||||||
import '../screens/laundry_detail_screen.dart';
|
import '../screens/laundry_detail_screen.dart';
|
||||||
|
import '../services/location_service.dart';
|
||||||
|
|
||||||
class LaundryCard extends StatelessWidget {
|
class LaundryCard extends StatelessWidget {
|
||||||
final Laundry laundry;
|
final Laundry laundry;
|
||||||
final int? ranking;
|
final int? ranking;
|
||||||
final double? skor;
|
final double? skor;
|
||||||
final bool showRanking;
|
final bool showRanking;
|
||||||
|
final double? userLatitude;
|
||||||
|
final double? userLongitude;
|
||||||
|
|
||||||
const LaundryCard({
|
const LaundryCard({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -15,6 +18,8 @@ class LaundryCard extends StatelessWidget {
|
||||||
this.ranking,
|
this.ranking,
|
||||||
this.skor,
|
this.skor,
|
||||||
this.showRanking = false,
|
this.showRanking = false,
|
||||||
|
this.userLatitude,
|
||||||
|
this.userLongitude,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -243,7 +248,7 @@ class LaundryCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'${laundry.jarakKampus.toStringAsFixed(1)} km dari kampus',
|
_distanceText(),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
|
|
@ -304,6 +309,26 @@ class LaundryCard extends StatelessWidget {
|
||||||
return const Color(0xFF00897B);
|
return const Color(0xFF00897B);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _distanceText() {
|
||||||
|
if (userLatitude != null &&
|
||||||
|
userLongitude != null &&
|
||||||
|
laundry.latitude != null &&
|
||||||
|
laundry.longitude != null) {
|
||||||
|
final distance = LocationService.calculateDistance(
|
||||||
|
userLatitude!,
|
||||||
|
userLongitude!,
|
||||||
|
laundry.latitude!,
|
||||||
|
laundry.longitude!,
|
||||||
|
);
|
||||||
|
if (distance < 1) {
|
||||||
|
return '${(distance * 1000).toStringAsFixed(0)} m dari lokasi Anda';
|
||||||
|
}
|
||||||
|
return '${distance.toStringAsFixed(1)} km dari lokasi Anda';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '${laundry.jarakKampus.toStringAsFixed(1)} km dari kampus';
|
||||||
|
}
|
||||||
|
|
||||||
IconData _getRankingIcon(int ranking) {
|
IconData _getRankingIcon(int ranking) {
|
||||||
if (ranking <= 3) return Icons.emoji_events;
|
if (ranking <= 3) return Icons.emoji_events;
|
||||||
return Icons.star;
|
return Icons.star;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue