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

This commit is contained in:
micko samawa 2026-02-24 19:42:59 +07:00
parent d94e7db501
commit 7453d3b322
18 changed files with 416 additions and 259 deletions

View File

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

View File

@ -198,11 +198,7 @@ class _LoginScreenState extends State<LoginScreen> {
}
Future<void> _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<LoginScreen> {
}
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<LoginScreen> {
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'),

View File

@ -349,13 +349,14 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
Widget _buildBookingList(List<Booking> 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<BookingHistoryScreen>
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<BookingHistoryScreen>
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]);
},
),
);
}

View File

@ -368,13 +368,25 @@ class _FavoritesScreenState extends State<FavoritesScreen>
],
),
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<FavoritesScreen>
],
),
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(

View File

@ -1000,12 +1000,12 @@ class _HomeScreenState extends State<HomeScreen> {
}
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() {

View File

@ -56,7 +56,7 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
_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<ImprovedHomeScreen> {
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<ImprovedHomeScreen> {
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<ImprovedHomeScreen> {
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<ImprovedHomeScreen> {
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(

View File

@ -145,16 +145,36 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
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<KontrakanDetailScreen> {
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<KontrakanDetailScreen> {
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<KontrakanDetailScreen> {
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),
),
),
],

View File

@ -329,19 +329,19 @@ class _LaundryDetailScreenState extends State<LaundryDetailScreen> {
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<LaundryDetailScreen> {
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<LaundryDetailScreen> {
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<LaundryDetailScreen> {
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),
),
),
],

View File

@ -239,7 +239,7 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
} 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 {

View File

@ -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<SearchScreen> {
});
}
} catch (e) {
print('Error loading favorite ids: $e');
// Error loading favorite ids silently
}
}
@ -434,71 +435,91 @@ class _SearchScreenState extends State<SearchScreen> {
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<SearchScreen> {
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<SearchScreen> {
),
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<SearchScreen> {
),
],
),
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<SearchScreen> {
),
],
),
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<SearchScreen> {
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<SearchScreen> {
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);

View File

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

View File

@ -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': <Kontrakan>[],
@ -191,7 +191,7 @@ class FavoriteService {
final kontrakanList = favorites['kontrakan'] as List<int>? ?? [];
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<int>? ?? [];
return laundryList.contains(laundryId);
} catch (e) {
print('Error checking favorite: $e');
// Error checking favorite silently
return false;
}
}

View File

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

View File

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

View File

@ -38,7 +38,7 @@ class LocationService {
return position;
} catch (e) {
print('Error getting location: $e');
// Error getting location silently
return null;
}
}

View File

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

View File

@ -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],
),
),
],

View File

@ -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(