From 7453d3b322d8dc16b08c43889ad8008de7941f81 Mon Sep 17 00:00:00 2001 From: micko samawa Date: Tue, 24 Feb 2026 19:42:59 +0700 Subject: [PATCH] Fix price display format and improve UI/UX across all screens - Replace confusing K notation with proper Rp format with thousand separators - Add /bln and /kg suffix for clarity - Improve card shadows, icons, and sections for professional appearance - Create currency formatter utility --- spk_mobile/lib/config/app_config.dart | 4 +- spk_mobile/lib/login.dart | 12 -- .../lib/screens/booking_history_screen.dart | 47 +++-- spk_mobile/lib/screens/favorites_screen.dart | 52 +++-- spk_mobile/lib/screens/home_screen.dart | 12 +- .../lib/screens/improved_home_screen.dart | 64 ++++-- .../lib/screens/kontrakan_detail_screen.dart | 85 +++++--- .../lib/screens/laundry_detail_screen.dart | 48 +++-- .../lib/screens/recommendation_screen.dart | 2 +- spk_mobile/lib/screens/search_screen.dart | 191 +++++++++--------- spk_mobile/lib/services/booking_service.dart | 4 +- spk_mobile/lib/services/favorite_service.dart | 8 +- .../lib/services/kontrakan_service.dart | 4 +- spk_mobile/lib/services/laundry_service.dart | 7 +- spk_mobile/lib/services/location_service.dart | 2 +- spk_mobile/lib/utils/currency_formatter.dart | 45 +++++ spk_mobile/lib/widgets/kontrakan_card.dart | 48 +++-- spk_mobile/lib/widgets/laundry_card.dart | 40 ++-- 18 files changed, 416 insertions(+), 259 deletions(-) create mode 100644 spk_mobile/lib/utils/currency_formatter.dart diff --git a/spk_mobile/lib/config/app_config.dart b/spk_mobile/lib/config/app_config.dart index 9bf1042..f8c5398 100644 --- a/spk_mobile/lib/config/app_config.dart +++ b/spk_mobile/lib/config/app_config.dart @@ -7,8 +7,8 @@ class AppConfig { // CATATAN: Untuk real device, ganti IP sesuai dengan IP komputer Anda // Cek IP dengan: ipconfig (di Windows) atau ifconfig (di Linux/Mac) - static const String baseUrl = 'http://10.215.176.99:8000/api'; - static const String storageUrl = 'http://10.215.176.99:8000/storage'; + static const String baseUrl = 'http://192.168.18.16:8000/api'; + static const String storageUrl = 'http://192.168.18.16:8000/storage'; // Timeouts static const Duration connectionTimeout = Duration(seconds: 10); diff --git a/spk_mobile/lib/login.dart b/spk_mobile/lib/login.dart index 98344c8..44540d4 100644 --- a/spk_mobile/lib/login.dart +++ b/spk_mobile/lib/login.dart @@ -198,11 +198,7 @@ class _LoginScreenState extends State { } Future _handleLogin() async { - print('=== LOGIN BUTTON CLICKED ==='); - print('FormKey currentState: ${_formKey.currentState}'); - if (_formKey.currentState == null) { - print('ERROR: FormKey currentState is null!'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Error: Form not initialized')), ); @@ -210,13 +206,9 @@ class _LoginScreenState extends State { } if (!_formKey.currentState!.validate()) { - print('Form validation failed'); return; } - print('Form validated, attempting login...'); - print('Email: ${_emailController.text.trim()}'); - setState(() => _isLoading = true); final result = await _authService.login( @@ -224,20 +216,16 @@ class _LoginScreenState extends State { password: _passwordController.text, ); - print('Login result: $result'); - setState(() => _isLoading = false); if (!mounted) return; if (result['success']) { - print('Login successful, navigating to home...'); Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const ImprovedHomeScreen()), ); } else { - print('Login failed: ${result['message']}'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(result['message'] ?? 'Login gagal'), diff --git a/spk_mobile/lib/screens/booking_history_screen.dart b/spk_mobile/lib/screens/booking_history_screen.dart index 4441f60..2cba779 100644 --- a/spk_mobile/lib/screens/booking_history_screen.dart +++ b/spk_mobile/lib/screens/booking_history_screen.dart @@ -349,13 +349,14 @@ class _BookingHistoryScreenState extends State Widget _buildBookingList(List bookings, bool isActive) { if (bookings.isEmpty) { - return Center( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( + return RefreshIndicator( + onRefresh: _loadBookings, + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + SizedBox(height: MediaQuery.of(context).size.height * 0.2), + Center( + child: Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: const Color(0xFF1565C0).withOpacity(0.06), @@ -367,8 +368,10 @@ class _BookingHistoryScreenState extends State color: const Color(0xFF1565C0).withOpacity(0.3), ), ), - const SizedBox(height: 20), - Text( + ), + const SizedBox(height: 20), + Center( + child: Text( isActive ? 'Belum ada booking aktif' : 'Belum ada riwayat', style: const TextStyle( fontSize: 17, @@ -376,26 +379,32 @@ class _BookingHistoryScreenState extends State color: Color(0xFF1A1A2E), ), ), - const SizedBox(height: 8), - Text( + ), + const SizedBox(height: 8), + Center( + child: Text( isActive ? 'Booking kontrakan Anda akan muncul di sini' : 'Riwayat booking sebelumnya akan muncul di sini', style: TextStyle(fontSize: 13, color: Colors.grey[500]), textAlign: TextAlign.center, ), - ], - ), + ), + ], ), ); } - return ListView.builder( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 16), - itemCount: bookings.length, - itemBuilder: (context, index) { - return _buildBookingCard(bookings[index]); - }, + return RefreshIndicator( + onRefresh: _loadBookings, + child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 16, 20, 16), + itemCount: bookings.length, + itemBuilder: (context, index) { + return _buildBookingCard(bookings[index]); + }, + ), ); } diff --git a/spk_mobile/lib/screens/favorites_screen.dart b/spk_mobile/lib/screens/favorites_screen.dart index a914cf7..ee7ad67 100644 --- a/spk_mobile/lib/screens/favorites_screen.dart +++ b/spk_mobile/lib/screens/favorites_screen.dart @@ -368,13 +368,25 @@ class _FavoritesScreenState extends State ], ), const SizedBox(height: 8), - Text( - kontrakan.formattedHarga, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w800, - color: Color(0xFF1565C0), - ), + Row( + children: [ + Text( + kontrakan.formattedHarga, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: Color(0xFF1565C0), + ), + ), + Text( + '/bln', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.grey[500], + ), + ), + ], ), const SizedBox(height: 6), Row( @@ -530,13 +542,25 @@ class _FavoritesScreenState extends State ], ), const SizedBox(height: 8), - Text( - laundry.formattedHarga, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w800, - color: Color(0xFF00897B), - ), + Row( + children: [ + Text( + laundry.formattedHarga, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: Color(0xFF00897B), + ), + ), + Text( + '/kg', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.grey[500], + ), + ), + ], ), const SizedBox(height: 6), Row( diff --git a/spk_mobile/lib/screens/home_screen.dart b/spk_mobile/lib/screens/home_screen.dart index e384f17..59d637c 100644 --- a/spk_mobile/lib/screens/home_screen.dart +++ b/spk_mobile/lib/screens/home_screen.dart @@ -1000,12 +1000,12 @@ class _HomeScreenState extends State { } String _formatPrice(double price) { - if (price >= 1000000) { - return '${(price / 1000000).toStringAsFixed(1)}Jt'; - } else if (price >= 1000) { - return '${(price / 1000).toStringAsFixed(0)}K'; - } - return price.toStringAsFixed(0); + return price + .toStringAsFixed(0) + .replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]}.', + ); } Widget _buildBottomNav() { diff --git a/spk_mobile/lib/screens/improved_home_screen.dart b/spk_mobile/lib/screens/improved_home_screen.dart index fa3ab4c..8cad355 100644 --- a/spk_mobile/lib/screens/improved_home_screen.dart +++ b/spk_mobile/lib/screens/improved_home_screen.dart @@ -56,7 +56,7 @@ class _ImprovedHomeScreenState extends State { _favLaundryIds = (ids['laundry'] ?? []).toSet(); }); } catch (e) { - print('Error loading favorite ids: $e'); + // Error loading favorite ids silently } } @@ -107,7 +107,7 @@ class _ImprovedHomeScreenState extends State { final list = await _kontrakanService.getKontrakan(); setState(() => _kontrakanList = list.take(6).toList()); } catch (e) { - print('Error loading kontrakan: $e'); + // Error loading kontrakan silently } } @@ -116,7 +116,7 @@ class _ImprovedHomeScreenState extends State { final list = await _laundryService.getLaundry(); setState(() => _laundryList = list.take(6).toList()); } catch (e) { - print('Error loading laundry: $e'); + // Error loading laundry silently } } @@ -1004,13 +1004,28 @@ class _ImprovedHomeScreenState extends State { Row( children: [ Expanded( - child: Text( - kontrakan.formattedHarga, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: Color(0xFF1565C0), - ), + child: Row( + children: [ + Flexible( + child: Text( + kontrakan.formattedHarga, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Color(0xFF1565C0), + ), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + '/bln', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: Colors.grey[500], + ), + ), + ], ), ), Container( @@ -1198,13 +1213,28 @@ class _ImprovedHomeScreenState extends State { Row( children: [ Expanded( - child: Text( - laundry.formattedHarga, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: Color(0xFF00897B), - ), + child: Row( + children: [ + Flexible( + child: Text( + laundry.formattedHarga, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Color(0xFF00897B), + ), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + '/kg', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: Colors.grey[500], + ), + ), + ], ), ), Container( diff --git a/spk_mobile/lib/screens/kontrakan_detail_screen.dart b/spk_mobile/lib/screens/kontrakan_detail_screen.dart index 9f9fc20..93e044a 100644 --- a/spk_mobile/lib/screens/kontrakan_detail_screen.dart +++ b/spk_mobile/lib/screens/kontrakan_detail_screen.dart @@ -145,16 +145,36 @@ class _KontrakanDetailScreenState extends State { style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, - color: Colors.black87, + color: Color(0xFF1A1A2E), ), ), - const SizedBox(height: 8), - Text( - '${widget.kontrakan.formattedHarga}/bulan', - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Color(0xFF1565C0), + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: const Color(0xFF1565C0).withOpacity(0.08), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.kontrakan.formattedHarga, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w800, + color: Color(0xFF1565C0), + ), + ), + Text( + ' /bulan', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey[600], + ), + ), + ], ), ), const SizedBox(height: 16), @@ -183,19 +203,31 @@ class _KontrakanDetailScreenState extends State { const Text( 'Fasilitas', style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.black87, + fontSize: 17, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1A2E), ), ), - const SizedBox(height: 8), + const SizedBox(height: 10), Wrap( spacing: 8, runSpacing: 8, children: widget.kontrakan.fasilitasList.map((f) { - return Chip( - label: Text(f), - backgroundColor: Colors.blue[50], + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFE3F2FD), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFF1565C0).withOpacity(0.15)), + ), + child: Text( + f, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFF1565C0), + ), + ), ); }).toList(), ), @@ -205,15 +237,15 @@ class _KontrakanDetailScreenState extends State { const Text( 'Deskripsi', style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.black87, + fontSize: 17, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1A2E), ), ), const SizedBox(height: 8), Text( widget.kontrakan.deskripsi!, - style: const TextStyle(fontSize: 14, height: 1.5), + style: TextStyle(fontSize: 14, height: 1.6, color: Colors.grey[700]), ), ], @@ -315,15 +347,22 @@ class _KontrakanDetailScreenState extends State { Widget _buildInfoRow(IconData icon, String text) { return Padding( - padding: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.only(bottom: 10), child: Row( children: [ - Icon(icon, size: 20, color: Colors.grey[600]), - const SizedBox(width: 8), + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFF1565C0).withOpacity(0.08), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, size: 18, color: const Color(0xFF1565C0)), + ), + const SizedBox(width: 12), Expanded( child: Text( text, - style: const TextStyle(fontSize: 14, color: Colors.black87), + style: const TextStyle(fontSize: 14, color: Color(0xFF333333), height: 1.3), ), ), ], diff --git a/spk_mobile/lib/screens/laundry_detail_screen.dart b/spk_mobile/lib/screens/laundry_detail_screen.dart index e783795..f9b0b56 100644 --- a/spk_mobile/lib/screens/laundry_detail_screen.dart +++ b/spk_mobile/lib/screens/laundry_detail_screen.dart @@ -329,19 +329,19 @@ class _LaundryDetailScreenState extends State { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: statusColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: statusColor, width: 2), + color: statusColor.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: statusColor.withOpacity(0.3), width: 1.5), ), child: Row( children: [ Container( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: statusColor, - shape: BoxShape.circle, + color: statusColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), ), - child: const Icon(Icons.store, color: Colors.white, size: 24), + child: Icon(Icons.store_rounded, color: statusColor, size: 24), ), const SizedBox(width: 12), Expanded( @@ -569,14 +569,21 @@ class _LaundryDetailScreenState extends State { children: [ Row( children: [ - Icon(icon, color: const Color(0xFF00897B)), - const SizedBox(width: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF00897B).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: const Color(0xFF00897B), size: 20), + ), + const SizedBox(width: 10), Text( title, style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black87, + fontSize: 17, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1A2E), ), ), ], @@ -590,11 +597,18 @@ class _LaundryDetailScreenState extends State { Widget _buildInfoRow(IconData icon, String label, String value) { return Padding( - padding: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.only(bottom: 14), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(icon, size: 20, color: Colors.grey[600]), + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFF00897B).withOpacity(0.08), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, size: 18, color: const Color(0xFF00897B)), + ), const SizedBox(width: 12), Expanded( child: Column( @@ -602,15 +616,15 @@ class _LaundryDetailScreenState extends State { children: [ Text( label, - style: TextStyle(fontSize: 13, color: Colors.grey[600]), + style: TextStyle(fontSize: 12, color: Colors.grey[500], fontWeight: FontWeight.w500), ), - const SizedBox(height: 2), + const SizedBox(height: 3), Text( value, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, - color: Colors.black87, + color: Color(0xFF1A1A2E), ), ), ], diff --git a/spk_mobile/lib/screens/recommendation_screen.dart b/spk_mobile/lib/screens/recommendation_screen.dart index ebaf54d..a063642 100644 --- a/spk_mobile/lib/screens/recommendation_screen.dart +++ b/spk_mobile/lib/screens/recommendation_screen.dart @@ -239,7 +239,7 @@ class _RecommendationScreenState extends State { } catch (e) { setState(() { _errorMessage = - 'Tidak dapat terhubung ke server. Pastikan server Laravel aktif dan IP di app_config.dart benar.'; + 'Tidak dapat terhubung ke server. Periksa koneksi internet Anda dan coba lagi.'; _hasCalculated = true; }); } finally { diff --git a/spk_mobile/lib/screens/search_screen.dart b/spk_mobile/lib/screens/search_screen.dart index 261efa0..33ff122 100644 --- a/spk_mobile/lib/screens/search_screen.dart +++ b/spk_mobile/lib/screens/search_screen.dart @@ -6,6 +6,7 @@ import '../services/kontrakan_service.dart'; import '../services/laundry_service.dart'; import '../services/auth_service.dart'; import '../services/favorite_service.dart'; +import '../utils/currency_formatter.dart'; import 'kontrakan_detail_screen.dart'; import 'laundry_detail_screen.dart'; @@ -71,7 +72,7 @@ class _SearchScreenState extends State { }); } } catch (e) { - print('Error loading favorite ids: $e'); + // Error loading favorite ids silently } } @@ -434,71 +435,91 @@ class _SearchScreenState extends State { Widget _buildKontrakanList() { if (_filteredKontrakan.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + return RefreshIndicator( + onRefresh: _loadData, + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), children: [ + SizedBox(height: MediaQuery.of(context).size.height * 0.2), Icon(Icons.search_off, size: 80, color: Colors.grey[300]), const SizedBox(height: 16), - Text( - 'Tidak ada hasil', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.grey[600], + Center( + child: Text( + 'Tidak ada hasil', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + ), ), ), const SizedBox(height: 8), - Text( - 'Coba kata kunci lain', - style: TextStyle(fontSize: 14, color: Colors.grey[500]), + Center( + child: Text( + 'Coba kata kunci lain', + style: TextStyle(fontSize: 14, color: Colors.grey[500]), + ), ), ], ), ); } - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: _filteredKontrakan.length, - itemBuilder: (context, index) { - return _buildKontrakanItem(_filteredKontrakan[index]); - }, + return RefreshIndicator( + onRefresh: _loadData, + child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + itemCount: _filteredKontrakan.length, + itemBuilder: (context, index) { + return _buildKontrakanItem(_filteredKontrakan[index]); + }, + ), ); } Widget _buildLaundryList() { if (_filteredLaundry.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + return RefreshIndicator( + onRefresh: _loadData, + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), children: [ + SizedBox(height: MediaQuery.of(context).size.height * 0.2), Icon(Icons.search_off, size: 80, color: Colors.grey[300]), const SizedBox(height: 16), - Text( - 'Tidak ada hasil', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.grey[600], + Center( + child: Text( + 'Tidak ada hasil', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + ), ), ), const SizedBox(height: 8), - Text( - 'Coba kata kunci lain', - style: TextStyle(fontSize: 14, color: Colors.grey[500]), + Center( + child: Text( + 'Coba kata kunci lain', + style: TextStyle(fontSize: 14, color: Colors.grey[500]), + ), ), ], ), ); } - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: _filteredLaundry.length, - itemBuilder: (context, index) { - return _buildLaundryItem(_filteredLaundry[index]); - }, + return RefreshIndicator( + onRefresh: _loadData, + child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + itemCount: _filteredLaundry.length, + itemBuilder: (context, index) { + return _buildLaundryItem(_filteredLaundry[index]); + }, + ), ); } @@ -552,9 +573,9 @@ class _SearchScreenState extends State { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.08), - blurRadius: 10, - offset: const Offset(0, 2), + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 12, + offset: const Offset(0, 3), ), ], ), @@ -663,16 +684,18 @@ class _SearchScreenState extends State { ), decoration: BoxDecoration( color: kontrakan.status == 'available' - ? Colors.green - : Colors.orange, - borderRadius: BorderRadius.circular(12), + ? const Color(0xFFE8F5E9) + : const Color(0xFFFFF3E0), + borderRadius: BorderRadius.circular(8), ), child: Text( kontrakan.status == 'available' ? 'Tersedia' : 'Penuh', - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: kontrakan.status == 'available' + ? const Color(0xFF2E7D32) + : const Color(0xFFF57C00), fontSize: 10, fontWeight: FontWeight.w600, ), @@ -702,47 +725,37 @@ class _SearchScreenState extends State { ), ], ), - const SizedBox(height: 8), + const SizedBox(height: 6), Row( children: [ - Icon(Icons.bed, size: 14, color: Colors.grey[600]), + Icon(Icons.bed_rounded, size: 14, color: Colors.grey[500]), const SizedBox(width: 4), Text( '${kontrakan.jumlahKamar} Kamar', style: TextStyle( fontSize: 12, - color: Colors.grey[700], + color: Colors.grey[600], ), ), const SizedBox(width: 12), - Icon(Icons.near_me, size: 14, color: Colors.grey[600]), + Icon(Icons.near_me_rounded, size: 14, color: Colors.grey[500]), const SizedBox(width: 4), Text( '${kontrakan.jarakKampus.toStringAsFixed(1)} km', style: TextStyle( fontSize: 12, - color: Colors.grey[700], + color: Colors.grey[600], ), ), ], ), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.symmetric( - vertical: 6, - horizontal: 10, - ), - decoration: BoxDecoration( - color: const Color(0xFF1565C0).withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - 'Rp ${(kontrakan.harga / 1000).toStringAsFixed(0)}K/bln', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Color(0xFF1565C0), - ), + const SizedBox(height: 10), + Text( + '${kontrakan.formattedHarga}/bln', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: Color(0xFF1565C0), ), ), ], @@ -873,51 +886,41 @@ class _SearchScreenState extends State { ), ], ), - const SizedBox(height: 8), + const SizedBox(height: 6), Row( children: [ Icon( - Icons.access_time, + Icons.schedule_rounded, size: 14, - color: Colors.grey[600], + color: Colors.grey[500], ), const SizedBox(width: 4), Text( - '${laundry.estimasiSelesai}jam', + '${laundry.waktuProses} jam', style: TextStyle( fontSize: 12, - color: Colors.grey[700], + color: Colors.grey[600], ), ), const SizedBox(width: 12), - Icon(Icons.near_me, size: 14, color: Colors.grey[600]), + Icon(Icons.near_me_rounded, size: 14, color: Colors.grey[500]), const SizedBox(width: 4), Text( '${laundry.jarak.toStringAsFixed(1)} km', style: TextStyle( fontSize: 12, - color: Colors.grey[700], + color: Colors.grey[600], ), ), ], ), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.symmetric( - vertical: 6, - horizontal: 10, - ), - decoration: BoxDecoration( - color: const Color(0xFF00897B).withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - 'Rp ${(laundry.hargaKiloan / 1000).toStringAsFixed(0)}K/kg', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Color(0xFF00897B), - ), + const SizedBox(height: 10), + Text( + '${laundry.formattedHarga}/kg', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: Color(0xFF00897B), ), ), ], @@ -946,7 +949,7 @@ class _SearchScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - 'Rp ${(_priceRange.start / 1000000).toStringAsFixed(1)}jt - Rp ${(_priceRange.end / 1000000).toStringAsFixed(1)}jt', + '${CurrencyFormatter.formatCompact(_priceRange.start)} - ${CurrencyFormatter.formatCompact(_priceRange.end)}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -961,8 +964,8 @@ class _SearchScreenState extends State { divisions: 20, activeColor: const Color(0xFF1565C0), labels: RangeLabels( - 'Rp ${(_priceRange.start / 1000).toStringAsFixed(0)}K', - 'Rp ${(_priceRange.end / 1000).toStringAsFixed(0)}K', + CurrencyFormatter.formatCompact(_priceRange.start), + CurrencyFormatter.formatCompact(_priceRange.end), ), onChanged: (values) { setDialogState(() => _priceRange = values); diff --git a/spk_mobile/lib/services/booking_service.dart b/spk_mobile/lib/services/booking_service.dart index af7fa11..5b6ae52 100644 --- a/spk_mobile/lib/services/booking_service.dart +++ b/spk_mobile/lib/services/booking_service.dart @@ -38,7 +38,7 @@ class BookingService { } return []; } catch (e) { - print('Error getting booking history: $e'); + // Error getting booking history silently return []; } } @@ -139,7 +139,7 @@ class BookingService { } return null; } catch (e) { - print('Error getting booking detail: $e'); + // Error getting booking detail silently return null; } } diff --git a/spk_mobile/lib/services/favorite_service.dart b/spk_mobile/lib/services/favorite_service.dart index b32ed41..9bb3285 100644 --- a/spk_mobile/lib/services/favorite_service.dart +++ b/spk_mobile/lib/services/favorite_service.dart @@ -45,7 +45,7 @@ class FavoriteService { }; } } catch (e) { - print('Error getting favorites: $e'); + // Error getting favorites silently return { 'success': false, 'kontrakan': [], @@ -74,7 +74,7 @@ class FavoriteService { 'laundry': laundryList, }; } catch (e) { - print('Error parsing favorites: $e'); + // Error parsing favorites silently return { 'success': false, 'kontrakan': [], @@ -191,7 +191,7 @@ class FavoriteService { final kontrakanList = favorites['kontrakan'] as List? ?? []; return kontrakanList.contains(kontrakanId); } catch (e) { - print('Error checking favorite: $e'); + // Error checking favorite silently return false; } } @@ -203,7 +203,7 @@ class FavoriteService { final laundryList = favorites['laundry'] as List? ?? []; return laundryList.contains(laundryId); } catch (e) { - print('Error checking favorite: $e'); + // Error checking favorite silently return false; } } diff --git a/spk_mobile/lib/services/kontrakan_service.dart b/spk_mobile/lib/services/kontrakan_service.dart index 9f90823..fefce98 100644 --- a/spk_mobile/lib/services/kontrakan_service.dart +++ b/spk_mobile/lib/services/kontrakan_service.dart @@ -56,7 +56,7 @@ class KontrakanService { } return []; } catch (e) { - print('Error getting kontrakan: $e'); + // Error getting kontrakan silently return []; } } @@ -77,7 +77,7 @@ class KontrakanService { } return null; } catch (e) { - print('Error getting kontrakan detail: $e'); + // Error getting kontrakan detail silently return null; } } diff --git a/spk_mobile/lib/services/laundry_service.dart b/spk_mobile/lib/services/laundry_service.dart index 5a94ad6..cabef9b 100644 --- a/spk_mobile/lib/services/laundry_service.dart +++ b/spk_mobile/lib/services/laundry_service.dart @@ -27,9 +27,6 @@ class LaundryService { .get(Uri.parse('${AppConfig.baseUrl}/laundry'), headers: _headers) .timeout(AppConfig.connectionTimeout); - print('Laundry API Response: ${response.statusCode}'); - print('Laundry API Body: ${response.body}'); - if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['success'] == true) { @@ -39,7 +36,7 @@ class LaundryService { } return []; } catch (e) { - print('Error getting laundry: $e'); + // Error getting laundry silently return []; } } @@ -59,7 +56,7 @@ class LaundryService { } return null; } catch (e) { - print('Error getting laundry detail: $e'); + // Error getting laundry detail silently return null; } } diff --git a/spk_mobile/lib/services/location_service.dart b/spk_mobile/lib/services/location_service.dart index d07b119..3b675f4 100644 --- a/spk_mobile/lib/services/location_service.dart +++ b/spk_mobile/lib/services/location_service.dart @@ -38,7 +38,7 @@ class LocationService { return position; } catch (e) { - print('Error getting location: $e'); + // Error getting location silently return null; } } diff --git a/spk_mobile/lib/utils/currency_formatter.dart b/spk_mobile/lib/utils/currency_formatter.dart new file mode 100644 index 0000000..b74418e --- /dev/null +++ b/spk_mobile/lib/utils/currency_formatter.dart @@ -0,0 +1,45 @@ +/// Utility class for formatting Indonesian Rupiah currency +class CurrencyFormatter { + /// Format a number to full Rupiah format: "Rp 8.500.000" + static String formatRupiah(double amount) { + final formatted = amount + .toStringAsFixed(0) + .replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]}.', + ); + return 'Rp $formatted'; + } + + /// Format to compact readable format: + /// - < 1000: "Rp 500" + /// - 1000-999999: "Rp 5rb" or "Rp 50rb" + /// - 1000000+: "Rp 1jt" or "Rp 8,5jt" + static String formatCompact(double amount) { + if (amount >= 1000000) { + final juta = amount / 1000000; + if (juta == juta.roundToDouble()) { + return 'Rp ${juta.toStringAsFixed(0)}jt'; + } + return 'Rp ${juta.toStringAsFixed(1).replaceAll('.0', '')}jt'; + } else if (amount >= 1000) { + final ribu = amount / 1000; + if (ribu == ribu.roundToDouble()) { + return 'Rp ${ribu.toStringAsFixed(0)}rb'; + } + return 'Rp ${ribu.toStringAsFixed(1).replaceAll('.0', '')}rb'; + } + return 'Rp ${amount.toStringAsFixed(0)}'; + } + + /// Format for price badge on cards - clear and unambiguous + /// e.g. "Rp 8.500.000" for millions, "Rp 5.000" for thousands + static String formatCardPrice(double amount) { + return formatRupiah(amount); + } + + /// Format for filter slider labels - compact + static String formatSlider(double amount) { + return formatCompact(amount); + } +} diff --git a/spk_mobile/lib/widgets/kontrakan_card.dart b/spk_mobile/lib/widgets/kontrakan_card.dart index f5e40ac..7e0c52d 100644 --- a/spk_mobile/lib/widgets/kontrakan_card.dart +++ b/spk_mobile/lib/widgets/kontrakan_card.dart @@ -29,10 +29,12 @@ class KontrakanCard extends StatelessWidget { ); }, child: Card( - elevation: 3, + elevation: 2, + shadowColor: Colors.black.withOpacity(0.1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), + clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -177,25 +179,27 @@ class KontrakanCard extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - kontrakan.formattedHarga, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color(0xFF1565C0), + Flexible( + child: Row( + children: [ + Text( + kontrakan.formattedHarga, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1565C0), + ), ), - ), - const Text( - '/bulan', - style: TextStyle( - fontSize: 11, - color: Colors.grey, + Text( + '/bln', + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + fontWeight: FontWeight.w500, + ), ), - ), - ], + ], + ), ), Container( padding: const EdgeInsets.symmetric( @@ -227,20 +231,20 @@ class KontrakanCard extends StatelessWidget { ), ], ), - const SizedBox(height: 8), + const SizedBox(height: 10), Row( children: [ const Icon( - Icons.directions_walk, + Icons.near_me_rounded, size: 14, color: Colors.orange, ), const SizedBox(width: 4), Text( '${kontrakan.jarakKampus.toStringAsFixed(1)} km dari kampus', - style: const TextStyle( + style: TextStyle( fontSize: 11, - color: Colors.grey, + color: Colors.grey[500], ), ), ], diff --git a/spk_mobile/lib/widgets/laundry_card.dart b/spk_mobile/lib/widgets/laundry_card.dart index 57e19b4..8b36a98 100644 --- a/spk_mobile/lib/widgets/laundry_card.dart +++ b/spk_mobile/lib/widgets/laundry_card.dart @@ -29,10 +29,12 @@ class LaundryCard extends StatelessWidget { ); }, child: Card( - elevation: 3, + elevation: 2, + shadowColor: Colors.black.withOpacity(0.1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), + clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -177,25 +179,27 @@ class LaundryCard extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - laundry.formattedHarga, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color(0xFF00897B), + Flexible( + child: Row( + children: [ + Text( + laundry.formattedHarga, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF00897B), + ), ), - ), - const Text( - '/kg', - style: TextStyle( - fontSize: 11, - color: Colors.grey, + Text( + '/kg', + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + fontWeight: FontWeight.w500, + ), ), - ), - ], + ], + ), ), Container( padding: const EdgeInsets.symmetric(