bookmywages/lib/view/user_main_screens/home_screen.dart
2025-10-16 11:21:52 +05:30

2481 lines
103 KiB
Dart

// Fixed All Services Tab Content Widget - No Loading States with Search Functionality
// ignore_for_file: unused_result
import 'dart:math';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/consts_widgets/user_flow_drawer.dart';
import 'package:bookmywages/model/Categories_model.dart';
import 'package:bookmywages/model/cancel_booking.dart';
import 'package:bookmywages/model/most_popular_model.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/view/user_main_screens/history_screen/Service_Booking.dart';
import 'package:bookmywages/view/user_main_screens/main_contoller.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:bookmywages/viewmodel/consts_api.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../../../consts_widgets/app_assets.dart';
class AllServicesTabContent extends ConsumerWidget {
final String? searchQuery;
const AllServicesTabContent({super.key, this.searchQuery});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Safe access to provider data with error handling
final popularServicesAsync = ref.watch(mostPopularProvider("0"));
// Handle different states safely
final allServices = popularServicesAsync.when(
data: (services) => services ?? [],
loading: () => <MostPopularModel>[],
error: (error, stack) {
// Log error but don't show it to user, just return empty list
debugPrint('Error loading popular services: $error');
return <MostPopularModel>[];
},
);
// Filter services based on search query
final services = searchQuery == null || searchQuery!.isEmpty
? allServices
: allServices.where((service) {
final serviceName = (service.serviceName ?? '').toLowerCase();
final query = searchQuery!.toLowerCase();
return serviceName.contains(query);
}).toList();
// Always show content, never loading
if (services.isEmpty) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Text(
searchQuery != null && searchQuery!.isNotEmpty
? "No services found for '$searchQuery'"
: "No services available",
),
),
);
}
final int itemCount = services.length > 4 ? 4 : services.length;
final int rowsNeeded = (itemCount + 1) ~/ 2;
final double gridHeight = rowsNeeded * 250.0;
return Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
height: gridHeight,
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: itemCount,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 0.7,
),
itemBuilder: (context, index) {
final service = services[index];
return ServiceCard(service: service);
},
),
),
);
}
}
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@override
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends ConsumerState<HomeScreen>
with WidgetsBindingObserver, TickerProviderStateMixin {
final CarouselSliderController _controller = CarouselSliderController();
final ScrollController _scrollController = ScrollController();
final TextEditingController _searchController = TextEditingController();
int? selectedIndex;
late TabController _tabController;
List<String> tabTitles = [];
List<dynamic> items = [];
bool _isFirstLoad = true;
bool _tabsInitialized = false;
String _searchQuery = '';
// Filter variables
String? selectedCategory;
String? selectedSubcategory;
String? selectedType;
String? selectedSubcategoryId;
String? selectedTypeValue;
List<CategoriesModel> categories = [];
List<dynamic> subcategories = [];
String? selectedCategoryId;
int defaultTabIndex = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_isFirstLoad = false;
// Add search listener
_searchController.addListener(() {
if (mounted) {
setState(() {
_searchQuery = _searchController.text;
});
}
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Removed problematic refresh calls
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// Removed problematic refresh calls
}
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
void _initializeTabController(List<dynamic> categories) {
if (categories.isEmpty || _tabsInitialized) return;
try {
tabTitles =
["All"] +
categories.map<String>((cat) => cat.name as String).toList();
selectedCategoryId = "0";
_tabController = TabController(
length: categories.length + 1,
vsync: this,
initialIndex: defaultTabIndex,
);
_tabController.addListener(() {
if (mounted) {
setState(() {});
}
});
_tabsInitialized = true;
} catch (e) {
debugPrint('Error initializing tab controller: $e');
}
}
@override
void dispose() {
if (_tabsInitialized) {
_tabController.dispose();
}
_searchController.dispose();
WidgetsBinding.instance.removeObserver(this);
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Safe access to all providers with proper error handling
final bannerAsyncValue = ref.watch(bannerListProvider);
final categoryAsync = ref.watch(categoryListProvider);
final bookingAsyncValue = ref.watch(userbookinghistorydetailsProvider);
final expiredPlan = ref.watch(expiredPlanProvider);
final profileData = ref.watch(profilegetuserProvider);
final indexController = InheritedIndexController.of(context);
final screenSize = MediaQuery.of(context).size;
final double verticalSpacing = screenSize.height * 0.03;
// Safe data extraction with error handling
final banners = bannerAsyncValue.when(
data: (data) => data ?? [],
loading: () => [],
error: (_, __) => [],
);
final categoriesData = categoryAsync.when(
data: (data) => data ?? [],
loading: () => [],
error: (_, __) => [],
);
final bookings = bookingAsyncValue.when(
data: (data) => data ?? [],
loading: () => [],
error: (_, __) => [],
);
final plan = expiredPlan.when(
data: (data) => data,
loading: () => null,
error: (_, __) => null,
);
final profiles = profileData.when(
data: (data) => data ?? [],
loading: () => [],
error: (_, __) => [],
);
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: AppColors.secondprimary,
key: _scaffoldKey,
drawer: DrawerMenu(
userName: profiles.isNotEmpty
? (profiles.first.name ?? "User")
: "User",
userImage: profiles.isNotEmpty
? (profiles.first.profilePic1 ?? "")
: "",
),
body: SafeArea(
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Header Section
Padding(
padding: const EdgeInsets.only(left: 16, right: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
// Safe scaffold access without geometry calls
if (mounted) {
try {
final scaffoldState = _scaffoldKey.currentState;
if (scaffoldState != null &&
!scaffoldState.isDrawerOpen) {
scaffoldState.openDrawer();
}
} catch (e) {
debugPrint('Error opening drawer: $e');
}
}
},
child: Image.asset(AppAssets.menu, height: 40),
),
Padding(
padding: const EdgeInsets.only(left: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Welcome',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20,
height: 1.0,
letterSpacing: 0.2372,
color: Colors.black,
),
),
SizedBox(height: 5),
Text(
'👋 ${profiles.isNotEmpty ? (profiles.first.name ?? "User") : "User"}',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 13.48,
height: 1.5,
letterSpacing: 0.2696,
color: Colors.black,
),
),
],
),
),
const Spacer(),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.lightGrey,
shape: BoxShape.circle,
),
child: IconButton(
icon: Icon(
Icons.notifications_none,
color: Colors.black,
),
onPressed: () {},
),
),
],
),
),
SizedBox(height: 20),
// Banner Section - Always show content
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 5,
),
child: banners.isNotEmpty
? CarouselSlider(
options: CarouselOptions(
height: 180,
autoPlay: false,
enlargeCenterPage: true,
viewportFraction: 1.0,
autoPlayCurve: Curves.fastOutSlowIn,
enableInfiniteScroll: true,
),
carouselController: _controller,
items: banners.map((banner) {
return Builder(
builder: (BuildContext context) {
return Container(
height: 180,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
image: DecorationImage(
image: CachedNetworkImageProvider(
banner.documentUrl ?? '',
),
fit: BoxFit.cover,
onError: (exception, stackTrace) {
debugPrint(
'Banner image error: $exception',
);
},
),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
// Left Arrow
Transform.translate(
offset: const Offset(-15, 0),
child: Padding(
padding: const EdgeInsets.only(
left: 8.0,
),
child: GestureDetector(
onTap: () =>
_controller.previousPage(),
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black
.withOpacity(0.2),
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Center(
child: Image.asset(
AppAssets.arrowbutton,
width: 50,
height: 50,
color: AppColors.thridprimary,
),
),
),
),
),
),
// Right Arrow
Transform.translate(
offset: const Offset(15, 0),
child: Padding(
padding: const EdgeInsets.only(
right: 8.0,
),
child: GestureDetector(
onTap: () => _controller.nextPage(),
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black
.withOpacity(0.2),
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Center(
child: Transform.rotate(
angle: 3.14,
child: Image.asset(
AppAssets.arrowbutton,
width: 50,
height: 50,
color: AppColors.thridprimary,
),
),
),
),
),
),
),
],
),
);
},
);
}).toList(),
)
: Container(
height: 180,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: Colors.grey[300],
),
child: Center(child: Text('No banners available')),
),
),
SizedBox(height: 20),
// Search Section with Enhanced Search Functionality
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 5,
child: Container(
width: 323.63,
height: 60.3,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.08),
border: Border.all(
color: const Color(0xFFAEAEAE),
width: 1.01,
),
),
child: TextFormField(
controller: _searchController,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 20,
),
border: InputBorder.none,
hintText: 'Search your service',
hintStyle: TextStyle(color: Colors.grey[600]),
prefixIcon: Icon(
Icons.search_sharp,
color: Colors.grey[600],
),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: Icon(
Icons.clear,
color: Colors.grey[600],
),
onPressed: () {
_searchController.clear();
if (mounted) {
setState(() {
_searchQuery = '';
});
}
},
)
: null,
),
style: const TextStyle(
fontSize: 16,
color: Colors.black,
),
onChanged: (value) {
if (mounted) {
setState(() {
_searchQuery = value;
});
}
},
),
),
),
SizedBox(width: 5),
Expanded(
flex: 1,
child: Container(
width: 62.31,
height: 60.30,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.08),
border: Border.all(
color: const Color(0xFFAEAEAE),
width: 1.01,
),
),
child: Center(
child: IconButton(
onPressed: () {
// Add small delay to avoid scaffold geometry issues
Future.delayed(Duration(milliseconds: 50), () {
if (mounted) {
_showCommanFilterBottomSheet(context);
}
});
},
icon: Image.asset(
AppAssets.filtericon,
height: 25,
width: 25,
color: const Color(0xff797777),
),
),
),
),
),
],
),
),
SizedBox(height: 20),
// Free/Paid Service Buttons
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: GestureDetector(
onTap: () {
Get.toNamed(
'${RouterConts.listservice}/0', // category id = 0
arguments: {
'service':
'free', // service type = 1 (manual/paid)
'subcategoryId':
null, // or specific subcategory id if needed
},
);
},
child: Container(
width: 181,
height: 60,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Color(0xFFC7C7C7),
width: 1,
),
borderRadius: BorderRadius.circular(15),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(
AppAssets.free,
width: 46,
height: 46,
),
SizedBox(width: 10),
Text(
"Free",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 22,
height: 1.7,
letterSpacing: 0.22,
color: Color(0xFF524F4F),
),
),
],
),
),
),
),
SizedBox(width: 16),
Expanded(
child: GestureDetector(
onTap: () {
Get.toNamed(
'${RouterConts.listservice}/0', // category id = 0
arguments: {
'service': '1', // service type = 1 (manual/paid)
'subcategoryId':
null, // or specific subcategory id if needed
},
);
},
child: Container(
width: 181,
height: 60,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Color(0xFFC7C7C7),
width: 1,
),
borderRadius: BorderRadius.circular(15),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(
AppAssets.paid,
width: 46,
height: 46,
),
SizedBox(width: 10),
Text(
"Paid",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 22,
height: 1.7,
letterSpacing: 0.22,
color: Color(0xFF524F4F),
),
),
],
),
),
),
),
],
),
),
SizedBox(height: verticalSpacing),
// Categories Section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Categories',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 18,
height: 27.59 / 24,
letterSpacing: 1,
color: AppColors.thridprimary,
),
),
GestureDetector(
onTap: () {
Get.offAllNamed(RouterConts.categorypage, arguments: 2);
},
child: Text(
'View more',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 16,
height: 25.82 / 16,
letterSpacing: 1,
color: AppColors.thridprimary,
),
),
),
],
),
),
// Categories Grid - Always show content
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 20,
),
child: categoriesData.isNotEmpty
? GridView.count(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
crossAxisCount: 3,
crossAxisSpacing: 10,
mainAxisSpacing: 20,
childAspectRatio: 0.9,
children: List.generate(
categoriesData.length > 6 ? 6 : categoriesData.length,
(index) {
var item = categoriesData[index];
bool isSelected = selectedIndex == index;
Widget imageWidget;
try {
if (item.name.toLowerCase() == 'plumbing') {
imageWidget = Image.network(
item.getIconUrl(),
height: 50,
width: 50,
errorBuilder: (context, error, stackTrace) =>
Icon(Icons.error, size: 50),
);
} else if (item.name.toLowerCase().contains(
'electrical',
)) {
imageWidget = Image.network(
item.getIconUrl(),
height: 50,
width: 50,
errorBuilder: (context, error, stackTrace) =>
Icon(Icons.error, size: 50),
);
} else {
imageWidget = Image.network(
item.getImageUrl(),
height: 50,
width: 50,
errorBuilder: (context, error, stackTrace) =>
Image.network(
item.iconUrl ?? '',
height: 50,
width: 50,
errorBuilder:
(context, error, stackTrace) =>
Icon(Icons.image, size: 50),
),
);
}
} catch (e) {
imageWidget = Icon(Icons.category, size: 50);
}
return GestureDetector(
onTap: () {
if (mounted) {
setState(() {
selectedIndex = index;
});
}
Future.delayed(Duration(milliseconds: 300), () {
Get.toNamed(
RouterConts.listservice,
arguments: {
'id': item.id, // Pass ID in arguments
'subcategoryId': null,
'service': '0',
},
);
});
},
child: Container(
decoration: BoxDecoration(
color: Color(0xFFE3E3E3),
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
offset: Offset(2, 1),
blurRadius: 3.5,
),
],
),
child: Padding(
padding: EdgeInsets.all(isSelected ? 8 : 0),
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
padding: EdgeInsets.all(
isSelected ? 8 : 16,
),
decoration: BoxDecoration(
color: isSelected
? Color(0xFF52CC40)
: Color(0xffFAFAFA),
borderRadius: BorderRadius.circular(15),
),
child: AnimatedRotation(
turns: isSelected ? -10 / 360 : 0,
duration: Duration(milliseconds: 300),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
imageWidget,
Padding(
padding: const EdgeInsets.only(
top: 6,
),
child: Text(
item.name ?? 'Category',
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w900,
fontSize: 10,
color: Colors.black,
),
),
),
],
),
),
),
),
),
);
},
),
)
: SizedBox(
height: 200,
child: Center(child: Text('No categories available')),
),
),
SizedBox(height: 20),
// Expired Plan Section - Show only if plan exists
if (plan != null && plan.endDate != null && plan.planName != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Container(
decoration: BoxDecoration(
color: AppColors.lightBlue,
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Subscription plan',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 24,
height: 27.59 / 24,
letterSpacing: 1.0,
color: Color(0xFF9C34C2),
),
),
GestureDetector(
onTap: () async {
try {
Get.offAllNamed(
RouterConts.history,
arguments: {
'historyTab': 2, // Enquiry list tab
},
);
} catch (e) {
debugPrint('Navigation error: $e');
}
},
child: Text(
'View More',
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 15,
color: Color(0xFF534E4E),
),
),
),
],
),
const SizedBox(height: 10),
Row(
children: [
Image.asset(
AppAssets.subscription,
width: 60,
height: 60,
),
const SizedBox(width: 16),
Flexible(
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: 'Your ',
style: TextStyle(
fontFamily: 'Gilroy-Medium',
color: Color(0xFF585454),
fontWeight: FontWeight.w400,
fontSize: 18,
height: 1.86,
letterSpacing: 0.1957,
),
),
TextSpan(
text: plan.planName ?? 'Subscription',
style: TextStyle(
fontFamily: 'Gilroy-Medium',
color: Color(0xFFFF0000),
fontWeight: FontWeight.w700,
fontSize: 18,
height: 1.86,
letterSpacing: 0.1957,
),
),
TextSpan(
text:
' subscription plan was expired on ',
style: TextStyle(
fontFamily: 'Gilroy-Medium',
color: Color(0xFF585454),
fontWeight: FontWeight.w400,
fontSize: 18,
height: 1.86,
letterSpacing: 0.1957,
),
),
TextSpan(
text: _formatDate(plan.endDate),
style: TextStyle(
fontFamily: 'Gilroy-Medium',
color: Color(0xFFFF0000),
fontWeight: FontWeight.w700,
fontSize: 18,
letterSpacing: 0.1957,
),
),
],
),
),
),
],
),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Start Date: ${plan.createdDate?.split(' ').first ?? 'Not available'}',
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14,
height: 13 / 14,
letterSpacing: 0.14,
color: Color(0xFF4F4F4F),
),
),
Text(
'End Date: ${plan.endDate ?? 'Not available'}',
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14,
height: 13 / 14,
letterSpacing: 0.14,
color: Color(0xFF4F4F4F),
),
),
],
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
onPressed: () {
Get.offAllNamed(
RouterConts.packageList,
arguments: 1,
);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
child: const Text(
'Renewal',
style: TextStyle(color: Colors.white),
),
),
],
),
],
),
),
),
),
SizedBox(height: 20),
// Your Booking Section - Always show content
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Only show header if there are bookings
if (bookings.isNotEmpty) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Your Booking',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
GestureDetector(
onTap: () {
Get.offAllNamed(
RouterConts.history,
arguments: {
'historyTab': 0, // Enquiry list tab
},
);
},
child: Text(
'View more',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 40),
],
// Booking list - Always show without loading
ListView.builder(
itemCount: bookings.isEmpty ? 0 : min(3, bookings.length),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final booking = bookings[index];
return _buildBookingCard(context, booking, ref);
},
),
],
),
),
SizedBox(height: 20),
// Most Popular Service Section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Most popular service',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 18,
height: 27.59 / 24,
letterSpacing: 1,
color: AppColors.thridprimary,
),
),
GestureDetector(
onTap: () {
Get.toNamed(RouterConts.mostpopluarserviceviewall);
},
child: Text(
'View more',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 16,
height: 25.82 / 16,
letterSpacing: 1,
color: AppColors.thridprimary,
),
),
),
],
),
),
SizedBox(height: 30),
// Dynamic TabBar and TabBarView - Always show content with search functionality
categoriesData.isNotEmpty
? Builder(
builder: (context) {
// Initialize tab controller if not already done
if (!_tabsInitialized && categoriesData.isNotEmpty) {
// Initialize immediately without post-frame callback
_initializeTabController(categoriesData);
}
if (!_tabsInitialized) {
return Center(child: Text("Loading categories..."));
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Material(
color: Colors.transparent,
child: TabBar(
tabAlignment: TabAlignment.start,
controller: _tabController,
isScrollable: true,
indicatorColor: Colors.transparent,
labelColor: Colors.white,
unselectedLabelColor: Colors.black,
dividerColor: Colors.transparent,
onTap: (index) {
if (mounted) {
setState(() {
_tabController.animateTo(index);
if (index == 0) {
selectedCategoryId = "0";
} else {
selectedCategoryId =
categoriesData[index - 1].id
.toString();
}
});
}
},
tabs: List.generate(categoriesData.length + 1, (
index,
) {
bool isSelected =
_tabController.index == index;
return Tab(
child: Container(
height: 48,
decoration: BoxDecoration(
color: isSelected
? Color(0xFF0066FF)
: Colors.white,
border: Border.all(
color: isSelected
? Color(0xFF0066FF)
: Color(0xFFB7B7B7),
width: isSelected ? 0 : 1,
),
borderRadius: BorderRadius.circular(38),
),
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 40,
),
child: Center(
child: Text(
index == 0
? "All"
: categoriesData[index - 1]
.name ??
"Category",
style: TextStyle(
color: isSelected
? Colors.white
: Colors.black,
fontWeight: FontWeight.w600,
),
),
),
),
);
}),
),
),
const SizedBox(height: 20),
IndexedStack(
index: _tabController.index,
sizing: StackFit.loose,
children: List.generate(
categoriesData.length + 1,
(index) => index == 0
? AllServicesTabContent(
searchQuery: _searchQuery,
)
: ServiceTabContent(
category: categoriesData[index - 1],
searchQuery: _searchQuery,
),
),
),
],
);
},
)
: Center(child: Text('No categories available')),
SizedBox(height: 10),
],
),
),
),
);
}
// Helper method to format date
String _formatDate(dynamic dateString) {
try {
final date = DateTime.parse(dateString.toString());
return DateFormat('MMMM dd').format(date);
} catch (_) {
return dateString?.toString() ?? 'Not available';
}
}
// Helper method to build booking card
Widget _buildBookingCard(
BuildContext context,
dynamic booking,
WidgetRef ref,
) {
return Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.lightGrey),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top Row: ID and "View order"
Padding(
padding: const EdgeInsets.only(right: 12, left: 12, top: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'ID : ${booking.id ?? 'N/A'}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
GestureDetector(
onTap: () {
Get.toNamed(
RouterConts.detailserivce,
arguments: booking.serviceId,
);
},
child: Text(
'View order',
style: TextStyle(
color: Colors.blue.shade600,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
const Divider(),
// Image, Company, and Status
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child:
(booking.images1 != null &&
booking.images1!.isNotEmpty)
? Image.network(
booking.images1![0],
width: 100,
height: 110,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 100,
height: 110,
color: Colors.grey[300],
child: const Icon(Icons.error, size: 50),
);
},
)
: Container(
width: 100,
height: 110,
color: Colors.grey[300],
child: const Icon(
Icons.image_not_supported,
size: 50,
),
),
),
Positioned(
top: 6,
left: 6,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
const Text(
'Live',
style: TextStyle(
fontSize: 12,
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
const SizedBox(width: 12),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
booking.vendorName ?? 'Vendor',
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 16.11,
height: 14.5 / 16.11,
letterSpacing: 0.01 * 16.11,
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
booking.serviceName ?? 'Service',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 13.91,
height: 1.3,
letterSpacing: 0.01 * 13.91,
color: Color(0xFF5A5A5A),
),
),
),
const SizedBox(width: 10),
Expanded(
child: Container(
width: booking.status == 3
? 70
: booking.status == 1
? 85
: 77.84,
height: booking.status == 3
? 25
: booking.status == 1
? 25
: 27.96,
decoration: BoxDecoration(
color: booking.status == 3
? const Color(0xFFFFEEEE)
: booking.status == 1
? const Color(0xFFE6F7E6)
: const Color(0xFFDAE9FF),
borderRadius: BorderRadius.circular(6.05),
),
child: Center(
child: Text(
booking.status == 3
? 'Cancel'
: booking.status == 1
? 'Scheduled'
: 'Pending',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontSize: 10.92,
fontWeight: FontWeight.w400,
color: booking.status == 3
? const Color(0xFFFF0000)
: booking.status == 1
? const Color(0xFF2E8B57)
: const Color(0xFF0066FF),
letterSpacing: 1.0,
height: 0.98,
),
),
),
),
),
],
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(color: AppColors.lightGrey),
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 15,
backgroundImage:
booking.profilePic != null &&
booking.profilePic!.isNotEmpty
? NetworkImage(booking.profilePic.toString())
: null,
child:
booking.profilePic == null ||
booking.profilePic!.isEmpty
? const Icon(Icons.person, size: 20)
: null,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
booking.vendorName ?? 'no data',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w600,
fontSize: 11,
height: 10.65 / 11.84,
letterSpacing: 0,
color: Color(0xFF353434),
),
overflow: TextOverflow.visible,
),
),
const Icon(
Icons.star,
color: Colors.orange,
size: 15,
),
const SizedBox(width: 4),
Text(
'${booking.isRated ?? '4.5'}',
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'SF UI Display',
fontWeight: FontWeight.w800,
fontSize: 10,
height: 12.5 / 8.82,
letterSpacing: -0.31,
),
),
],
),
const SizedBox(height: 7),
Text(
booking.categoryName ?? 'Category',
style: const TextStyle(
fontFamily: 'Gilroy-Regular',
fontWeight: FontWeight.w400,
fontSize: 11.84,
height: 10.65 / 11.84,
letterSpacing: 0,
color: Color(0xFF717171),
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
// Date / Time / Hours
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Date :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.serviceDate ?? 'April 23, 2024',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Time :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.serviceTime ?? '12:00 PM',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Working hours :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.workingHours ?? '8 hours',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
],
),
),
const SizedBox(height: 20),
// Buttons Section
if (booking.status != 3)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
mainAxisAlignment: booking.status == 1
? MainAxisAlignment.end
: MainAxisAlignment.spaceBetween,
children: [
if (booking.status != 1)
Expanded(
child: SizedBox(
height: 42,
child: ElevatedButton(
onPressed: () {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
width: double.maxFinite,
decoration: BoxDecoration(
color: AppColors.secondprimary,
borderRadius: BorderRadius.circular(16),
),
child: BookingModificationDialog(
id: booking.id.toString(),
serviceId: booking.serviceId.toString(),
),
),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0066FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: const FittedBox(
child: Text(
"Modification",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w800,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0.289,
color: Colors.white,
),
),
),
),
),
),
if (booking.status != 1) const SizedBox(width: 8),
GestureDetector(
onTap: () async {
final data = CancelBookingRequest(
id: booking.id.toString(),
serviceId: booking.serviceId.toString(),
type: booking.type == 0 ? "0" : booking.type.toString(),
);
try {
await ref.read(cancelbookingProvider(data).future);
if (mounted) {
Fluttertoast.showToast(
msg: 'Booking cancelled successfully',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
);
ref.invalidate(userbookinghistorydetailsProvider);
}
} catch (e) {
if (mounted) {
Fluttertoast.showToast(
msg: 'Failed to cancel booking: $e',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'Cancel Booking',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
const SizedBox(height: 20),
],
),
);
}
// Fixed Filter Bottom Sheet Implementation
void _showCommanFilterBottomSheet(BuildContext context) {
String? localSelectedCategory = selectedCategory;
String? localSelectedSubcategory = selectedSubcategory;
String? localSelectedType = selectedType;
List<dynamic> localSubcategories = List.from(subcategories);
List<CategoriesModel> localCategories = List.from(categories);
// Direct call without post-frame callback to avoid scaffold geometry issues
if (localCategories.isEmpty) {
final categoryRepo = ref.read(categoryRepositoryProvider);
categoryRepo
.fetchCategories(ConstsApi.catgories)
.then((loadedCategories) {
if (mounted) {
localCategories = loadedCategories;
_displayBottomSheet(
context,
localCategories,
localSubcategories,
localSelectedCategory,
localSelectedSubcategory,
localSelectedType,
);
}
})
.catchError((e) {
debugPrint('Error loading categories for filter: $e');
if (mounted) {
_displayBottomSheet(
context,
[],
localSubcategories,
localSelectedCategory,
localSelectedSubcategory,
localSelectedType,
);
}
});
} else {
_displayBottomSheet(
context,
localCategories,
localSubcategories,
localSelectedCategory,
localSelectedSubcategory,
localSelectedType,
);
}
}
void _displayBottomSheet(
BuildContext context,
List<CategoriesModel> localCategories,
List<dynamic> localSubcategories,
String? localSelectedCategory,
String? localSelectedSubcategory,
String? localSelectedType,
) {
if (!mounted) return;
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return StatefulBuilder(
builder: (context, setModalState) {
Future<void> loadSubcategoriesForModal(String categoryId) async {
setModalState(() {
localSubcategories = [];
});
final subcategoryRepo = ref.read(subcategoryRepositoryProvider);
try {
final newSubcategories = await subcategoryRepo
.fetchSubcategories(ConstsApi.subcat, categoryId);
if (context.mounted) {
setModalState(() {
localSubcategories = newSubcategories;
});
}
} catch (e) {
debugPrint('Error loading subcategories: $e');
if (context.mounted) {
setModalState(() {
localSubcategories = [];
});
}
}
}
return Container(
height: MediaQuery.of(context).size.height * 0.6,
margin: const EdgeInsets.only(top: 100),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(40),
topRight: Radius.circular(40),
),
border: Border.all(color: const Color(0xFF858181), width: 1),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20.0,
vertical: 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Filter",
style: TextStyle(
fontFamily: 'Gilroy',
fontWeight: FontWeight.w900,
fontSize: 25,
height: 1.0,
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: Image.asset(
AppAssets.filtericon,
height: 25,
width: 25,
color: const Color(0xff797777),
),
),
],
),
const SizedBox(height: 20),
// Category Dropdown
Container(
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFFFDFDFD),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFD3D3D3),
width: 1,
),
),
child: DropdownButtonHideUnderline(
child: ButtonTheme(
alignedDropdown: true,
child: DropdownButton<String>(
hint: Text("Select Category"),
value: localSelectedCategory,
isExpanded: true,
icon: Icon(Icons.arrow_drop_down),
iconSize: 24,
elevation: 16,
items: localCategories.isEmpty
? [
DropdownMenuItem<String>(
value: null,
child: Text("No categories available"),
),
]
: localCategories.map((category) {
return DropdownMenuItem<String>(
value: category.id.toString(),
child: Text(
category.name ?? 'Category',
),
);
}).toList(),
onChanged: localCategories.isEmpty
? null
: (value) {
if (value != null) {
setModalState(() {
localSelectedCategory = value;
localSelectedSubcategory = null;
});
loadSubcategoriesForModal(value);
}
},
),
),
),
),
const SizedBox(height: 20),
// Subcategory Dropdown
Container(
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFFFDFDFD),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFD3D3D3),
width: 1,
),
),
child: DropdownButtonHideUnderline(
child: ButtonTheme(
alignedDropdown: true,
child: DropdownButton<String>(
hint: Text("Select Subcategory"),
value: localSelectedSubcategory,
isExpanded: true,
icon: Icon(Icons.arrow_drop_down),
iconSize: 24,
elevation: 16,
items: localSubcategories.isEmpty
? [
DropdownMenuItem<String>(
value: null,
child: localSelectedCategory == null
? Text("Select a category first")
: Text(
"No subcategories available",
),
),
]
: localSubcategories.map((subcategory) {
return DropdownMenuItem<String>(
value: subcategory.id.toString(),
child: Text(
subcategory.name ?? 'Subcategory',
),
);
}).toList(),
onChanged: localSubcategories.isEmpty
? null
: (value) {
setModalState(() {
localSelectedSubcategory = value;
});
},
),
),
),
),
const SizedBox(height: 20),
// Type Dropdown
Container(
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFFFDFDFD),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFD3D3D3),
width: 1,
),
),
child: DropdownButtonHideUnderline(
child: ButtonTheme(
alignedDropdown: true,
child: DropdownButton<String>(
hint: Text("Select Type"),
value: localSelectedType,
isExpanded: true,
icon: Icon(Icons.arrow_drop_down),
iconSize: 24,
elevation: 16,
items: ["Free", "Paid"].map((type) {
return DropdownMenuItem<String>(
value: type,
child: Text(type),
);
}).toList(),
onChanged: (value) {
setModalState(() {
localSelectedType = value;
});
},
),
),
),
),
SizedBox(height: 50),
// Save Button
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF000000),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () {
final updatedCategory =
localSelectedCategory ?? "0";
final updatedSubcategory = localSelectedSubcategory;
final updatedType = localSelectedType;
String? updatedTypeValue;
if (localSelectedType != null) {
updatedTypeValue = localSelectedType == "Paid"
? '1'
: 'free';
}
if (mounted) {
setState(() {
selectedCategory = updatedCategory;
selectedSubcategory = updatedSubcategory;
selectedType = updatedType;
selectedSubcategoryId = updatedSubcategory;
selectedTypeValue = updatedTypeValue;
if (selectedCategory == "0") {
selectedIndex = 0;
}
categories = List.from(localCategories);
subcategories = List.from(localSubcategories);
});
}
debugPrint(
"Filter applied - Category: $selectedCategory, Subcategory: $selectedSubcategoryId, Type: $selectedTypeValue",
);
Navigator.pop(context);
// Pass everything in arguments instead of query params
Get.toNamed(
RouterConts.listservice,
arguments: {
'id': selectedCategory, // Move id to arguments
'subcategoryId': selectedSubcategoryId,
'service': selectedTypeValue,
'sourceTab':
0, // or whatever tab you want to maintain
},
);
},
child: const Text(
'Save',
style: TextStyle(
color: Colors.white,
fontFamily: 'Gilroy',
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
);
},
);
},
);
}
}
class ServiceTabContent extends ConsumerWidget {
final dynamic category;
final String? searchQuery;
const ServiceTabContent({
super.key,
required this.category,
this.searchQuery,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Safe access to provider data with error handling
final popularServicesAsync = ref.watch(
mostPopularProvider(category.id.toString()),
);
// Handle different states safely
final allServices = popularServicesAsync.when(
data: (services) => services ?? [],
loading: () => <MostPopularModel>[],
error: (error, stack) {
debugPrint(
'Error loading services for category ${category.id}: $error',
);
return <MostPopularModel>[];
},
);
// Filter services based on search query
final services = searchQuery == null || searchQuery!.isEmpty
? allServices
: allServices.where((service) {
final serviceName = (service.serviceName ?? '').toLowerCase();
final query = searchQuery!.toLowerCase();
return serviceName.contains(query);
}).toList();
// Always show content, never loading
if (services.isEmpty) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Text(
searchQuery != null && searchQuery!.isNotEmpty
? "No services found for '$searchQuery' in this category"
: "No services available for this category",
),
),
);
}
final int itemCount = services.length > 4 ? 4 : services.length;
final int rowsNeeded = (itemCount + 1) ~/ 2;
final double gridHeight = rowsNeeded * 250.0;
return Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
height: gridHeight,
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: itemCount,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 0.7,
),
itemBuilder: (context, index) {
final service = services[index];
return ServiceCard(service: service);
},
),
),
);
}
}
// Service Card Widget - Enhanced with better error handling
class ServiceCard extends StatelessWidget {
final MostPopularModel service;
const ServiceCard({super.key, required this.service});
@override
Widget build(BuildContext context) {
return Container(
width: 189,
decoration: BoxDecoration(
color: const Color(0xFFFCFAFA),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFE3E3E3), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
// Service image
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(19)),
child: SizedBox(
height: 120,
width: double.infinity,
child:
(service.images1 != null &&
service.images1!.isNotEmpty &&
_isValidCompleteUrl(service.images1![0]))
? Image.network(
service.images1!.first,
width: double.infinity,
height: 120,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
AppAssets.cleaning,
width: double.infinity,
height: 120,
fit: BoxFit.cover,
);
},
)
: Image.asset(
AppAssets.cleaning,
width: double.infinity,
height: 120,
fit: BoxFit.cover,
),
),
),
// Price tag
Align(
alignment: Alignment.centerRight,
child: Container(
height: 30,
width: 80,
transform: Matrix4.translationValues(-10, -15, 0),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white, width: 1.5),
),
child: Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
'Rs.${service.amount ?? "N/A"}',
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w400,
fontSize: 16.86,
height: 1.0,
letterSpacing: 0.02,
color: Color(0xFFFCFAFA),
),
),
),
),
),
),
),
// Service details
Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 8),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 2,
child: Text(
service.serviceName ?? 'Service',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w700,
fontSize: 15.02,
height: 1.0,
letterSpacing: 0.0,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.star,
color: Colors.orange,
size: 18,
),
const SizedBox(width: 2),
Flexible(
child: Text(
service.averageReview?.toString() ?? '0.0',
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'SF UI Display',
fontWeight: FontWeight.w600,
fontSize: 14,
height: 1.0,
letterSpacing: -0.5,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 2,
child: Container(
width: 110,
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
border: Border.all(color: AppColors.lightGrey),
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
service.profilePic1 != null &&
service.profilePic1!.isNotEmpty
? CircleAvatar(
radius: 15,
backgroundImage: NetworkImage(
service.profilePic1!,
),
onBackgroundImageError:
(exception, stackTrace) {
debugPrint(
'Profile image error: $exception',
);
},
)
: const CircleAvatar(
radius: 15,
backgroundImage: AssetImage(
AppAssets.login,
),
),
const SizedBox(width: 4),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
service.vendorName ?? 'Vendor',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 11.75,
height: 1.0,
letterSpacing: 0.0,
color: Color(0xFF353434),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 5),
Text(
service.serviceName ?? 'Service',
style: const TextStyle(
fontFamily: 'Gilroy-Regular',
fontWeight: FontWeight.w400,
fontSize: 11.75,
height: 1.0,
letterSpacing: 0.0,
color: Color(0xFF717171),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
),
const SizedBox(width: 4),
GestureDetector(
onTap: () {
Get.toNamed(
RouterConts.detailserivce,
arguments: service.id,
);
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.arrow_forward_ios_rounded,
size: 16,
color: Colors.white,
),
),
),
],
),
],
),
),
],
),
);
}
bool _isValidCompleteUrl(String url) {
try {
final uri = Uri.parse(url);
return uri.hasScheme &&
(uri.scheme == 'http' || uri.scheme == 'https') &&
uri.host.isNotEmpty;
} catch (e) {
return false;
}
}
}