import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../widgets/app_placeholder.dart'; import '../models/kontrakan.dart'; import '../models/laundry.dart'; import '../services/kontrakan_service.dart'; import '../services/laundry_service.dart'; import '../services/auth_service.dart'; import 'search_screen.dart'; import 'kontrakan_detail_screen.dart'; import 'laundry_list_screen.dart'; import 'laundry_detail_screen.dart'; import 'booking_history_screen.dart'; import 'profile_screen.dart'; import '../login.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { final _kontrakanService = KontrakanService(); final _laundryService = LaundryService(); final _authService = AuthService(); List _kontrakanList = []; List _laundryList = []; bool _isLoading = true; int _selectedIndex = 0; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { setState(() => _isLoading = true); final results = await Future.wait([ _kontrakanService.getKontrakan(), _laundryService.getLaundry(), ]); setState(() { _kontrakanList = results[0] as List; _laundryList = results[1] as List; _isLoading = false; }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF8F9FD), body: _buildBody(), bottomNavigationBar: _buildBottomNav(), ); } Widget _buildBody() { switch (_selectedIndex) { case 0: return _buildHomeContent(); case 1: return const SearchScreen(); case 2: return const BookingHistoryScreen(); case 3: return const ProfileScreen(); default: return _buildHomeContent(); } } Widget _buildHomeContent() { return RefreshIndicator( onRefresh: _loadData, color: const Color(0xFF6C63FF), child: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ // GRADIENT HEADER SliverAppBar( expandedHeight: 220, floating: false, pinned: true, backgroundColor: const Color(0xFF6C63FF), flexibleSpace: FlexibleSpaceBar( background: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Color(0xFF6C63FF), Color(0xFF5A52D5), Color(0xFF7B73FF), ], ), ), child: SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.end, children: [ Row( children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(14), ), child: const Icon( Icons.dashboard_rounded, color: Colors.white, size: 28, ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Rekomendasi', style: TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: 0.3, height: 1.2, ), ), const Text( 'Kontrakan & Laundry', style: TextStyle( color: Colors.white70, fontSize: 16, fontWeight: FontWeight.w500, height: 1.3, ), ), const SizedBox(height: 2), Text( 'Halo, ${_authService.currentUser?.name ?? "User"} 👋', style: TextStyle( color: Colors.white.withValues( alpha: 0.85, ), fontSize: 13, fontWeight: FontWeight.w400, ), ), ], ), ), Container( decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), ), child: IconButton( icon: const Icon( Icons.logout_rounded, color: Colors.white, size: 24, ), onPressed: _handleLogout, ), ), ], ), const SizedBox(height: 18), // Search Bar GestureDetector( onTap: () => setState(() => _selectedIndex = 1), child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 13, ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.08), blurRadius: 15, offset: const Offset(0, 5), ), ], ), child: Row( children: [ Icon( Icons.search_rounded, color: Colors.grey[400], size: 22, ), const SizedBox(width: 10), Text( 'Cari kontrakan atau laundry...', style: TextStyle( color: Colors.grey[400], fontSize: 15, ), ), const Spacer(), Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: const Color( 0xFF6C63FF, ).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.tune_rounded, color: Color(0xFF6C63FF), size: 18, ), ), ], ), ), ), ], ), ), ), ), ), ), // QUICK STATS SliverToBoxAdapter( child: Container( margin: const EdgeInsets.fromLTRB(16, 20, 16, 8), child: Row( children: [ Expanded( child: _buildStatCard( icon: Icons.home_work_rounded, title: 'Kontrakan', value: '${_kontrakanList.length}', color: const Color(0xFF6C63FF), ), ), const SizedBox(width: 10), Expanded( child: _buildStatCard( icon: Icons.local_laundry_service_rounded, title: 'Laundry', value: '${_laundryList.length}', color: const Color(0xFF00BFA5), ), ), const SizedBox(width: 10), Expanded( child: _buildStatCard( icon: Icons.bookmark_rounded, title: 'Booking', value: '0', color: const Color(0xFFFF6B6B), ), ), ], ), ), ), // KATEGORI QUICK ACCESS SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Kategori', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF2D3142), ), ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildCategoryCard( icon: Icons.home_work_rounded, title: 'Kontrakan', subtitle: 'Cari tempat tinggal', gradientColors: [ const Color(0xFF6C63FF), const Color(0xFF8B83FF), ], onTap: () => setState(() => _selectedIndex = 1), ), ), const SizedBox(width: 12), Expanded( child: _buildCategoryCard( icon: Icons.local_laundry_service_rounded, title: 'Laundry', subtitle: 'Layanan terdekat', gradientColors: [ const Color(0xFF00BFA5), const Color(0xFF26D9B8), ], onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const LaundryListScreen(), ), ); }, ), ), ], ), ], ), ), ), // KONTRAKAN SECTION TITLE SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: const Color(0xFF6C63FF).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.home_work_rounded, color: Color(0xFF6C63FF), size: 18, ), ), const SizedBox(width: 10), const Text( 'Kontrakan Tersedia', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF2D3142), ), ), ], ), TextButton( onPressed: () => setState(() => _selectedIndex = 1), style: TextButton.styleFrom( foregroundColor: const Color(0xFF6C63FF), ), child: const Row( children: [ Text( 'Semua', style: TextStyle(fontWeight: FontWeight.w600), ), SizedBox(width: 4), Icon(Icons.arrow_forward_ios_rounded, size: 14), ], ), ), ], ), ), ), // Kontrakan Horizontal List _isLoading ? const SliverToBoxAdapter( child: SizedBox( height: 230, child: Center( child: CircularProgressIndicator( color: Color(0xFF6C63FF), ), ), ), ) : _kontrakanList.isEmpty ? SliverToBoxAdapter( child: _buildEmptyState( icon: Icons.home_work_outlined, message: 'Belum ada kontrakan', ), ) : SliverToBoxAdapter( child: SizedBox( height: 265, child: ListView.builder( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: _kontrakanList.length > 6 ? 6 : _kontrakanList.length, itemBuilder: (context, index) { return _buildKontrakanCard(_kontrakanList[index]); }, ), ), ), // LAUNDRY SECTION TITLE SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: const Color(0xFF00BFA5).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.local_laundry_service_rounded, color: Color(0xFF00BFA5), size: 18, ), ), const SizedBox(width: 10), const Text( 'Laundry Terdekat', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF2D3142), ), ), ], ), TextButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const LaundryListScreen(), ), ); }, style: TextButton.styleFrom( foregroundColor: const Color(0xFF00BFA5), ), child: const Row( children: [ Text( 'Semua', style: TextStyle(fontWeight: FontWeight.w600), ), SizedBox(width: 4), Icon(Icons.arrow_forward_ios_rounded, size: 14), ], ), ), ], ), ), ), // Laundry Vertical List _isLoading ? const SliverToBoxAdapter( child: SizedBox( height: 150, child: Center( child: CircularProgressIndicator( color: Color(0xFF00BFA5), ), ), ), ) : _laundryList.isEmpty ? SliverToBoxAdapter( child: _buildEmptyState( icon: Icons.local_laundry_service_outlined, message: 'Belum ada laundry', ), ) : SliverPadding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, index) { return _buildLaundryCard(_laundryList[index]); }, childCount: _laundryList.length > 5 ? 5 : _laundryList.length, ), ), ), ], ), ); } Widget _buildStatCard({ required IconData icon, required String title, required String value, required Color color, }) { return Container( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: color.withValues(alpha: 0.08), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Column( children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: color, size: 22), ), const SizedBox(height: 8), Text( value, style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: color, ), ), const SizedBox(height: 2), Text( title, style: TextStyle( fontSize: 11, color: Colors.grey[600], fontWeight: FontWeight.w500, ), ), ], ), ); } Widget _buildCategoryCard({ required IconData icon, required String title, required String subtitle, required List gradientColors, required VoidCallback onTap, }) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: gradientColors, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: gradientColors[0].withValues(alpha: 0.3), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.25), borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: Colors.white, size: 24), ), const Icon( Icons.arrow_forward_ios_rounded, color: Colors.white70, size: 16, ), ], ), const SizedBox(height: 14), Text( title, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 2), Text( subtitle, style: TextStyle( color: Colors.white.withValues(alpha: 0.8), fontSize: 12, ), ), ], ), ), ); } Widget _buildEmptyState({required IconData icon, required String message}) { return Container( height: 150, alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 50, color: Colors.grey[300]), const SizedBox(height: 8), Text( message, style: TextStyle( fontSize: 15, color: Colors.grey[500], fontWeight: FontWeight.w500, ), ), ], ), ); } Widget _buildKontrakanCard(Kontrakan kontrakan) { return GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => KontrakanDetailScreen(kontrakan: kontrakan), ), ); }, child: Container( width: 200, margin: const EdgeInsets.only(right: 14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.06), blurRadius: 14, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Image Stack( children: [ ClipRRect( borderRadius: const BorderRadius.vertical( top: Radius.circular(18), ), child: kontrakan.hasPhoto && kontrakan.primaryPhoto.isNotEmpty ? CachedNetworkImage( imageUrl: kontrakan.primaryPhoto, height: 130, width: double.infinity, fit: BoxFit.cover, placeholder: (context, url) => Container( height: 130, color: Colors.grey[100], child: const Center( child: CircularProgressIndicator( strokeWidth: 2, color: Color(0xFF6C63FF), ), ), ), errorWidget: (context, url, error) => Container( height: 130, color: Colors.grey[100], child: const Center( child: AppPlaceholder( height: 56, width: 56, fit: BoxFit.contain, ), ), ), ) : Container( height: 130, color: Colors.grey[100], child: const Center( child: AppPlaceholder( height: 56, width: 56, fit: BoxFit.contain, ), ), ), ), // Status badge Positioned( top: 8, left: 8, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration( color: kontrakan.status == 'available' ? const Color(0xFF00BFA5) : Colors.orange, borderRadius: BorderRadius.circular(20), ), child: Text( kontrakan.status == 'available' ? 'Tersedia' : 'Penuh', style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.w600, ), ), ), ), ], ), // Info Expanded( child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( kontrakan.nama, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Color(0xFF2D3142), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Row( children: [ const Icon( Icons.location_on_rounded, size: 12, color: Color(0xFF6C63FF), ), const SizedBox(width: 3), Expanded( child: Text( kontrakan.alamat, style: TextStyle( fontSize: 11, color: Colors.grey[600], ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const Spacer(), Row( children: [ Icon( Icons.bed_rounded, size: 13, color: Colors.grey[500], ), const SizedBox(width: 3), Text( '${kontrakan.jumlahKamar}', style: TextStyle( fontSize: 11, color: Colors.grey[600], fontWeight: FontWeight.w500, ), ), const SizedBox(width: 10), Icon( Icons.near_me_rounded, size: 13, color: Colors.grey[500], ), const SizedBox(width: 3), Text( '${kontrakan.jarakKampus.toStringAsFixed(1)} km', style: TextStyle( fontSize: 11, color: Colors.grey[600], fontWeight: FontWeight.w500, ), ), ], ), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric( vertical: 5, horizontal: 10, ), decoration: BoxDecoration( color: const Color(0xFF6C63FF).withValues(alpha: 0.08), borderRadius: BorderRadius.circular(8), ), child: Text( 'Rp ${_formatPrice(kontrakan.harga)}/thn', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Color(0xFF6C63FF), ), ), ), ], ), ), ), ], ), ), ); } Widget _buildLaundryCard(Laundry laundry) { return GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => LaundryDetailScreen(laundry: laundry), ), ); }, child: Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 12, offset: const Offset(0, 3), ), ], ), child: Padding( padding: const EdgeInsets.all(14), child: Row( children: [ // Laundry Image/Icon Container( width: 70, height: 70, decoration: BoxDecoration( color: const Color(0xFF00BFA5).withValues(alpha: 0.08), borderRadius: BorderRadius.circular(14), ), child: laundry.hasPhoto && laundry.primaryPhoto.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(14), child: CachedNetworkImage( imageUrl: laundry.primaryPhoto, width: 70, height: 70, fit: BoxFit.cover, errorWidget: (context, url, error) => const Center( child: AppPlaceholder( height: 40, width: 40, fit: BoxFit.contain, ), ), ), ) : const Center( child: AppPlaceholder( height: 40, width: 40, fit: BoxFit.contain, ), ), ), const SizedBox(width: 14), // Info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( laundry.nama, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: Color(0xFF2D3142), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: laundry.status == 'buka' ? const Color(0xFF00BFA5).withValues(alpha: 0.1) : Colors.red.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Text( laundry.status == 'buka' ? 'Buka' : 'Tutup', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: laundry.status == 'buka' ? const Color(0xFF00BFA5) : Colors.red, ), ), ), ], ), const SizedBox(height: 6), Row( children: [ Icon( Icons.location_on_rounded, size: 13, color: Colors.grey[500], ), const SizedBox(width: 3), Expanded( child: Text( laundry.alamat, style: TextStyle( fontSize: 12, color: Colors.grey[600], ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 8), Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: const Color( 0xFF00BFA5, ).withValues(alpha: 0.08), borderRadius: BorderRadius.circular(6), ), child: Text( 'Rp ${_formatPrice(laundry.hargaPerKg)}/kg', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFF00BFA5), ), ), ), const SizedBox(width: 8), Icon( Icons.near_me_rounded, size: 13, color: Colors.grey[500], ), const SizedBox(width: 3), Text( '${laundry.jarakKampus.toStringAsFixed(1)} km', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), const SizedBox(width: 8), if (laundry.avgRating != null) ...[ const Icon( Icons.star_rounded, size: 14, color: Color(0xFFFFB800), ), const SizedBox(width: 2), Text( laundry.avgRating!.toStringAsFixed(1), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF2D3142), ), ), ], ], ), ], ), ), const SizedBox(width: 8), const Icon( Icons.chevron_right_rounded, color: Colors.grey, size: 22, ), ], ), ), ), ); } String _formatPrice(double price) { return price .toStringAsFixed(0) .replaceAllMapped( RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.', ); } Widget _buildBottomNav() { return Container( decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.06), blurRadius: 12, offset: const Offset(0, -2), ), ], borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildNavItem(Icons.home_rounded, 'Home', 0), _buildNavItem(Icons.search_rounded, 'Cari', 1), _buildNavItem(Icons.bookmark_rounded, 'Booking', 2), _buildNavItem(Icons.person_rounded, 'Profile', 3), ], ), ), ), ); } Widget _buildNavItem(IconData icon, String label, int index) { final isSelected = _selectedIndex == index; return GestureDetector( onTap: () => setState(() => _selectedIndex = index), child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: isSelected ? const Color(0xFF6C63FF).withValues(alpha: 0.1) : Colors.transparent, borderRadius: BorderRadius.circular(14), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( icon, color: isSelected ? const Color(0xFF6C63FF) : Colors.grey[400], size: 24, ), const SizedBox(height: 3), Text( label, style: TextStyle( color: isSelected ? const Color(0xFF6C63FF) : Colors.grey[400], fontSize: 11, fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500, ), ), ], ), ), ); } Future _handleLogout() async { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: const Text('Konfirmasi Logout'), content: const Text('Apakah Anda yakin ingin keluar?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Batal'), ), ElevatedButton( onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFFF6B6B), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), child: const Text('Logout'), ), ], ), ); if (confirm == true) { await _authService.logout(); if (mounted) { Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (context) => const LoginScreen()), (route) => false, ); } } } }