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:
parent
d94e7db501
commit
7453d3b322
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class LocationService {
|
|||
|
||||
return position;
|
||||
} catch (e) {
|
||||
print('Error getting location: $e');
|
||||
// Error getting location silently
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue