Web API Security Improvements: Rate limiting, centralized error handling, N+1 query fixes, CORS/Sanctum config, caching, export system fix, and pagination UI improvements

This commit is contained in:
micko samawa 2026-02-26 00:18:36 +07:00
parent 7453d3b322
commit 79305d29c4
15 changed files with 693 additions and 291 deletions

View File

@ -1,12 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions for location --> <!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:label="spk_mobile" android:label="spk_mobile"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

View File

@ -7,8 +7,8 @@ class AppConfig {
// CATATAN: Untuk real device, ganti IP sesuai dengan IP komputer Anda // CATATAN: Untuk real device, ganti IP sesuai dengan IP komputer Anda
// Cek IP dengan: ipconfig (di Windows) atau ifconfig (di Linux/Mac) // Cek IP dengan: ipconfig (di Windows) atau ifconfig (di Linux/Mac)
static const String baseUrl = 'http://192.168.18.16:8000/api'; static const String baseUrl = 'http://10.215.176.99:8000/api';
static const String storageUrl = 'http://192.168.18.16:8000/storage'; static const String storageUrl = 'http://10.215.176.99:8000/storage';
// Timeouts // Timeouts
static const Duration connectionTimeout = Duration(seconds: 10); static const Duration connectionTimeout = Duration(seconds: 10);

View File

@ -104,7 +104,6 @@ class _LoginScreenState extends State<LoginScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Email Field // Email Field
_buildTextField( _buildTextField(
label: 'Email', label: 'Email',
@ -287,7 +286,10 @@ class _LoginScreenState extends State<LoginScreen> {
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Color(0xFF1565C0), width: 1.5), borderSide: const BorderSide(
color: Color(0xFF1565C0),
width: 1.5,
),
), ),
errorBorder: OutlineInputBorder( errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
@ -309,9 +311,7 @@ class _LoginScreenState extends State<LoginScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF7F8FC), color: const Color(0xFFF7F8FC),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(color: const Color(0xFFE0E0E0)),
color: const Color(0xFFE0E0E0),
),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -324,7 +324,11 @@ class _LoginScreenState extends State<LoginScreen> {
color: const Color(0xFF1565C0).withOpacity(0.08), color: const Color(0xFF1565C0).withOpacity(0.08),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: const Icon(Icons.school_rounded, color: Color(0xFF1565C0), size: 22), child: const Icon(
Icons.school_rounded,
color: Color(0xFF1565C0),
size: 22,
),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
const Expanded( const Expanded(

View File

@ -57,10 +57,16 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Row( title: Row(
children: [ children: [
Icon(Icons.warning_amber_rounded, color: Colors.red.shade600, size: 24), Icon(
Icons.warning_amber_rounded,
color: Colors.red.shade600,
size: 24,
),
const SizedBox(width: 10), const SizedBox(width: 10),
const Text('Batalkan Booking', const Text(
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), 'Batalkan Booking',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
], ],
), ),
content: const Text('Apakah Anda yakin ingin membatalkan booking ini?'), content: const Text('Apakah Anda yakin ingin membatalkan booking ini?'),
@ -92,8 +98,11 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
content: Row( content: Row(
children: [ children: [
Icon( Icon(
result['success'] == true ? Icons.check_circle_rounded : Icons.error_outline_rounded, result['success'] == true
color: Colors.white, size: 20, ? Icons.check_circle_rounded
: Icons.error_outline_rounded,
color: Colors.white,
size: 20,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded(child: Text(result['message'] ?? '')), Expanded(child: Text(result['message'] ?? '')),
@ -103,7 +112,9 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
? const Color(0xFF2E7D32) ? const Color(0xFF2E7D32)
: const Color(0xFFC62828), : const Color(0xFFC62828),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
), ),
); );
@ -164,8 +175,10 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
size: 22, size: 22,
), ),
), ),
title: const Text('Pilih dari Galeri', title: const Text(
style: TextStyle(fontWeight: FontWeight.w600)), 'Pilih dari Galeri',
style: TextStyle(fontWeight: FontWeight.w600),
),
onTap: () => Navigator.pop(ctx, ImageSource.gallery), onTap: () => Navigator.pop(ctx, ImageSource.gallery),
), ),
ListTile( ListTile(
@ -182,8 +195,10 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
size: 22, size: 22,
), ),
), ),
title: const Text('Ambil Foto', title: const Text(
style: TextStyle(fontWeight: FontWeight.w600)), 'Ambil Foto',
style: TextStyle(fontWeight: FontWeight.w600),
),
onTap: () => Navigator.pop(ctx, ImageSource.camera), onTap: () => Navigator.pop(ctx, ImageSource.camera),
), ),
], ],
@ -213,8 +228,11 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
content: Row( content: Row(
children: [ children: [
Icon( Icon(
result['success'] == true ? Icons.check_circle_rounded : Icons.error_outline_rounded, result['success'] == true
color: Colors.white, size: 20, ? Icons.check_circle_rounded
: Icons.error_outline_rounded,
color: Colors.white,
size: 20,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded(child: Text(result['message'] ?? '')), Expanded(child: Text(result['message'] ?? '')),
@ -224,7 +242,9 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
? const Color(0xFF2E7D32) ? const Color(0xFF2E7D32)
: const Color(0xFFC62828), : const Color(0xFFC62828),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
), ),
@ -363,7 +383,9 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: Icon(
isActive ? Icons.bookmark_outline_rounded : Icons.history_rounded, isActive
? Icons.bookmark_outline_rounded
: Icons.history_rounded,
size: 56, size: 56,
color: const Color(0xFF1565C0).withOpacity(0.3), color: const Color(0xFF1565C0).withOpacity(0.3),
), ),
@ -562,10 +584,7 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: Divider( child: Divider(height: 1, color: Colors.grey[200]),
height: 1,
color: Colors.grey[200],
),
), ),
_buildDateRow( _buildDateRow(
Icons.logout_rounded, Icons.logout_rounded,
@ -574,10 +593,7 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: Divider( child: Divider(height: 1, color: Colors.grey[200]),
height: 1,
color: Colors.grey[200],
),
), ),
_buildDateRow( _buildDateRow(
Icons.timelapse_rounded, Icons.timelapse_rounded,
@ -641,7 +657,9 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
), ),
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
booking.paymentStatus == 'paid' ? 'Lunas' : 'Belum Bayar', booking.paymentStatus == 'paid'
? 'Lunas'
: 'Belum Bayar',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -661,7 +679,8 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
const SizedBox(height: 12), const SizedBox(height: 12),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
final url = '${AppConfig.storageUrl}/${booking.paymentProof}'; final url =
'${AppConfig.storageUrl}/${booking.paymentProof}';
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => Dialog( builder: (ctx) => Dialog(
@ -685,8 +704,11 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
), ),
child: Row( child: Row(
children: [ children: [
const Icon(Icons.receipt_long_rounded, const Icon(
color: Colors.white, size: 20), Icons.receipt_long_rounded,
color: Colors.white,
size: 20,
),
const SizedBox(width: 10), const SizedBox(width: 10),
const Expanded( const Expanded(
child: Text( child: Text(
@ -704,10 +726,15 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2), color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(
8,
),
),
child: const Icon(
Icons.close_rounded,
color: Colors.white,
size: 18,
), ),
child: const Icon(Icons.close_rounded,
color: Colors.white, size: 18),
), ),
), ),
], ],
@ -742,8 +769,11 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
color: const Color(0xFF2E7D32).withOpacity(0.1), color: const Color(0xFF2E7D32).withOpacity(0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Icon(Icons.image_rounded, child: const Icon(
size: 16, color: Color(0xFF2E7D32)), Icons.image_rounded,
size: 16,
color: Color(0xFF2E7D32),
),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
const Expanded( const Expanded(
@ -756,8 +786,11 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
), ),
), ),
), ),
const Icon(Icons.chevron_right_rounded, const Icon(
color: Color(0xFF2E7D32), size: 20), Icons.chevron_right_rounded,
color: Color(0xFF2E7D32),
size: 20,
),
], ],
), ),
), ),
@ -854,8 +887,18 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
String _formatDate(DateTime date) { String _formatDate(DateTime date) {
const months = [ const months = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jan',
'Jul', 'Agt', 'Sep', 'Okt', 'Nov', 'Des', 'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agt',
'Sep',
'Okt',
'Nov',
'Des',
]; ];
return '${date.day} ${months[date.month - 1]} ${date.year}'; return '${date.day} ${months[date.month - 1]} ${date.year}';
} }

View File

@ -60,8 +60,10 @@ class _FavoritesScreenState extends State<FavoritesScreen>
children: [ children: [
Icon(Icons.favorite_rounded, color: Colors.red.shade400, size: 24), Icon(Icons.favorite_rounded, color: Colors.red.shade400, size: 24),
const SizedBox(width: 10), const SizedBox(width: 10),
const Text('Hapus Favorit', const Text(
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), 'Hapus Favorit',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
], ],
), ),
content: const Text('Hapus dari daftar favorit Anda?'), content: const Text('Hapus dari daftar favorit Anda?'),
@ -99,14 +101,20 @@ class _FavoritesScreenState extends State<FavoritesScreen>
SnackBar( SnackBar(
content: Row( content: Row(
children: [ children: [
const Icon(Icons.check_circle_rounded, color: Colors.white, size: 20), const Icon(
Icons.check_circle_rounded,
color: Colors.white,
size: 20,
),
const SizedBox(width: 10), const SizedBox(width: 10),
Text(result['message'] ?? 'Dihapus dari favorit'), Text(result['message'] ?? 'Dihapus dari favorit'),
], ],
), ),
backgroundColor: const Color(0xFF2E7D32), backgroundColor: const Color(0xFF2E7D32),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
), ),
); );
@ -225,7 +233,10 @@ class _FavoritesScreenState extends State<FavoritesScreen>
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.local_laundry_service_rounded, size: 18), const Icon(
Icons.local_laundry_service_rounded,
size: 18,
),
const SizedBox(width: 6), const SizedBox(width: 6),
Text('Laundry (${_laundryFavorites.length})'), Text('Laundry (${_laundryFavorites.length})'),
], ],
@ -242,14 +253,13 @@ class _FavoritesScreenState extends State<FavoritesScreen>
Expanded( Expanded(
child: _isLoading child: _isLoading
? const Center( ? const Center(
child: CircularProgressIndicator(color: Color(0xFF1565C0)), child: CircularProgressIndicator(
color: Color(0xFF1565C0),
),
) )
: TabBarView( : TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [_buildKontrakanList(), _buildLaundryList()],
_buildKontrakanList(),
_buildLaundryList(),
],
), ),
), ),
], ],
@ -264,7 +274,8 @@ class _FavoritesScreenState extends State<FavoritesScreen>
return _buildEmptyState( return _buildEmptyState(
icon: Icons.home_work_outlined, icon: Icons.home_work_outlined,
title: 'Belum ada kontrakan favorit', title: 'Belum ada kontrakan favorit',
subtitle: 'Tambahkan kontrakan ke favorit dari halaman detail\natau setelah mendapat hasil rekomendasi', subtitle:
'Tambahkan kontrakan ke favorit dari halaman detail\natau setelah mendapat hasil rekomendasi',
); );
} }
@ -322,7 +333,11 @@ class _FavoritesScreenState extends State<FavoritesScreen>
height: 120, height: 120,
color: const Color(0xFFE3F2FD), color: const Color(0xFFE3F2FD),
child: const Center( child: const Center(
child: Icon(Icons.home_work_rounded, color: Color(0xFF90CAF9), size: 32), child: Icon(
Icons.home_work_rounded,
color: Color(0xFF90CAF9),
size: 32,
),
), ),
), ),
errorWidget: (_, __, ___) => Container( errorWidget: (_, __, ___) => Container(
@ -330,7 +345,11 @@ class _FavoritesScreenState extends State<FavoritesScreen>
height: 120, height: 120,
color: const Color(0xFFE3F2FD), color: const Color(0xFFE3F2FD),
child: const Center( child: const Center(
child: Icon(Icons.home_work_rounded, color: Color(0xFF90CAF9), size: 32), child: Icon(
Icons.home_work_rounded,
color: Color(0xFF90CAF9),
size: 32,
),
), ),
), ),
), ),
@ -355,12 +374,19 @@ class _FavoritesScreenState extends State<FavoritesScreen>
const SizedBox(height: 4), const SizedBox(height: 4),
Row( Row(
children: [ children: [
Icon(Icons.location_on_rounded, size: 13, color: Colors.grey[400]), Icon(
Icons.location_on_rounded,
size: 13,
color: Colors.grey[400],
),
const SizedBox(width: 3), const SizedBox(width: 3),
Expanded( Expanded(
child: Text( child: Text(
kontrakan.alamat, kontrakan.alamat,
style: TextStyle(fontSize: 12, color: Colors.grey[500]), style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -391,9 +417,17 @@ class _FavoritesScreenState extends State<FavoritesScreen>
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(
children: [ children: [
_buildInfoChip(Icons.bed_rounded, '${kontrakan.jumlahKamar} Kamar', const Color(0xFF1565C0)), _buildInfoChip(
Icons.bed_rounded,
'${kontrakan.jumlahKamar} Kamar',
const Color(0xFF1565C0),
),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildInfoChip(Icons.directions_walk_rounded, '${kontrakan.jarakKampus.toStringAsFixed(1)} km', const Color(0xFF5C6BC0)), _buildInfoChip(
Icons.directions_walk_rounded,
'${kontrakan.jarakKampus.toStringAsFixed(1)} km',
const Color(0xFF5C6BC0),
),
], ],
), ),
], ],
@ -405,11 +439,18 @@ class _FavoritesScreenState extends State<FavoritesScreen>
children: [ children: [
IconButton( IconButton(
onPressed: () => _removeFavorite('kontrakan', kontrakan.id), onPressed: () => _removeFavorite('kontrakan', kontrakan.id),
icon: const Icon(Icons.favorite_rounded, color: Colors.red, size: 24), icon: const Icon(
Icons.favorite_rounded,
color: Colors.red,
size: 24,
),
splashRadius: 20, splashRadius: 20,
), ),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: kontrakan.isAvailable color: kontrakan.isAvailable
? const Color(0xFFE8F5E9) ? const Color(0xFFE8F5E9)
@ -442,7 +483,8 @@ class _FavoritesScreenState extends State<FavoritesScreen>
return _buildEmptyState( return _buildEmptyState(
icon: Icons.local_laundry_service_outlined, icon: Icons.local_laundry_service_outlined,
title: 'Belum ada laundry favorit', title: 'Belum ada laundry favorit',
subtitle: 'Tambahkan laundry ke favorit dari halaman detail\natau setelah mendapat hasil rekomendasi', subtitle:
'Tambahkan laundry ke favorit dari halaman detail\natau setelah mendapat hasil rekomendasi',
); );
} }
@ -529,12 +571,19 @@ class _FavoritesScreenState extends State<FavoritesScreen>
const SizedBox(height: 4), const SizedBox(height: 4),
Row( Row(
children: [ children: [
Icon(Icons.location_on_rounded, size: 13, color: Colors.grey[400]), Icon(
Icons.location_on_rounded,
size: 13,
color: Colors.grey[400],
),
const SizedBox(width: 3), const SizedBox(width: 3),
Expanded( Expanded(
child: Text( child: Text(
laundry.alamat, laundry.alamat,
style: TextStyle(fontSize: 12, color: Colors.grey[500]), style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -565,9 +614,17 @@ class _FavoritesScreenState extends State<FavoritesScreen>
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(
children: [ children: [
_buildInfoChip(Icons.schedule_rounded, '${laundry.waktuProses}h proses', const Color(0xFF00897B)), _buildInfoChip(
Icons.schedule_rounded,
'${laundry.waktuProses}h proses',
const Color(0xFF00897B),
),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildInfoChip(Icons.access_time_rounded, '${laundry.jamBuka}-${laundry.jamTutup}', const Color(0xFF5C6BC0)), _buildInfoChip(
Icons.access_time_rounded,
'${laundry.jamBuka}-${laundry.jamTutup}',
const Color(0xFF5C6BC0),
),
], ],
), ),
], ],
@ -579,11 +636,18 @@ class _FavoritesScreenState extends State<FavoritesScreen>
children: [ children: [
IconButton( IconButton(
onPressed: () => _removeFavorite('laundry', laundry.id), onPressed: () => _removeFavorite('laundry', laundry.id),
icon: const Icon(Icons.favorite_rounded, color: Colors.red, size: 24), icon: const Icon(
Icons.favorite_rounded,
color: Colors.red,
size: 24,
),
splashRadius: 20, splashRadius: 20,
), ),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: laundry.status == 'buka' color: laundry.status == 'buka'
? const Color(0xFFE8F5E9) ? const Color(0xFFE8F5E9)
@ -625,7 +689,11 @@ class _FavoritesScreenState extends State<FavoritesScreen>
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
label, label,
style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: color), style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: color,
),
), ),
], ],
), ),
@ -649,7 +717,11 @@ class _FavoritesScreenState extends State<FavoritesScreen>
color: const Color(0xFF1565C0).withOpacity(0.06), color: const Color(0xFF1565C0).withOpacity(0.06),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon(icon, size: 56, color: const Color(0xFF1565C0).withOpacity(0.3)), child: Icon(
icon,
size: 56,
color: const Color(0xFF1565C0).withOpacity(0.3),
),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
@ -664,7 +736,11 @@ class _FavoritesScreenState extends State<FavoritesScreen>
Text( Text(
subtitle, subtitle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(fontSize: 13, color: Colors.grey[500], height: 1.5), style: TextStyle(
fontSize: 13,
color: Colors.grey[500],
height: 1.5,
),
), ),
], ],
), ),

View File

@ -149,7 +149,9 @@ class _HomeScreenState extends State<HomeScreen> {
Text( Text(
'Halo, ${_authService.currentUser?.name ?? "User"} 👋', 'Halo, ${_authService.currentUser?.name ?? "User"} 👋',
style: TextStyle( style: TextStyle(
color: Colors.white.withValues(alpha: 0.85), color: Colors.white.withValues(
alpha: 0.85,
),
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
@ -212,7 +214,9 @@ class _HomeScreenState extends State<HomeScreen> {
Container( Container(
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF6C63FF).withValues(alpha: 0.1), color: const Color(
0xFF6C63FF,
).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Icon( child: const Icon(
@ -366,7 +370,10 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
child: const Row( child: const Row(
children: [ children: [
Text('Semua', style: TextStyle(fontWeight: FontWeight.w600)), Text(
'Semua',
style: TextStyle(fontWeight: FontWeight.w600),
),
SizedBox(width: 4), SizedBox(width: 4),
Icon(Icons.arrow_forward_ios_rounded, size: 14), Icon(Icons.arrow_forward_ios_rounded, size: 14),
], ],
@ -383,29 +390,35 @@ class _HomeScreenState extends State<HomeScreen> {
child: SizedBox( child: SizedBox(
height: 230, height: 230,
child: Center( child: Center(
child: CircularProgressIndicator(color: Color(0xFF6C63FF)), child: CircularProgressIndicator(
color: Color(0xFF6C63FF),
),
), ),
), ),
) )
: _kontrakanList.isEmpty : _kontrakanList.isEmpty
? SliverToBoxAdapter(child: _buildEmptyState( ? SliverToBoxAdapter(
icon: Icons.home_work_outlined, child: _buildEmptyState(
message: 'Belum ada kontrakan', icon: Icons.home_work_outlined,
)) message: 'Belum ada kontrakan',
: SliverToBoxAdapter( ),
child: SizedBox( )
height: 265, : SliverToBoxAdapter(
child: ListView.builder( child: SizedBox(
scrollDirection: Axis.horizontal, height: 265,
physics: const BouncingScrollPhysics(), child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal,
itemCount: _kontrakanList.length > 6 ? 6 : _kontrakanList.length, physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) { padding: const EdgeInsets.symmetric(horizontal: 16),
return _buildKontrakanCard(_kontrakanList[index]); itemCount: _kontrakanList.length > 6
}, ? 6
), : _kontrakanList.length,
), itemBuilder: (context, index) {
return _buildKontrakanCard(_kontrakanList[index]);
},
), ),
),
),
// LAUNDRY SECTION TITLE // LAUNDRY SECTION TITLE
SliverToBoxAdapter( SliverToBoxAdapter(
@ -453,7 +466,10 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
child: const Row( child: const Row(
children: [ children: [
Text('Semua', style: TextStyle(fontWeight: FontWeight.w600)), Text(
'Semua',
style: TextStyle(fontWeight: FontWeight.w600),
),
SizedBox(width: 4), SizedBox(width: 4),
Icon(Icons.arrow_forward_ios_rounded, size: 14), Icon(Icons.arrow_forward_ios_rounded, size: 14),
], ],
@ -470,26 +486,32 @@ class _HomeScreenState extends State<HomeScreen> {
child: SizedBox( child: SizedBox(
height: 150, height: 150,
child: Center( child: Center(
child: CircularProgressIndicator(color: Color(0xFF00BFA5)), child: CircularProgressIndicator(
color: Color(0xFF00BFA5),
),
), ),
), ),
) )
: _laundryList.isEmpty : _laundryList.isEmpty
? SliverToBoxAdapter(child: _buildEmptyState( ? SliverToBoxAdapter(
icon: Icons.local_laundry_service_outlined, child: _buildEmptyState(
message: 'Belum ada laundry', icon: Icons.local_laundry_service_outlined,
)) message: 'Belum ada laundry',
: SliverPadding( ),
padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), )
sliver: SliverList( : SliverPadding(
delegate: SliverChildBuilderDelegate( padding: const EdgeInsets.fromLTRB(16, 0, 16, 100),
(context, index) { sliver: SliverList(
return _buildLaundryCard(_laundryList[index]); delegate: SliverChildBuilderDelegate(
}, (context, index) {
childCount: _laundryList.length > 5 ? 5 : _laundryList.length, return _buildLaundryCard(_laundryList[index]);
), },
), childCount: _laundryList.length > 5
? 5
: _laundryList.length,
), ),
),
),
], ],
), ),
); );
@ -673,7 +695,8 @@ class _HomeScreenState extends State<HomeScreen> {
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(
top: Radius.circular(18), top: Radius.circular(18),
), ),
child: kontrakan.fotoUtama != null && child:
kontrakan.fotoUtama != null &&
kontrakan.fotoUtama!.isNotEmpty kontrakan.fotoUtama!.isNotEmpty
? CachedNetworkImage( ? CachedNetworkImage(
imageUrl: imageUrl:
@ -780,7 +803,11 @@ class _HomeScreenState extends State<HomeScreen> {
const Spacer(), const Spacer(),
Row( Row(
children: [ children: [
Icon(Icons.bed_rounded, size: 13, color: Colors.grey[500]), Icon(
Icons.bed_rounded,
size: 13,
color: Colors.grey[500],
),
const SizedBox(width: 3), const SizedBox(width: 3),
Text( Text(
'${kontrakan.jumlahKamar}', '${kontrakan.jumlahKamar}',
@ -791,7 +818,11 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Icon(Icons.near_me_rounded, size: 13, color: Colors.grey[500]), Icon(
Icons.near_me_rounded,
size: 13,
color: Colors.grey[500],
),
const SizedBox(width: 3), const SizedBox(width: 3),
Text( Text(
'${kontrakan.jarakKampus.toStringAsFixed(1)} km', '${kontrakan.jarakKampus.toStringAsFixed(1)} km',
@ -867,7 +898,8 @@ class _HomeScreenState extends State<HomeScreen> {
color: const Color(0xFF00BFA5).withValues(alpha: 0.08), color: const Color(0xFF00BFA5).withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
), ),
child: laundry.fotoUtama != null && laundry.fotoUtama!.isNotEmpty child:
laundry.fotoUtama != null && laundry.fotoUtama!.isNotEmpty
? ClipRRect( ? ClipRRect(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
child: CachedNetworkImage( child: CachedNetworkImage(
@ -935,12 +967,19 @@ class _HomeScreenState extends State<HomeScreen> {
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(
children: [ children: [
Icon(Icons.location_on_rounded, size: 13, color: Colors.grey[500]), Icon(
Icons.location_on_rounded,
size: 13,
color: Colors.grey[500],
),
const SizedBox(width: 3), const SizedBox(width: 3),
Expanded( Expanded(
child: Text( child: Text(
laundry.alamat, laundry.alamat,
style: TextStyle(fontSize: 12, color: Colors.grey[600]), style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -951,9 +990,14 @@ class _HomeScreenState extends State<HomeScreen> {
Row( Row(
children: [ children: [
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF00BFA5).withValues(alpha: 0.08), color: const Color(
0xFF00BFA5,
).withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Text( child: Text(
@ -966,15 +1010,26 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Icon(Icons.near_me_rounded, size: 13, color: Colors.grey[500]), Icon(
Icons.near_me_rounded,
size: 13,
color: Colors.grey[500],
),
const SizedBox(width: 3), const SizedBox(width: 3),
Text( Text(
'${laundry.jarakKampus.toStringAsFixed(1)} km', '${laundry.jarakKampus.toStringAsFixed(1)} km',
style: TextStyle(fontSize: 12, color: Colors.grey[600]), style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
if (laundry.avgRating != null) ...[ if (laundry.avgRating != null) ...[
const Icon(Icons.star_rounded, size: 14, color: Color(0xFFFFB800)), const Icon(
Icons.star_rounded,
size: 14,
color: Color(0xFFFFB800),
),
const SizedBox(width: 2), const SizedBox(width: 2),
Text( Text(
laundry.avgRating!.toStringAsFixed(1), laundry.avgRating!.toStringAsFixed(1),
@ -991,7 +1046,11 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
const Icon(Icons.chevron_right_rounded, color: Colors.grey, size: 22), const Icon(
Icons.chevron_right_rounded,
color: Colors.grey,
size: 22,
),
], ],
), ),
), ),

View File

@ -403,7 +403,9 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => const RecommendationScreen(category: 'kontrakan'), builder: (_) => const RecommendationScreen(
category: 'kontrakan',
),
), ),
); );
}, },
@ -421,7 +423,9 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => const RecommendationScreen(category: 'laundry'), builder: (_) => const RecommendationScreen(
category: 'laundry',
),
), ),
); );
}, },
@ -451,18 +455,21 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
child: Center(child: CircularProgressIndicator()), child: Center(child: CircularProgressIndicator()),
) )
: _kontrakanList.isEmpty : _kontrakanList.isEmpty
? _buildEmptyState('Belum ada data kontrakan', Icons.home_work_outlined) ? _buildEmptyState(
: SizedBox( 'Belum ada data kontrakan',
height: 250, Icons.home_work_outlined,
child: ListView.builder( )
scrollDirection: Axis.horizontal, : SizedBox(
padding: const EdgeInsets.fromLTRB(20, 0, 8, 0), height: 250,
itemCount: _kontrakanList.length, child: ListView.builder(
itemBuilder: (context, index) { scrollDirection: Axis.horizontal,
return _buildKontrakanCard(_kontrakanList[index]); padding: const EdgeInsets.fromLTRB(20, 0, 8, 0),
}, itemCount: _kontrakanList.length,
), itemBuilder: (context, index) {
), return _buildKontrakanCard(_kontrakanList[index]);
},
),
),
), ),
// Laundry Section // Laundry Section
@ -483,18 +490,21 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
child: Center(child: CircularProgressIndicator()), child: Center(child: CircularProgressIndicator()),
) )
: _laundryList.isEmpty : _laundryList.isEmpty
? _buildEmptyState('Belum ada data laundry', Icons.local_laundry_service_outlined) ? _buildEmptyState(
: SizedBox( 'Belum ada data laundry',
height: 240, Icons.local_laundry_service_outlined,
child: ListView.builder( )
scrollDirection: Axis.horizontal, : SizedBox(
padding: const EdgeInsets.fromLTRB(20, 0, 8, 0), height: 240,
itemCount: _laundryList.length, child: ListView.builder(
itemBuilder: (context, index) { scrollDirection: Axis.horizontal,
return _buildLaundryCard(_laundryList[index]); padding: const EdgeInsets.fromLTRB(20, 0, 8, 0),
}, itemCount: _laundryList.length,
), itemBuilder: (context, index) {
), return _buildLaundryCard(_laundryList[index]);
},
),
),
), ),
// Quick Actions // Quick Actions
@ -589,10 +599,7 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
'Pilih jenis layanan yang ingin Anda cari', 'Pilih jenis layanan yang ingin Anda cari',
style: TextStyle( style: TextStyle(fontSize: 14, color: Colors.grey[500]),
fontSize: 14,
color: Colors.grey[500],
),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Padding( Padding(
@ -603,7 +610,8 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
child: _buildChoiceCard( child: _buildChoiceCard(
icon: Icons.home_work_rounded, icon: Icons.home_work_rounded,
label: 'Kontrakan', label: 'Kontrakan',
description: 'Cari kontrakan terbaik sesuai budget dan kebutuhan', description:
'Cari kontrakan terbaik sesuai budget dan kebutuhan',
color: const Color(0xFF1565C0), color: const Color(0xFF1565C0),
bgColor: const Color(0xFFE3F2FD), bgColor: const Color(0xFFE3F2FD),
onTap: () { onTap: () {
@ -611,7 +619,9 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => const RecommendationScreen(category: 'kontrakan'), builder: (_) => const RecommendationScreen(
category: 'kontrakan',
),
), ),
); );
}, },
@ -622,7 +632,8 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
child: _buildChoiceCard( child: _buildChoiceCard(
icon: Icons.local_laundry_service_rounded, icon: Icons.local_laundry_service_rounded,
label: 'Laundry', label: 'Laundry',
description: 'Cari laundry terbaik dari harga dan kecepatan', description:
'Cari laundry terbaik dari harga dan kecepatan',
color: const Color(0xFF00897B), color: const Color(0xFF00897B),
bgColor: const Color(0xFFE0F2F1), bgColor: const Color(0xFFE0F2F1),
onTap: () { onTap: () {
@ -630,7 +641,8 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => const RecommendationScreen(category: 'laundry'), builder: (_) =>
const RecommendationScreen(category: 'laundry'),
), ),
); );
}, },
@ -745,7 +757,11 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
), ),
), ),
), ),
Icon(Icons.arrow_forward_ios_rounded, size: 14, color: color.withOpacity(0.5)), Icon(
Icons.arrow_forward_ios_rounded,
size: 14,
color: color.withOpacity(0.5),
),
], ],
), ),
), ),
@ -767,8 +783,14 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: isLast borderRadius: isLast
? const BorderRadius.only(topRight: Radius.circular(20), bottomRight: Radius.circular(20)) ? const BorderRadius.only(
: const BorderRadius.only(topLeft: Radius.circular(20), bottomLeft: Radius.circular(20)), topRight: Radius.circular(20),
bottomRight: Radius.circular(20),
)
: const BorderRadius.only(
topLeft: Radius.circular(20),
bottomLeft: Radius.circular(20),
),
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -793,7 +815,11 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Icon(Icons.arrow_forward_rounded, size: 16, color: color.withOpacity(0.6)), Icon(
Icons.arrow_forward_rounded,
size: 16,
color: color.withOpacity(0.6),
),
], ],
), ),
), ),
@ -836,7 +862,9 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
onPressed: onSeeAll, onPressed: onSeeAll,
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -891,7 +919,9 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
Stack( Stack(
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: kontrakan.primaryPhoto, imageUrl: kontrakan.primaryPhoto,
height: 120, height: 120,
@ -902,7 +932,11 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
width: 200, width: 200,
color: const Color(0xFFE3F2FD), color: const Color(0xFFE3F2FD),
child: const Center( child: const Center(
child: Icon(Icons.home_work_rounded, size: 36, color: Color(0xFF90CAF9)), child: Icon(
Icons.home_work_rounded,
size: 36,
color: Color(0xFF90CAF9),
),
), ),
), ),
errorWidget: (_, __, ___) => Container( errorWidget: (_, __, ___) => Container(
@ -910,7 +944,11 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
width: 200, width: 200,
color: const Color(0xFFE3F2FD), color: const Color(0xFFE3F2FD),
child: const Center( child: const Center(
child: Icon(Icons.home_work_rounded, size: 36, color: Color(0xFF90CAF9)), child: Icon(
Icons.home_work_rounded,
size: 36,
color: Color(0xFF90CAF9),
),
), ),
), ),
), ),
@ -920,7 +958,10 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
top: 8, top: 8,
right: 8, right: 8,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: kontrakan.isAvailable color: kontrakan.isAvailable
? const Color(0xFF2E7D32) ? const Color(0xFF2E7D32)
@ -988,12 +1029,19 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
const SizedBox(height: 4), const SizedBox(height: 4),
Row( Row(
children: [ children: [
Icon(Icons.location_on_rounded, size: 12, color: Colors.grey[400]), Icon(
Icons.location_on_rounded,
size: 12,
color: Colors.grey[400],
),
const SizedBox(width: 3), const SizedBox(width: 3),
Expanded( Expanded(
child: Text( child: Text(
kontrakan.alamat, kontrakan.alamat,
style: TextStyle(fontSize: 11, color: Colors.grey[500]), style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -1029,7 +1077,10 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
), ),
), ),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFE3F2FD), color: const Color(0xFFE3F2FD),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
@ -1037,7 +1088,11 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.bed_rounded, size: 12, color: Color(0xFF1565C0)), const Icon(
Icons.bed_rounded,
size: 12,
color: Color(0xFF1565C0),
),
const SizedBox(width: 3), const SizedBox(width: 3),
Text( Text(
'${kontrakan.jumlahKamar}', '${kontrakan.jumlahKamar}',
@ -1055,7 +1110,11 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(
children: [ children: [
Icon(Icons.directions_walk_rounded, size: 12, color: Colors.grey[400]), Icon(
Icons.directions_walk_rounded,
size: 12,
color: Colors.grey[400],
),
const SizedBox(width: 3), const SizedBox(width: 3),
Text( Text(
'${kontrakan.jarakKampus.toStringAsFixed(1)} km dari kampus', '${kontrakan.jarakKampus.toStringAsFixed(1)} km dari kampus',
@ -1104,7 +1163,9 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
Stack( Stack(
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
child: Container( child: Container(
height: 110, height: 110,
width: 200, width: 200,
@ -1129,7 +1190,10 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
top: 8, top: 8,
right: 8, right: 8,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: laundry.status == 'buka' color: laundry.status == 'buka'
? const Color(0xFF2E7D32) ? const Color(0xFF2E7D32)
@ -1197,12 +1261,19 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
const SizedBox(height: 4), const SizedBox(height: 4),
Row( Row(
children: [ children: [
Icon(Icons.location_on_rounded, size: 12, color: Colors.grey[400]), Icon(
Icons.location_on_rounded,
size: 12,
color: Colors.grey[400],
),
const SizedBox(width: 3), const SizedBox(width: 3),
Expanded( Expanded(
child: Text( child: Text(
laundry.alamat, laundry.alamat,
style: TextStyle(fontSize: 11, color: Colors.grey[500]), style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -1238,7 +1309,10 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
), ),
), ),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFE0F2F1), color: const Color(0xFFE0F2F1),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
@ -1246,7 +1320,11 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.schedule_rounded, size: 12, color: Color(0xFF00897B)), const Icon(
Icons.schedule_rounded,
size: 12,
color: Color(0xFF00897B),
),
const SizedBox(width: 3), const SizedBox(width: 3),
Text( Text(
'${laundry.waktuProses}h', '${laundry.waktuProses}h',
@ -1264,7 +1342,11 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(
children: [ children: [
Icon(Icons.access_time_rounded, size: 12, color: Colors.grey[400]), Icon(
Icons.access_time_rounded,
size: 12,
color: Colors.grey[400],
),
const SizedBox(width: 3), const SizedBox(width: 3),
Text( Text(
'${laundry.jamBuka} - ${laundry.jamTutup}', '${laundry.jamBuka} - ${laundry.jamTutup}',
@ -1331,7 +1413,10 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
children: [ children: [
Icon(icon, size: 48, color: Colors.grey[300]), Icon(icon, size: 48, color: Colors.grey[300]),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(message, style: TextStyle(fontSize: 13, color: Colors.grey[500])), Text(
message,
style: TextStyle(fontSize: 13, color: Colors.grey[500]),
),
], ],
), ),
), ),
@ -1343,8 +1428,16 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
const items = [ const items = [
_NavItem(Icons.home_outlined, Icons.home_rounded, 'Beranda'), _NavItem(Icons.home_outlined, Icons.home_rounded, 'Beranda'),
_NavItem(Icons.search_outlined, Icons.search_rounded, 'Cari'), _NavItem(Icons.search_outlined, Icons.search_rounded, 'Cari'),
_NavItem(Icons.favorite_outline_rounded, Icons.favorite_rounded, 'Favorit'), _NavItem(
_NavItem(Icons.bookmark_outline_rounded, Icons.bookmark_rounded, 'Booking'), Icons.favorite_outline_rounded,
Icons.favorite_rounded,
'Favorit',
),
_NavItem(
Icons.bookmark_outline_rounded,
Icons.bookmark_rounded,
'Booking',
),
_NavItem(Icons.person_outline_rounded, Icons.person_rounded, 'Profil'), _NavItem(Icons.person_outline_rounded, Icons.person_rounded, 'Profil'),
]; ];
@ -1403,7 +1496,9 @@ class _ImprovedHomeScreenState extends State<ImprovedHomeScreen> {
items[i].label, items[i].label,
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
color: isSelected color: isSelected
? const Color(0xFF1565C0) ? const Color(0xFF1565C0)
: const Color(0xFF9E9E9E), : const Color(0xFF9E9E9E),

View File

@ -33,13 +33,17 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
} }
Future<void> _checkFavorite() async { Future<void> _checkFavorite() async {
final result = await _favoriteService.isKontrakanFavorite(widget.kontrakan.id); final result = await _favoriteService.isKontrakanFavorite(
widget.kontrakan.id,
);
if (mounted) setState(() => _isFavorite = result); if (mounted) setState(() => _isFavorite = result);
} }
Future<void> _toggleFavorite() async { Future<void> _toggleFavorite() async {
setState(() => _isFavLoading = true); setState(() => _isFavLoading = true);
final result = await _favoriteService.toggleKontrakanFavorite(widget.kontrakan.id); final result = await _favoriteService.toggleKontrakanFavorite(
widget.kontrakan.id,
);
if (mounted) { if (mounted) {
setState(() { setState(() {
_isFavLoading = false; _isFavLoading = false;
@ -52,16 +56,23 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
content: Row( content: Row(
children: [ children: [
Icon( Icon(
_isFavorite ? Icons.favorite_rounded : Icons.favorite_border_rounded, _isFavorite
color: Colors.white, size: 20, ? Icons.favorite_rounded
: Icons.favorite_border_rounded,
color: Colors.white,
size: 20,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text(result['message'] ?? 'Status favorit diubah'), Text(result['message'] ?? 'Status favorit diubah'),
], ],
), ),
backgroundColor: _isFavorite ? const Color(0xFF1565C0) : Colors.grey[700], backgroundColor: _isFavorite
? const Color(0xFF1565C0)
: Colors.grey[700],
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
), ),
@ -94,7 +105,9 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
: IconButton( : IconButton(
onPressed: _toggleFavorite, onPressed: _toggleFavorite,
icon: Icon( icon: Icon(
_isFavorite ? Icons.favorite_rounded : Icons.favorite_border_rounded, _isFavorite
? Icons.favorite_rounded
: Icons.favorite_border_rounded,
color: _isFavorite ? Colors.redAccent : Colors.white, color: _isFavorite ? Colors.redAccent : Colors.white,
size: 26, size: 26,
), ),
@ -150,7 +163,10 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 10,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1565C0).withOpacity(0.08), color: const Color(0xFF1565C0).withOpacity(0.08),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -214,11 +230,16 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
runSpacing: 8, runSpacing: 8,
children: widget.kontrakan.fasilitasList.map((f) { children: widget.kontrakan.fasilitasList.map((f) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFE3F2FD), color: const Color(0xFFE3F2FD),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF1565C0).withOpacity(0.15)), border: Border.all(
color: const Color(0xFF1565C0).withOpacity(0.15),
),
), ),
child: Text( child: Text(
f, f,
@ -245,7 +266,11 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
widget.kontrakan.deskripsi!, widget.kontrakan.deskripsi!,
style: TextStyle(fontSize: 14, height: 1.6, color: Colors.grey[700]), style: TextStyle(
fontSize: 14,
height: 1.6,
color: Colors.grey[700],
),
), ),
], ],
@ -272,15 +297,20 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
child: Row( child: Row(
children: [ children: [
// WhatsApp Button // WhatsApp Button
if (widget.kontrakan.noWhatsapp != null && widget.kontrakan.noWhatsapp!.isNotEmpty) if (widget.kontrakan.noWhatsapp != null &&
widget.kontrakan.noWhatsapp!.isNotEmpty)
Container( Container(
margin: const EdgeInsets.only(right: 12), margin: const EdgeInsets.only(right: 12),
child: ElevatedButton( child: ElevatedButton(
onPressed: () => _openWhatsApp(widget.kontrakan.noWhatsapp!), onPressed: () =>
_openWhatsApp(widget.kontrakan.noWhatsapp!),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF25D366), backgroundColor: const Color(0xFF25D366),
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18), padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 18,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
@ -291,7 +321,13 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
children: [ children: [
Icon(Icons.chat_rounded, size: 20), Icon(Icons.chat_rounded, size: 20),
SizedBox(width: 6), SizedBox(width: 6),
Text('WhatsApp', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), Text(
'WhatsApp',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
], ],
), ),
), ),
@ -305,7 +341,9 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
if (!authService.isAuthenticated) { if (!authService.isAuthenticated) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Silakan login terlebih dahulu untuk booking'), content: Text(
'Silakan login terlebih dahulu untuk booking',
),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@ -314,7 +352,9 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => BookingFormScreen(kontrakan: widget.kontrakan), builder: (_) => BookingFormScreen(
kontrakan: widget.kontrakan,
),
), ),
); );
} }
@ -333,8 +373,13 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
elevation: 0, elevation: 0,
), ),
child: Text( child: Text(
widget.kontrakan.isAvailable ? 'Booking Sekarang' : 'Tidak Tersedia', widget.kontrakan.isAvailable
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700), ? 'Booking Sekarang'
: 'Tidak Tersedia',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
),
), ),
), ),
), ),
@ -362,7 +407,11 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
Expanded( Expanded(
child: Text( child: Text(
text, text,
style: const TextStyle(fontSize: 14, color: Color(0xFF333333), height: 1.3), style: const TextStyle(
fontSize: 14,
color: Color(0xFF333333),
height: 1.3,
),
), ),
), ),
], ],
@ -508,14 +557,21 @@ class _KontrakanDetailScreenState extends State<KontrakanDetailScreen> {
} else if (!cleaned.startsWith('62')) { } else if (!cleaned.startsWith('62')) {
cleaned = '62$cleaned'; cleaned = '62$cleaned';
} }
final message = Uri.encodeComponent('Halo, saya tertarik dengan kontrakan ${widget.kontrakan.nama}'); final message = Uri.encodeComponent(
'Halo, saya tertarik dengan kontrakan ${widget.kontrakan.nama}',
);
final url = Uri.parse('https://wa.me/$cleaned?text=$message'); final url = Uri.parse('https://wa.me/$cleaned?text=$message');
try { try {
await launchUrl(url, mode: LaunchMode.externalApplication); await launchUrl(url, mode: LaunchMode.externalApplication);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tidak dapat membuka WhatsApp. Pastikan WhatsApp terpasang.'), backgroundColor: Colors.red), const SnackBar(
content: Text(
'Tidak dapat membuka WhatsApp. Pastikan WhatsApp terpasang.',
),
backgroundColor: Colors.red,
),
); );
} }
} }

View File

@ -36,7 +36,9 @@ class _LaundryDetailScreenState extends State<LaundryDetailScreen> {
Future<void> _toggleFavorite() async { Future<void> _toggleFavorite() async {
setState(() => _isFavLoading = true); setState(() => _isFavLoading = true);
final result = await _favoriteService.toggleLaundryFavorite(widget.laundry.id); final result = await _favoriteService.toggleLaundryFavorite(
widget.laundry.id,
);
if (mounted) { if (mounted) {
setState(() { setState(() {
_isFavLoading = false; _isFavLoading = false;
@ -49,16 +51,23 @@ class _LaundryDetailScreenState extends State<LaundryDetailScreen> {
content: Row( content: Row(
children: [ children: [
Icon( Icon(
_isFavorite ? Icons.favorite_rounded : Icons.favorite_border_rounded, _isFavorite
color: Colors.white, size: 20, ? Icons.favorite_rounded
: Icons.favorite_border_rounded,
color: Colors.white,
size: 20,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text(result['message'] ?? 'Status favorit diubah'), Text(result['message'] ?? 'Status favorit diubah'),
], ],
), ),
backgroundColor: _isFavorite ? const Color(0xFF00897B) : Colors.grey[700], backgroundColor: _isFavorite
? const Color(0xFF00897B)
: Colors.grey[700],
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
), ),
@ -93,7 +102,9 @@ class _LaundryDetailScreenState extends State<LaundryDetailScreen> {
: IconButton( : IconButton(
onPressed: _toggleFavorite, onPressed: _toggleFavorite,
icon: Icon( icon: Icon(
_isFavorite ? Icons.favorite_rounded : Icons.favorite_border_rounded, _isFavorite
? Icons.favorite_rounded
: Icons.favorite_border_rounded,
color: _isFavorite ? Colors.redAccent : Colors.white, color: _isFavorite ? Colors.redAccent : Colors.white,
size: 26, size: 26,
), ),
@ -616,7 +627,11 @@ class _LaundryDetailScreenState extends State<LaundryDetailScreen> {
children: [ children: [
Text( Text(
label, label,
style: TextStyle(fontSize: 12, color: Colors.grey[500], fontWeight: FontWeight.w500), style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
fontWeight: FontWeight.w500,
),
), ),
const SizedBox(height: 3), const SizedBox(height: 3),
Text( Text(

View File

@ -173,11 +173,11 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
_noData = false; _noData = false;
}); });
try { final endpoint = widget.category == 'kontrakan'
final endpoint = widget.category == 'kontrakan' ? '/saw/calculate/kontrakan'
? '/saw/calculate/kontrakan' : '/saw/calculate/laundry';
: '/saw/calculate/laundry';
try {
final bodyParams = <String, dynamic>{}; final bodyParams = <String, dynamic>{};
bodyParams['bobot_harga'] = _bobotHarga; bodyParams['bobot_harga'] = _bobotHarga;
bodyParams['bobot_jarak'] = _bobotJarak; bodyParams['bobot_jarak'] = _bobotJarak;
@ -237,9 +237,11 @@ class _RecommendationScreenState extends State<RecommendationScreen> {
}); });
} }
} catch (e) { } catch (e) {
debugPrint('SAW API Error: $e');
debugPrint('URL: ${AppConfig.baseUrl}$endpoint');
setState(() { setState(() {
_errorMessage = _errorMessage =
'Tidak dapat terhubung ke server. Periksa koneksi internet Anda dan coba lagi.'; 'Tidak dapat terhubung ke server (${AppConfig.baseUrl}). Periksa koneksi internet Anda dan coba lagi.\n\nDetail: $e';
_hasCalculated = true; _hasCalculated = true;
}); });
} finally { } finally {

View File

@ -79,12 +79,18 @@ class _SearchScreenState extends State<SearchScreen> {
Future<void> _toggleKontrakanFav(int id) async { Future<void> _toggleKontrakanFav(int id) async {
final wasFav = _favKontrakanIds.contains(id); final wasFav = _favKontrakanIds.contains(id);
setState(() { setState(() {
if (wasFav) _favKontrakanIds.remove(id); else _favKontrakanIds.add(id); if (wasFav)
_favKontrakanIds.remove(id);
else
_favKontrakanIds.add(id);
}); });
final result = await _favoriteService.toggleKontrakanFavorite(id); final result = await _favoriteService.toggleKontrakanFavorite(id);
if (result['success'] != true && mounted) { if (result['success'] != true && mounted) {
setState(() { setState(() {
if (wasFav) _favKontrakanIds.add(id); else _favKontrakanIds.remove(id); if (wasFav)
_favKontrakanIds.add(id);
else
_favKontrakanIds.remove(id);
}); });
} }
} }
@ -92,12 +98,18 @@ class _SearchScreenState extends State<SearchScreen> {
Future<void> _toggleLaundryFav(int id) async { Future<void> _toggleLaundryFav(int id) async {
final wasFav = _favLaundryIds.contains(id); final wasFav = _favLaundryIds.contains(id);
setState(() { setState(() {
if (wasFav) _favLaundryIds.remove(id); else _favLaundryIds.add(id); if (wasFav)
_favLaundryIds.remove(id);
else
_favLaundryIds.add(id);
}); });
final result = await _favoriteService.toggleLaundryFavorite(id); final result = await _favoriteService.toggleLaundryFavorite(id);
if (result['success'] != true && mounted) { if (result['success'] != true && mounted) {
setState(() { setState(() {
if (wasFav) _favLaundryIds.add(id); else _favLaundryIds.remove(id); if (wasFav)
_favLaundryIds.add(id);
else
_favLaundryIds.remove(id);
}); });
} }
} }
@ -217,7 +229,11 @@ class _SearchScreenState extends State<SearchScreen> {
color: Colors.white.withOpacity(0.15), color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: const Icon(Icons.search_rounded, color: Colors.white, size: 22), child: const Icon(
Icons.search_rounded,
color: Colors.white,
size: 22,
),
), ),
const SizedBox(width: 14), const SizedBox(width: 14),
const Text( const Text(
@ -236,7 +252,11 @@ class _SearchScreenState extends State<SearchScreen> {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: IconButton( child: IconButton(
icon: const Icon(Icons.tune_rounded, color: Colors.white, size: 22), icon: const Icon(
Icons.tune_rounded,
color: Colors.white,
size: 22,
),
onPressed: _showFilterDialog, onPressed: _showFilterDialog,
), ),
), ),
@ -266,7 +286,13 @@ class _SearchScreenState extends State<SearchScreen> {
: Colors.transparent, : Colors.transparent,
borderRadius: BorderRadius.circular(11), borderRadius: BorderRadius.circular(11),
boxShadow: _selectedCategory == 'Kontrakan' boxShadow: _selectedCategory == 'Kontrakan'
? [BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 4, offset: const Offset(0, 2))] ? [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 4,
offset: const Offset(0, 2),
),
]
: null, : null,
), ),
child: Row( child: Row(
@ -309,7 +335,13 @@ class _SearchScreenState extends State<SearchScreen> {
: Colors.transparent, : Colors.transparent,
borderRadius: BorderRadius.circular(11), borderRadius: BorderRadius.circular(11),
boxShadow: _selectedCategory == 'Laundry' boxShadow: _selectedCategory == 'Laundry'
? [BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 4, offset: const Offset(0, 2))] ? [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 4,
offset: const Offset(0, 2),
),
]
: null, : null,
), ),
child: Row( child: Row(
@ -322,24 +354,24 @@ class _SearchScreenState extends State<SearchScreen> {
: Colors.white.withOpacity(0.85), : Colors.white.withOpacity(0.85),
size: 18, size: 18,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Laundry', 'Laundry',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: _selectedCategory == 'Laundry' color: _selectedCategory == 'Laundry'
? const Color(0xFF00897B) ? const Color(0xFF00897B)
: Colors.white.withOpacity(0.85), : Colors.white.withOpacity(0.85),
),
), ),
), ],
], ),
), ),
), ),
), ),
), ],
], ),
),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
@ -351,11 +383,22 @@ class _SearchScreenState extends State<SearchScreen> {
hintText: _selectedCategory == 'Kontrakan' hintText: _selectedCategory == 'Kontrakan'
? 'Cari kontrakan...' ? 'Cari kontrakan...'
: 'Cari laundry...', : 'Cari laundry...',
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14), hintStyle: TextStyle(
prefixIcon: Icon(Icons.search_rounded, color: Colors.grey[400], size: 20), color: Colors.grey[400],
fontSize: 14,
),
prefixIcon: Icon(
Icons.search_rounded,
color: Colors.grey[400],
size: 20,
),
suffixIcon: _searchController.text.isNotEmpty suffixIcon: _searchController.text.isNotEmpty
? IconButton( ? IconButton(
icon: Icon(Icons.close_rounded, color: Colors.grey[400], size: 20), icon: Icon(
Icons.close_rounded,
color: Colors.grey[400],
size: 20,
),
onPressed: () { onPressed: () {
_searchController.clear(); _searchController.clear();
_filterKontrakan(''); _filterKontrakan('');
@ -591,7 +634,8 @@ class _SearchScreenState extends State<SearchScreen> {
bottomLeft: Radius.circular(16), bottomLeft: Radius.circular(16),
), ),
child: child:
kontrakan.fotoUtama != null && kontrakan.fotoUtama!.isNotEmpty kontrakan.fotoUtama != null &&
kontrakan.fotoUtama!.isNotEmpty
? CachedNetworkImage( ? CachedNetworkImage(
imageUrl: kontrakan.primaryPhoto, imageUrl: kontrakan.primaryPhoto,
width: 120, width: 120,
@ -638,7 +682,10 @@ class _SearchScreenState extends State<SearchScreen> {
color: Colors.white.withOpacity(0.9), color: Colors.white.withOpacity(0.9),
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 4), BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
),
], ],
), ),
child: Icon( child: Icon(
@ -728,7 +775,11 @@ class _SearchScreenState extends State<SearchScreen> {
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(
children: [ children: [
Icon(Icons.bed_rounded, size: 14, color: Colors.grey[500]), Icon(
Icons.bed_rounded,
size: 14,
color: Colors.grey[500],
),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'${kontrakan.jumlahKamar} Kamar', '${kontrakan.jumlahKamar} Kamar',
@ -738,7 +789,11 @@ class _SearchScreenState extends State<SearchScreen> {
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Icon(Icons.near_me_rounded, size: 14, color: Colors.grey[500]), Icon(
Icons.near_me_rounded,
size: 14,
color: Colors.grey[500],
),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'${kontrakan.jarakKampus.toStringAsFixed(1)} km', '${kontrakan.jarakKampus.toStringAsFixed(1)} km',
@ -830,7 +885,10 @@ class _SearchScreenState extends State<SearchScreen> {
color: Colors.white.withOpacity(0.9), color: Colors.white.withOpacity(0.9),
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 4), BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
),
], ],
), ),
child: Icon( child: Icon(
@ -903,7 +961,11 @@ class _SearchScreenState extends State<SearchScreen> {
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Icon(Icons.near_me_rounded, size: 14, color: Colors.grey[500]), Icon(
Icons.near_me_rounded,
size: 14,
color: Colors.grey[500],
),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'${laundry.jarak.toStringAsFixed(1)} km', '${laundry.jarak.toStringAsFixed(1)} km',

View File

@ -54,10 +54,7 @@ class BookingService {
try { try {
// Validate payment proof is provided // Validate payment proof is provided
if (paymentProof == null) { if (paymentProof == null) {
return { return {'success': false, 'message': 'Bukti pembayaran wajib diunggah'};
'success': false,
'message': 'Bukti pembayaran wajib diunggah',
};
} }
// Use multipart request with payment proof // Use multipart request with payment proof
@ -70,7 +67,9 @@ class BookingService {
} }
request.fields['kontrakan_id'] = kontrakanId.toString(); request.fields['kontrakan_id'] = kontrakanId.toString();
request.fields['tanggal_mulai'] = tanggalMulai.toIso8601String().split('T')[0]; request.fields['tanggal_mulai'] = tanggalMulai.toIso8601String().split(
'T',
)[0];
request.fields['durasi_bulan'] = durasiBulan.toString(); request.fields['durasi_bulan'] = durasiBulan.toString();
if (catatan != null) request.fields['catatan'] = catatan; if (catatan != null) request.fields['catatan'] = catatan;
@ -78,7 +77,9 @@ class BookingService {
await http.MultipartFile.fromPath('payment_proof', paymentProof.path), await http.MultipartFile.fromPath('payment_proof', paymentProof.path),
); );
final streamedResponse = await request.send().timeout(const Duration(seconds: 30)); final streamedResponse = await request.send().timeout(
const Duration(seconds: 30),
);
final response = await http.Response.fromStream(streamedResponse); final response = await http.Response.fromStream(streamedResponse);
final data = jsonDecode(response.body); final data = jsonDecode(response.body);

View File

@ -38,19 +38,11 @@ class FavoriteService {
'laundry': data['data']['laundry'] ?? [], 'laundry': data['data']['laundry'] ?? [],
}; };
} else { } else {
return { return {'success': false, 'kontrakan': [], 'laundry': []};
'success': false,
'kontrakan': [],
'laundry': [],
};
} }
} catch (e) { } catch (e) {
// Error getting favorites silently // Error getting favorites silently
return { return {'success': false, 'kontrakan': [], 'laundry': []};
'success': false,
'kontrakan': [],
'laundry': [],
};
} }
} }

View File

@ -31,9 +31,7 @@ class KontrakanCard extends StatelessWidget {
child: Card( child: Card(
elevation: 2, elevation: 2,
shadowColor: Colors.black.withOpacity(0.1), shadowColor: Colors.black.withOpacity(0.1),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -93,7 +91,9 @@ class KontrakanCard extends StatelessWidget {
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: BorderRadius.vertical( borderRadius: BorderRadius.vertical(
top: Radius.circular(showRanking && ranking != null ? 0 : 16), top: Radius.circular(
showRanking && ranking != null ? 0 : 16,
),
), ),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: kontrakan.primaryPhoto, imageUrl: kontrakan.primaryPhoto,
@ -103,9 +103,7 @@ class KontrakanCard extends StatelessWidget {
placeholder: (context, url) => Container( placeholder: (context, url) => Container(
height: 180, height: 180,
color: Colors.grey[300], color: Colors.grey[300],
child: const Center( child: const Center(child: CircularProgressIndicator()),
child: CircularProgressIndicator(),
),
), ),
errorWidget: (context, url, error) => Container( errorWidget: (context, url, error) => Container(
height: 180, height: 180,
@ -123,9 +121,7 @@ class KontrakanCard extends StatelessWidget {
vertical: 6, vertical: 6,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: kontrakan.isAvailable color: kontrakan.isAvailable ? Colors.green : Colors.red,
? Colors.green
: Colors.red,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Text( child: Text(
@ -160,7 +156,11 @@ class KontrakanCard extends StatelessWidget {
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(
children: [ children: [
const Icon(Icons.location_on, size: 14, color: Colors.grey), const Icon(
Icons.location_on,
size: 14,
color: Colors.grey,
),
const SizedBox(width: 4), const SizedBox(width: 4),
Expanded( Expanded(
child: Text( child: Text(
@ -242,10 +242,7 @@ class KontrakanCard extends StatelessWidget {
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'${kontrakan.jarakKampus.toStringAsFixed(1)} km dari kampus', '${kontrakan.jarakKampus.toStringAsFixed(1)} km dari kampus',
style: TextStyle( style: TextStyle(fontSize: 11, color: Colors.grey[500]),
fontSize: 11,
color: Colors.grey[500],
),
), ),
], ],
), ),

View File

@ -31,9 +31,7 @@ class LaundryCard extends StatelessWidget {
child: Card( child: Card(
elevation: 2, elevation: 2,
shadowColor: Colors.black.withOpacity(0.1), shadowColor: Colors.black.withOpacity(0.1),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -93,7 +91,9 @@ class LaundryCard extends StatelessWidget {
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: BorderRadius.vertical( borderRadius: BorderRadius.vertical(
top: Radius.circular(showRanking && ranking != null ? 0 : 16), top: Radius.circular(
showRanking && ranking != null ? 0 : 16,
),
), ),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: laundry.primaryPhoto, imageUrl: laundry.primaryPhoto,
@ -103,9 +103,7 @@ class LaundryCard extends StatelessWidget {
placeholder: (context, url) => Container( placeholder: (context, url) => Container(
height: 180, height: 180,
color: Colors.grey[300], color: Colors.grey[300],
child: const Center( child: const Center(child: CircularProgressIndicator()),
child: CircularProgressIndicator(),
),
), ),
errorWidget: (context, url, error) => Container( errorWidget: (context, url, error) => Container(
height: 180, height: 180,
@ -160,7 +158,11 @@ class LaundryCard extends StatelessWidget {
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(
children: [ children: [
const Icon(Icons.location_on, size: 14, color: Colors.grey), const Icon(
Icons.location_on,
size: 14,
color: Colors.grey,
),
const SizedBox(width: 4), const SizedBox(width: 4),
Expanded( Expanded(
child: Text( child: Text(
@ -252,11 +254,7 @@ class LaundryCard extends StatelessWidget {
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(
children: [ children: [
const Icon( const Icon(Icons.schedule, size: 14, color: Colors.green),
Icons.schedule,
size: 14,
color: Colors.green,
),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'${laundry.jamBuka} - ${laundry.jamTutup}', '${laundry.jamBuka} - ${laundry.jamTutup}',