2481 lines
103 KiB
Dart
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;
|
|
}
|
|
}
|
|
}
|