INFRA: Set Up Project.

This commit is contained in:
2025-11-28 11:10:49 +05:00
commit c798279f7d
609 changed files with 77436 additions and 0 deletions

View File

@@ -0,0 +1,312 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:customer/constant/constant.dart';
import 'package:customer/controllers/favourite_ondemmand_controller.dart';
import 'package:customer/controllers/theme_controller.dart';
import 'package:customer/models/category_model.dart';
import 'package:customer/models/provider_serivce_model.dart';
import 'package:customer/screen_ui/auth_screens/login_screen.dart';
import 'package:customer/screen_ui/on_demand_service/on_demand_details_screen.dart';
import 'package:customer/service/fire_store_utils.dart';
import 'package:customer/themes/app_them_data.dart';
import 'package:customer/themes/round_button_fill.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class FavouriteOndemandScreen extends StatelessWidget {
const FavouriteOndemandScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDark.value;
return GetX(
init: FavouriteOndemmandController(),
builder: (controller) {
return Scaffold(
backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface,
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: AppThemeData.onDemand300,
title: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
children: [
const SizedBox(width: 10),
Text("Favourite Services".tr, style: TextStyle(fontFamily: AppThemeData.semiBold, color: isDark ? AppThemeData.grey900 : AppThemeData.grey900, fontSize: 20)),
],
),
),
),
body:
controller.isLoading.value
? Constant.loader()
: Constant.userModel == null
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset("assets/images/login.gif", height: 120),
const SizedBox(height: 12),
Text("Please Log In to Continue".tr, style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 22, fontFamily: AppThemeData.semiBold)),
const SizedBox(height: 5),
Text(
"Youre not logged in. Please sign in to access your account and explore all features.".tr,
textAlign: TextAlign.center,
style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.bold),
),
const SizedBox(height: 20),
RoundedButtonFill(
title: "Log in".tr,
width: 55,
height: 5.5,
color: AppThemeData.primary300,
textColor: AppThemeData.grey50,
onPress: () async {
Get.offAll(const LoginScreen());
},
),
],
),
)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child:
controller.lstFav.isEmpty
? Constant.showEmptyView(message: "Favourite Service not found.".tr)
: ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
scrollDirection: Axis.vertical,
physics: const BouncingScrollPhysics(),
itemCount: controller.lstFav.length,
itemBuilder: (context, index) {
return FutureBuilder<List<ProviderServiceModel>>(
future: FireStoreUtils.getCurrentProviderService(controller.lstFav[index]),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || snapshot.data == null || snapshot.data!.isEmpty) {
return const SizedBox(); // or a placeholder widget
}
final provider = snapshot.data!.first; // safer way than [0]
return GestureDetector(
onTap: () {
Get.to(() => OnDemandDetailsScreen(), arguments: {'providerModel': provider});
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Container(
height: MediaQuery.of(context).size.height * 0.16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isDark ? AppThemeData.grey500 : Colors.grey.shade100, width: 1),
color: isDark ? AppThemeData.grey900 : Colors.white,
),
child: Row(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(10), topLeft: Radius.circular(10)),
child: CachedNetworkImage(
imageUrl: provider.photos.isNotEmpty ? provider.photos.first : Constant.placeHolderImage,
height: MediaQuery.of(context).size.height * 0.16,
width: 110,
fit: BoxFit.cover,
placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))),
errorWidget: (context, url, error) => Image.network(Constant.placeHolderImage, fit: BoxFit.cover),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
provider.title ?? "",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black),
),
),
Obx(
() => GestureDetector(
onTap: () => controller.toggleFavourite(provider),
child: Icon(
controller.lstFav.where((element) => element.service_id == provider.id).isNotEmpty ? Icons.favorite : Icons.favorite_border,
size: 24,
color:
controller.lstFav.where((element) => element.service_id == provider.id).isNotEmpty
? AppThemeData.primary300
: (isDark ? Colors.white38 : Colors.black38),
),
),
),
],
),
FutureBuilder<CategoryModel?>(
future: controller.getCategory(provider.categoryId ?? ""),
builder: (ctx, snap) {
if (!snap.hasData) return const SizedBox();
return Text(snap.data?.title ?? "", style: TextStyle(fontSize: 14, color: isDark ? Colors.white : Colors.black));
},
),
_buildPrice(provider, isDark: isDark),
_buildRating(provider),
],
),
),
),
],
),
),
),
);
},
);
FutureBuilder<List<ProviderServiceModel>>(
future: FireStoreUtils.getCurrentProviderService(controller.lstFav[index]),
builder: (context, snapshot) {
return snapshot.data != null
? GestureDetector(
onTap: () {
Get.to(() => OnDemandDetailsScreen(), arguments: {'providerModel': snapshot.data![0]});
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Container(
height: MediaQuery.of(context).size.height * 0.16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isDark ? AppThemeData.grey500 : Colors.grey.shade100, width: 1),
color: isDark ? AppThemeData.grey900 : Colors.white,
),
child: Row(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(10), topLeft: Radius.circular(10)),
child: CachedNetworkImage(
imageUrl: snapshot.data![0].photos.isNotEmpty ? snapshot.data![0].photos[0] : Constant.placeHolderImage,
height: MediaQuery.of(context).size.height * 0.16,
width: 110,
fit: BoxFit.cover,
placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))),
errorWidget: (context, url, error) => Image.network(Constant.placeHolderImage, fit: BoxFit.cover),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
snapshot.data![0].title ?? "",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black),
),
),
Obx(
() => GestureDetector(
onTap: () => controller.toggleFavourite(snapshot.data![0]),
child: Icon(
controller.lstFav.where((element) => element.service_id == snapshot.data![0].id).isNotEmpty
? Icons.favorite
: Icons.favorite_border,
size: 24,
color:
controller.lstFav.where((element) => element.service_id == snapshot.data![0].id).isNotEmpty
? AppThemeData.primary300
: (isDark ? Colors.white38 : Colors.black38),
),
),
),
],
),
FutureBuilder<CategoryModel?>(
future: controller.getCategory(snapshot.data![0].categoryId ?? ""),
builder: (ctx, snap) {
if (!snap.hasData) return const SizedBox();
return Text(snap.data?.title ?? "", style: TextStyle(fontSize: 14, color: isDark ? Colors.white : Colors.black));
},
),
_buildPrice(snapshot.data![0], isDark: isDark),
_buildRating(snapshot.data![0]),
],
),
),
),
],
),
),
),
)
: Container();
},
);
},
),
),
);
},
);
}
Widget _buildPrice(ProviderServiceModel provider, {bool isDark = false}) {
if (provider.disPrice == "" || provider.disPrice == "0") {
return Text(
provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.price) : '${Constant.amountShow(amount: provider.price ?? "0")}/${'hr'.tr}',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: isDark ? Colors.white : AppThemeData.primary300),
);
} else {
return Row(
children: [
Text(
provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.disPrice ?? '0') : '${Constant.amountShow(amount: provider.disPrice)}/${'hr'.tr}',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: isDark ? Colors.white : AppThemeData.primary300),
),
const SizedBox(width: 5),
Text(
provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.price) : '${Constant.amountShow(amount: provider.price ?? "0")}/${'hr'.tr}',
style: const TextStyle(fontSize: 12, color: Colors.grey, decoration: TextDecoration.lineThrough),
),
],
);
}
}
Widget _buildRating(ProviderServiceModel provider) {
double rating = 0;
if (provider.reviewsCount != null && provider.reviewsCount != 0) {
rating = (provider.reviewsSum ?? 0) / (provider.reviewsCount ?? 1);
}
return Container(
decoration: BoxDecoration(color: AppThemeData.warning400, borderRadius: BorderRadius.circular(16)),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star, size: 16, color: Colors.white),
const SizedBox(width: 3),
Text(rating.toStringAsFixed(1), style: const TextStyle(letterSpacing: 0.5, fontSize: 12, color: Colors.white)),
],
),
);
}
}

View File

@@ -0,0 +1,203 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../../constant/constant.dart';
import '../../controllers/my_booking_on_demand_controller.dart';
import '../../controllers/theme_controller.dart';
import '../../models/onprovider_order_model.dart';
import '../../models/worker_model.dart';
import '../../themes/app_them_data.dart';
import 'on_demand_order_details_screen.dart';
class MyBookingOnDemandScreen extends StatelessWidget {
const MyBookingOnDemandScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDark.value;
return GetX<MyBookingOnDemandController>(
init: MyBookingOnDemandController(),
builder: (controller) {
return DefaultTabController(
length: controller.tabTitles.length,
initialIndex: controller.tabTitles.indexOf(controller.selectedTab.value),
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: AppThemeData.primary300,
centerTitle: false,
title: Padding(padding: const EdgeInsets.only(bottom: 10), child: Text("Booking History".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900))),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: TabBar(
onTap: (index) {
controller.selectTab(controller.tabTitles[index]);
},
indicatorColor: AppThemeData.grey900,
labelColor: AppThemeData.grey900,
unselectedLabelColor: AppThemeData.grey900,
labelStyle: AppThemeData.boldTextStyle(fontSize: 16),
unselectedLabelStyle: AppThemeData.mediumTextStyle(fontSize: 16),
tabs: controller.tabTitles.map((title) => Tab(child: Center(child: Text(title)))).toList(),
),
),
),
body:
controller.isLoading.value
? Constant.loader()
: TabBarView(
children:
controller.tabTitles.map((title) {
final orders = controller.getOrdersForTab(title);
if (orders.isEmpty) {
return Center(child: Text("No ride found".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)));
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: orders.length,
itemBuilder: (context, index) {
OnProviderOrderModel onProviderOrder = orders[index];
WorkerModel? worker = controller.getWorker(onProviderOrder.workerId);
return InkWell(
onTap: () {
Get.to(() => OnDemandOrderDetailsScreen(), arguments: onProviderOrder);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
margin: const EdgeInsets.only(bottom: 15),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isDark ? AppThemeData.grey500 : Colors.grey.shade100, width: 1),
color: isDark ? AppThemeData.grey500 : Colors.white,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: CachedNetworkImage(
imageUrl: onProviderOrder.provider.photos.first,
height: 80,
width: 80,
imageBuilder:
(context, imageProvider) =>
Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), image: DecorationImage(image: imageProvider, fit: BoxFit.cover))),
placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))),
errorWidget:
(context, url, error) => ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(Constant.placeHolderImage, fit: BoxFit.cover, cacheHeight: 80, cacheWidth: 80),
),
fit: BoxFit.cover,
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
decoration: BoxDecoration(color: AppThemeData.info50, border: Border.all(color: AppThemeData.info300), borderRadius: BorderRadius.circular(12)),
child: Text(onProviderOrder.status, style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.info500)),
),
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(
onProviderOrder.provider.title.toString(),
style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
),
),
Padding(padding: const EdgeInsets.only(top: 6), child: buildPriceText(onProviderOrder)),
const SizedBox(height: 6),
if (onProviderOrder.status != Constant.orderCompleted &&
onProviderOrder.status != Constant.orderCancelled &&
onProviderOrder.otp != null &&
onProviderOrder.otp!.isNotEmpty)
Text(
"${'OTP :'.tr} ${onProviderOrder.otp}",
style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
),
],
),
),
),
],
),
/// Bottom Details (Date, Provider, Worker)
buildBottomDetails(context, onProviderOrder, isDark, worker),
],
),
),
);
},
);
}).toList(),
),
),
);
},
);
}
Widget buildPriceText(OnProviderOrderModel order) {
final hasDiscount = order.provider.disPrice != "" && order.provider.disPrice != "0";
final price = hasDiscount ? order.provider.disPrice.toString() : order.provider.price.toString();
return Text(
order.provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: price) : "${Constant.amountShow(amount: price)}/${'hr'.tr}",
style: AppThemeData.mediumTextStyle(fontSize: 16, color: AppThemeData.primary300),
);
}
Widget buildBottomDetails(BuildContext context, OnProviderOrderModel order, bool isDark, WorkerModel? worker) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isDark ? AppThemeData.grey400 : AppThemeData.grey100, width: 1),
color: isDark ? AppThemeData.greyDark100 : AppThemeData.grey100,
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
children: [
detailRow("Date & Time", DateFormat('dd-MMM-yyyy hh:mm a').format(order.scheduleDateTime!.toDate()), isDark),
const Divider(thickness: 1),
detailRow("Provider", order.provider.authorName.toString(), isDark),
if (order.provider.priceUnit == "Hourly") ...[
if (order.startTime != null) ...[const Divider(thickness: 1), detailRow("Start Time", DateFormat('dd-MMM-yyyy hh:mm a').format(order.startTime!.toDate()), isDark)],
if (order.endTime != null) ...[const Divider(thickness: 1), detailRow("End Time", DateFormat('dd-MMM-yyyy hh:mm a').format(order.endTime!.toDate()), isDark)],
],
if (worker != null) ...[const Divider(thickness: 1), detailRow("Worker", worker.fullName().toString(), isDark)],
],
),
),
);
}
Widget detailRow(String label, String value, bool isDark) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label.tr, style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
Text(value.tr, style: AppThemeData.regularTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
],
),
);
}
}

View File

@@ -0,0 +1,557 @@
import 'package:bottom_picker/bottom_picker.dart';
import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import '../../constant/constant.dart';
import '../../controllers/theme_controller.dart';
import '../../controllers/on_demand_booking_controller.dart';
import '../../models/tax_model.dart';
import '../../models/user_model.dart';
import '../../themes/app_them_data.dart';
import '../../themes/round_button_fill.dart';
import '../../themes/show_toast_dialog.dart';
import '../../themes/text_field_widget.dart';
import '../../widget/osm_map/map_picker_page.dart';
import '../../widget/place_picker/location_picker_screen.dart';
import '../../widget/place_picker/selected_location_model.dart';
import '../location_enable_screens/address_list_screen.dart';
class OnDemandBookingScreen extends StatelessWidget {
const OnDemandBookingScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDark.value;
return GetX(
init: OnDemandBookingController(),
builder: (controller) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: AppThemeData.primary300,
title: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50),
child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))),
),
),
const SizedBox(width: 10),
Text("Book Service".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)),
],
),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Services Section
Text("Services".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
const SizedBox(height: 10),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100),
color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50,
),
padding: const EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(controller.provider.value?.title ?? '', style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
const SizedBox(height: 5),
Text(controller.categoryTitle.value, style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
if (controller.provider.value?.priceUnit == "Fixed") ...[
const SizedBox(height: 20),
Row(
children: [
GestureDetector(onTap: controller.decrementQuantity, child: Icon(Icons.remove_circle_outline, color: AppThemeData.primary300, size: 30)),
const SizedBox(width: 10),
Text('${controller.quantity.value}', style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
const SizedBox(width: 10),
GestureDetector(onTap: controller.incrementQuantity, child: Icon(Icons.add_circle_outline, color: AppThemeData.primary300, size: 30)),
],
),
],
],
),
),
SizedBox(width: 10),
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.grey.shade300,
image: controller.provider.value!.photos.isNotEmpty ? DecorationImage(image: NetworkImage(controller.provider.value?.photos.first), fit: BoxFit.cover) : null,
),
),
],
),
),
const SizedBox(height: 15),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100),
color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Address".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
SizedBox(height: 5),
InkWell(
onTap: () async {
if (Constant.userModel != null) {
Get.to(AddressListScreen())!.then((value) {
if (value != null) {
ShippingAddress shippingAddress = value;
if (Constant.checkZoneCheck(shippingAddress.location!.latitude ?? 0.0, shippingAddress.location!.longitude ?? 0.0)) {
controller.selectedAddress.value = shippingAddress;
controller.calculatePrice();
} else {
ShowToastDialog.showToast("Service not available in this area".tr);
}
}
});
} else {
Constant.checkPermission(
onTap: () async {
ShowToastDialog.showLoader("Please wait...".tr);
ShippingAddress shippingAddress = ShippingAddress();
try {
await Geolocator.requestPermission();
await Geolocator.getCurrentPosition();
ShowToastDialog.closeLoader();
if (Constant.selectedMapType == 'osm') {
final result = await Get.to(() => MapPickerPage());
if (result != null) {
final firstPlace = result;
final lat = firstPlace.coordinates.latitude;
final lng = firstPlace.coordinates.longitude;
final address = firstPlace.address;
shippingAddress.addressAs = "Home";
shippingAddress.locality = address.toString();
shippingAddress.location = UserLocation(latitude: lat, longitude: lng);
controller.selectedAddress.value = shippingAddress;
Get.back();
}
} else {
Get.to(LocationPickerScreen())!.then((value) async {
if (value != null) {
SelectedLocationModel selectedLocationModel = value;
shippingAddress.addressAs = "Home";
shippingAddress.location = UserLocation(latitude: selectedLocationModel.latLng!.latitude, longitude: selectedLocationModel.latLng!.longitude);
shippingAddress.locality = "Picked from Map";
controller.selectedAddress.value = shippingAddress;
}
});
}
} catch (e) {
await placemarkFromCoordinates(19.228825, 72.854118).then((valuePlaceMaker) {
Placemark placeMark = valuePlaceMaker[0];
shippingAddress.location = UserLocation(latitude: 19.228825, longitude: 72.854118);
String currentLocation =
"${placeMark.name}, ${placeMark.subLocality}, ${placeMark.locality}, ${placeMark.administrativeArea}, ${placeMark.postalCode}, ${placeMark.country}";
shippingAddress.locality = currentLocation;
});
controller.selectedAddress.value = shippingAddress;
ShowToastDialog.closeLoader();
}
},
context: context,
);
}
},
child: Text(
controller.selectedAddress.value.getFullAddress(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppThemeData.mediumTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
),
),
],
),
),
],
),
),
const SizedBox(height: 15),
TextFieldWidget(title: "Description".tr, hintText: "Enter Description".tr, controller: controller.descriptionController.value, maxLine: 5),
const SizedBox(height: 10),
GestureDetector(
onTap: () {
BottomPicker.dateTime(
onSubmit: (date) {
controller.setDateTime(date);
},
minDateTime: DateTime.now(),
buttonAlignment: MainAxisAlignment.center,
displaySubmitButton: true,
buttonSingleColor: AppThemeData.primary300,
buttonPadding: 10,
buttonWidth: 70,
pickerTitle: Text("", style: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
backgroundColor: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50,
pickerTextStyle: AppThemeData.mediumTextStyle(fontSize: 14, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
closeIconColor: isDark ? Colors.white : Colors.black,
).show(context);
},
child: TextFieldWidget(title: "Booking Date & Slot".tr, hintText: "Choose Date and Time".tr, controller: controller.dateTimeController.value, enable: false),
),
const SizedBox(height: 15),
controller.provider.value?.priceUnit == "Fixed"
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
controller.couponList.isNotEmpty
? SizedBox(
height: 85,
child: ListView.builder(
itemCount: controller.couponList.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
final coupon = controller.couponList[index];
return GestureDetector(onTap: () => controller.applyCoupon(coupon), child: buildOfferItem(controller, index, isDark));
},
),
)
: Container(),
buildPromoCode(controller, isDark),
Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Text("Price Detail".tr, style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
),
priceTotalRow(controller, isDark),
],
)
: SizedBox(),
],
),
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(20.0),
child: RoundedButtonFill(title: "Confirm".tr, color: AppThemeData.primary300, textColor: AppThemeData.grey50, onPress: () => controller.confirmBooking(context)),
),
);
},
);
}
Widget buildOfferItem(OnDemandBookingController controller, int index, bool isDark) {
return Obx(() {
final coupon = controller.couponList[index];
return Container(
margin: const EdgeInsets.fromLTRB(7, 10, 7, 10),
height: 85,
child: DottedBorder(
options: RoundedRectDottedBorderOptions(strokeWidth: 1, radius: const Radius.circular(10), color: AppThemeData.primary300),
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 5, 12, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Image(image: AssetImage('assets/images/offer_icon.png'), height: 25, width: 25),
const SizedBox(width: 10),
Container(
margin: const EdgeInsets.only(top: 3),
child: Text(
coupon.discountType == "Fix Price" ? "${Constant.amountShow(amount: coupon.discount.toString())} ${'OFF'.tr}" : "${coupon.discount} ${'% Off'.tr}",
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 0.7, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
),
),
],
),
const SizedBox(height: 5),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(coupon.code ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.normal, letterSpacing: 0.5, color: Colors.orange)),
Container(margin: const EdgeInsets.only(left: 15, right: 15, top: 3), width: 1, color: AppThemeData.grey50),
Text(
"valid till ".tr + controller.getDate(coupon.expiresAt!.toDate().toString()),
style: TextStyle(letterSpacing: 0.5, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
),
],
),
],
),
),
),
);
});
}
Widget buildPromoCode(OnDemandBookingController controller, bool isDark) {
return GestureDetector(
child: Container(
margin: const EdgeInsets.only(top: 10, bottom: 13),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100),
color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50,
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 15.0, horizontal: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Image.asset("assets/images/reedem.png", height: 50, width: 50),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Promo Code".tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), overflow: TextOverflow.ellipsis),
const SizedBox(height: 5),
Text(
"Apply promo code".tr,
style: AppThemeData.mediumTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
FloatingActionButton(
onPressed: () {
Get.bottomSheet(promoCodeSheet(controller, isDark), isScrollControlled: true, isDismissible: true, backgroundColor: Colors.transparent, enableDrag: true);
},
mini: true,
backgroundColor: Colors.blueGrey.shade50,
elevation: 0,
child: const Icon(Icons.add, color: Colors.black54),
),
],
),
),
),
);
}
Widget promoCodeSheet(OnDemandBookingController controller, bool isDark) {
return Container(
padding: EdgeInsets.only(bottom: Get.height / 4.3, left: 25, right: 25),
height: Get.height * 0.88,
decoration: BoxDecoration(color: Colors.transparent, border: Border.all(style: BorderStyle.none)),
child: Column(
children: [
InkWell(
onTap: () => Get.back(),
child: Container(
height: 45,
decoration: BoxDecoration(
border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100, width: 0.3),
color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50,
shape: BoxShape.circle,
),
child: Center(
child: Icon(
Icons.close,
color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, // ✅ visible color
size: 28,
),
),
),
),
const SizedBox(height: 25),
Expanded(
child: Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50),
alignment: Alignment.center,
child: SingleChildScrollView(
child: Column(
children: [
Container(padding: const EdgeInsets.only(top: 30), child: const Image(image: AssetImage('assets/images/redeem_coupon.png'), width: 100)),
Container(
padding: const EdgeInsets.only(top: 20),
child: Text('Redeem Your Coupons'.tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900, fontSize: 16)),
),
Center(
child: Container(
padding: const EdgeInsets.only(top: 10, left: 22, right: 22),
child: Text("Voucher or Coupon code".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
),
),
Container(
padding: const EdgeInsets.only(left: 20, right: 20, top: 20),
child: DottedBorder(
options: RoundedRectDottedBorderOptions(strokeWidth: 1, radius: const Radius.circular(12), color: AppThemeData.primary300),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Container(
padding: const EdgeInsets.all(20),
color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50,
alignment: Alignment.center,
child: TextFormField(
textAlign: TextAlign.center,
style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
controller: controller.couponTextController.value,
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Write Coupon Code".tr,
hintStyle: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey400),
),
),
),
),
),
),
Padding(
padding: const EdgeInsets.only(top: 30, bottom: 30, left: 15, right: 15),
child: RoundedButtonFill(
title: "REDEEM NOW".tr,
color: AppThemeData.primary300,
textColor: AppThemeData.grey50,
onPress: () {
final inputCode = controller.couponTextController.value.text.trim().toLowerCase();
final matchingCoupon = controller.couponList.firstWhereOrNull((c) => c.code?.toLowerCase() == inputCode);
if (matchingCoupon != null) {
controller.applyCoupon(matchingCoupon);
Get.back();
} else {
ShowToastDialog.showToast("Applied coupon not valid.".tr);
}
},
),
),
],
),
),
),
),
],
),
);
}
Widget priceTotalRow(OnDemandBookingController controller, bool isDark) {
return Obx(() {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isDark ? AppThemeData.greyDark400 : AppThemeData.grey100),
color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50,
),
child: Column(
children: [
const SizedBox(height: 5),
rowText("Price".tr, Constant.amountShow(amount: controller.price.value.toString()), isDark),
controller.discountAmount.value != 0 ? const Divider() : const SizedBox(),
controller.discountAmount.value != 0
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${"Discount".tr} ${controller.discountType.value == 'Percentage' || controller.discountType.value == 'Percent' ? "(${controller.discountLabel.value}%)" : "(${Constant.amountShow(amount: controller.discountLabel.value)})"}",
style: TextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
),
Text(controller.offerCode.value, style: TextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
],
),
),
Text("(-${Constant.amountShow(amount: controller.discountAmount.value.toString())})", style: const TextStyle(color: Colors.red)),
],
),
)
: const SizedBox(),
const Divider(),
rowText("SubTotal".tr, Constant.amountShow(amount: controller.subTotal.value.toString()), isDark),
const Divider(),
ListView.builder(
itemCount: Constant.taxList.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
TaxModel taxModel = Constant.taxList[index];
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
"${taxModel.title} (${taxModel.type == "fix" ? Constant.amountShow(amount: taxModel.tax) : "${taxModel.tax}%"})",
style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
),
),
Text(
Constant.amountShow(amount: Constant.getTaxValue(amount: controller.subTotal.value.toString(), taxModel: taxModel).toString()),
style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
),
],
),
),
const Divider(),
],
);
},
),
rowText("Total Amount".tr, Constant.amountShow(amount: controller.totalAmount.value.toString()), isDark),
const SizedBox(height: 5),
],
),
);
});
}
Widget rowText(String title, String value, bool isDark) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title.tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
Text(value.tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
],
),
);
}
}

View File

@@ -0,0 +1,101 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:customer/screen_ui/on_demand_service/view_category_service_screen.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constant/constant.dart';
import '../../controllers/on_demand_category_controller.dart';
import '../../controllers/theme_controller.dart';
import '../../models/category_model.dart';
import '../../themes/app_them_data.dart';
class OnDemandCategoryScreen extends StatelessWidget {
const OnDemandCategoryScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDark.value;
return GetX(
init: OnDemandCategoryController(),
builder: (controller) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: AppThemeData.primary300,
title: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50),
child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Explore services".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)),
Text(
"Explore services tailored for you—quick, easy, and personalized.".tr,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppThemeData.boldTextStyle(fontSize: 14, color: AppThemeData.grey900),
),
],
),
),
],
),
),
),
body:
controller.isLoading.value
? Constant.loader()
: Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
child: SingleChildScrollView(
child: Column(
children: [
controller.categories.isEmpty
? Center(child: Text("No Categories".tr))
: GridView.builder(
padding: const EdgeInsets.all(5),
itemCount: controller.categories.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
itemBuilder: (context, index) {
return categoriesCell(context, controller.categories[index], index, isDark);
},
),
],
),
),
),
);
},
);
}
Widget categoriesCell(BuildContext context, CategoryModel category, int index, bool isDark) {
return GestureDetector(
onTap: () {
Get.to(() => ViewCategoryServiceListScreen(), arguments: {'categoryId': category.id, 'categoryTitle': category.title});
},
child: Column(
children: [
ClipRRect(borderRadius: BorderRadius.circular(12), child: CachedNetworkImage(imageUrl: category.image ?? "", height: 60, width: 60, fit: BoxFit.cover)),
const SizedBox(height: 5),
Text(category.title ?? "", style: AppThemeData.semiBoldTextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900), textAlign: TextAlign.center),
],
),
);
}
}

View File

@@ -0,0 +1,85 @@
import 'package:customer/constant/constant.dart';
import 'package:customer/controllers/cab_dashboard_controller.dart';
import 'package:customer/controllers/theme_controller.dart';
import 'package:customer/themes/app_them_data.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import '../../controllers/on_demand_dashboard_controller.dart';
class OnDemandDashboardScreen extends StatelessWidget {
const OnDemandDashboardScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
return Obx(() {
final isDark = themeController.isDark.value;
return GetX(
init: OnDemandDashboardController(),
builder: (controller) {
return Scaffold(
body: controller.pageList[controller.selectedIndex.value],
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
showUnselectedLabels: true,
showSelectedLabels: true,
selectedFontSize: 12,
selectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold),
unselectedLabelStyle: const TextStyle(fontFamily: AppThemeData.bold),
currentIndex: controller.selectedIndex.value,
backgroundColor: isDark ? AppThemeData.grey900 : AppThemeData.grey50,
selectedItemColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300,
unselectedItemColor: isDark ? AppThemeData.grey300 : AppThemeData.grey600,
onTap: (int index) {
if (index == 0) {
Get.put(CabDashboardController());
}
controller.selectedIndex.value = index;
},
items:
Constant.walletSetting == false
? [
navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home_cab.svg", label: 'Home'.tr, controller: controller),
navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_fav.svg", label: 'Favourites'.tr, controller: controller),
navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_booking_cab.svg", label: 'My Bookings'.tr, controller: controller),
navigationBarItem(isDark, index: 3, assetIcon: "assets/icons/ic_profile.svg", label: 'Profile'.tr, controller: controller),
]
: [
navigationBarItem(isDark, index: 0, assetIcon: "assets/icons/ic_home_cab.svg", label: 'Home'.tr, controller: controller),
navigationBarItem(isDark, index: 1, assetIcon: "assets/icons/ic_fav.svg", label: 'Favourites'.tr, controller: controller),
navigationBarItem(isDark, index: 2, assetIcon: "assets/icons/ic_booking_cab.svg", label: 'My Bookings'.tr, controller: controller),
navigationBarItem(isDark, index: 3, assetIcon: "assets/icons/ic_wallet_cab.svg", label: 'Wallet'.tr, controller: controller),
navigationBarItem(isDark, index: 4, assetIcon: "assets/icons/ic_profile.svg", label: 'Profile'.tr, controller: controller),
],
),
);
},
);
});
}
BottomNavigationBarItem navigationBarItem(isDark, {required int index, required String label, required String assetIcon, required OnDemandDashboardController controller}) {
return BottomNavigationBarItem(
icon: Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: SvgPicture.asset(
assetIcon,
height: 22,
width: 22,
color:
controller.selectedIndex.value == index
? isDark
? AppThemeData.primary300
: AppThemeData.primary300
: isDark
? AppThemeData.grey300
: AppThemeData.grey600,
),
),
label: label,
);
}
}

View File

@@ -0,0 +1,499 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:customer/constant/constant.dart';
import 'package:customer/screen_ui/on_demand_service/provider_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../../controllers/theme_controller.dart';
import '../../models/provider_serivce_model.dart';
import '../../controllers/on_demand_details_controller.dart';
import '../../themes/app_them_data.dart';
import '../../themes/round_button_fill.dart';
import '../auth_screens/login_screen.dart';
import 'on_demand_booking_screen.dart';
class OnDemandDetailsScreen extends StatelessWidget {
const OnDemandDetailsScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDark.value;
return GetX<OnDemandDetailsController>(
init: OnDemandDetailsController(),
builder: (controller) {
return Scaffold(
body: buildSliverScrollView(context, controller, controller.provider, controller.userModel, isDark),
bottomNavigationBar:
controller.isOpen.value == false
? SizedBox()
: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
RoundedButtonFill(
title: "Book Now".tr,
color: AppThemeData.primary300,
textColor: AppThemeData.grey50,
onPress: () async {
if (Constant.userModel == null) {
Get.offAll(const LoginScreen());
} else {
print("providerModel ::::::::${controller.provider.title ?? 'No provider'}");
print("categoryTitle ::::::: ${controller.categoryTitle.value}");
Get.to(() => OnDemandBookingScreen(), arguments: {'providerModel': controller.provider, 'categoryTitle': controller.categoryTitle.value});
}
},
),
],
),
),
);
},
);
}
SingleChildScrollView buildSliverScrollView(BuildContext context, OnDemandDetailsController controller, ProviderServiceModel provider, user, isDark) {
final width = MediaQuery.of(context).size.width;
final height = MediaQuery.of(context).size.height;
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
CachedNetworkImage(
imageUrl: provider.photos.isNotEmpty ? provider.photos.first : "",
placeholder: (context, url) => Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))),
errorWidget: (context, url, error) => Image.network(Constant.placeHolderImage, fit: BoxFit.fitWidth),
fit: BoxFit.fitWidth,
width: width,
height: height * 0.45,
),
Positioned(top: height * 0.05, left: width * 0.03, child: _circleButton(context, icon: Icons.arrow_back, onTap: () => Get.back())),
Positioned(
top: height * 0.05,
right: width * 0.03,
child: Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(40), color: controller.isOpen.value ? Colors.green : Colors.red),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Text(controller.isOpen.value ? "Open".tr : "Close".tr, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white, fontSize: 14)),
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
child: GetBuilder<OnDemandDetailsController>(
builder: (controller) {
final provider = controller.provider;
final categoryTitle = controller.categoryTitle.value;
final subCategoryTitle = controller.subCategoryTitle.value;
// final tabString = controller.tabString.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
provider.title.toString(),
style: TextStyle(fontSize: 20, fontFamily: AppThemeData.regular, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black),
),
),
Row(
children: [
provider.disPrice == "" || provider.disPrice == "0"
? Text(
provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.price ?? '0') : '${Constant.amountShow(amount: provider.price ?? '0')}/${'hr'.tr}',
style: TextStyle(fontSize: 18, fontFamily: AppThemeData.regular, fontWeight: FontWeight.bold, color: isDark ? Colors.white : AppThemeData.primary300),
)
: Row(
children: [
Text(
provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.disPrice ?? '0') : '${Constant.amountShow(amount: provider.disPrice ?? '0')}/${'hr'.tr}',
style: TextStyle(fontSize: 18, fontFamily: AppThemeData.regular, fontWeight: FontWeight.bold, color: isDark ? Colors.white : AppThemeData.primary300),
),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.price ?? '0') : '${Constant.amountShow(amount: provider.price ?? '0')}/${'hr'.tr}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Colors.grey, decoration: TextDecoration.lineThrough),
),
),
],
),
],
),
],
),
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Text(categoryTitle, style: TextStyle(fontSize: 14, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400, color: isDark ? Colors.white : Colors.black)),
),
),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Row(
children: [
const Icon(Icons.star, size: 16, color: AppThemeData.warning400),
const SizedBox(width: 3),
Text(
provider.reviewsCount != 0 ? ((provider.reviewsSum ?? 0.0) / (provider.reviewsCount ?? 0.0)).toStringAsFixed(1) : '0',
style: const TextStyle(letterSpacing: 0.5, fontSize: 16, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500, color: AppThemeData.warning400),
),
const SizedBox(width: 10),
Text(
"(${provider.reviewsCount} ${'Reviews'.tr})",
style: TextStyle(letterSpacing: 0.5, fontSize: 16, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500, color: isDark ? Colors.white : Colors.black),
),
],
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Row(
children: [
subCategoryTitle.isNotEmpty
? Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppThemeData.primary300.withOpacity(0.20)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Text(subCategoryTitle, style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, fontFamily: AppThemeData.regular, color: AppThemeData.primary300)),
),
)
: Container(),
const SizedBox(width: 10),
Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: Colors.green.withOpacity(0.20)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: InkWell(
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
isDismissible: true,
context: context,
backgroundColor: Colors.transparent,
enableDrag: true,
builder: (context) => showTiming(context, controller, isDark),
);
},
child: Text("View Timing".tr, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.green, letterSpacing: 0.5)),
),
),
),
],
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.location_on_outlined, color: isDark ? Colors.white : Colors.black, size: 20),
const SizedBox(width: 5),
Expanded(
child: Text(
provider.address.toString(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontFamily: AppThemeData.regular, fontWeight: FontWeight.w400, color: isDark ? Colors.white : Colors.black),
),
),
],
),
const SizedBox(height: 10),
const Divider(),
_tabBar(controller),
Obx(() {
if (controller.tabString.value == "About") {
return aboutTabViewWidget(controller, controller.provider, isDark);
} else if (controller.tabString.value == "Gallery") {
return galleryTabViewWidget(controller);
} else {
return reviewTabViewWidget(controller, isDark);
}
}),
const SizedBox(height: 15),
],
);
},
),
),
],
),
);
}
Widget _circleButton(BuildContext context, {required IconData icon, required VoidCallback onTap}) {
return ClipOval(
child: Container(color: Colors.black.withOpacity(0.7), child: InkWell(onTap: onTap, child: Padding(padding: const EdgeInsets.all(8.0), child: Icon(icon, size: 30, color: Colors.white)))),
);
}
Widget _tabBar(OnDemandDetailsController controller) {
return Obx(() => Row(children: [_tabItem("About", controller), _tabItem("Gallery", controller), _tabItem("Review", controller)]));
}
Widget _tabItem(String title, OnDemandDetailsController controller) {
return GestureDetector(
onTap: () => controller.changeTab(title),
child: Container(
margin: const EdgeInsets.only(right: 10),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(color: controller.tabString.value == title ? AppThemeData.primary300 : Colors.grey.shade200, borderRadius: BorderRadius.circular(10)),
child: Text(title.tr, style: TextStyle(fontWeight: FontWeight.bold, color: controller.tabString.value == title ? Colors.white : Colors.black)),
),
);
}
Widget aboutTabViewWidget(OnDemandDetailsController controller, ProviderServiceModel providerModel, bool isDark) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text((providerModel.description ?? '').tr, style: TextStyle(color: isDark ? Colors.white : Colors.black, fontSize: 14, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500)),
const SizedBox(height: 10),
Obx(() {
final user = controller.userModel.value;
if (user == null) return const SizedBox();
return InkWell(
onTap: () {
Get.to(() => ProviderScreen(), arguments: {'providerId': user.id});
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isDark ? AppThemeData.grey500 : Colors.grey.shade100, width: 1),
color: isDark ? AppThemeData.grey500 : Colors.white,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: Row(
children: [
CircleAvatar(radius: 30, backgroundImage: NetworkImage(user.profilePictureURL?.isNotEmpty == true ? user.profilePictureURL! : Constant.placeHolderImage)),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(user.fullName(), style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 5),
Text(user.email ?? '', style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 14)),
const SizedBox(height: 10),
// Rating Box
Container(
decoration: BoxDecoration(color: AppThemeData.warning400, borderRadius: BorderRadius.circular(16)),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star, size: 16, color: Colors.white),
const SizedBox(width: 3),
Text(
double.parse(user.reviewsCount.toString()) != 0
? (double.parse(user.reviewsSum.toString()) / double.parse(user.reviewsCount.toString())).toStringAsFixed(1)
: '0',
style: const TextStyle(letterSpacing: 0.5, fontSize: 12, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500, color: Colors.white),
),
],
),
),
],
),
),
],
),
),
const Icon(Icons.chevron_right),
],
),
),
),
),
);
}),
],
),
);
}
Widget galleryTabViewWidget(OnDemandDetailsController controller) {
final photos = controller.provider.photos;
if (photos.isEmpty) {
return Center(child: Text("No Image Found".tr));
}
return GridView.builder(
itemCount: photos.length,
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, mainAxisSpacing: 0, crossAxisSpacing: 8, mainAxisExtent: 180),
itemBuilder: (context, index) {
final imageUrl = photos[index];
return Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: CachedNetworkImage(
imageUrl: imageUrl,
height: 60,
width: 60,
imageBuilder: (context, imageProvider) => Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), image: DecorationImage(image: imageProvider, fit: BoxFit.cover))),
placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))),
errorWidget: (context, url, error) => Image.network(Constant.placeHolderImage, fit: BoxFit.cover),
fit: BoxFit.cover,
),
),
);
},
);
}
Widget reviewTabViewWidget(OnDemandDetailsController controller, bool isDark) {
final reviews = controller.ratingService;
if (reviews.isEmpty) {
return SizedBox(height: 200, child: Center(child: Text("No review Found".tr, style: AppThemeData.mediumTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900))));
}
return ListView.builder(
itemCount: reviews.length,
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final review = reviews[index];
return Padding(
padding: const EdgeInsets.only(top: 10),
child: Container(
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: isDark ? AppThemeData.grey700 : AppThemeData.grey50,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shadows: const [BoxShadow(color: Color(0x0A000000), blurRadius: 32, offset: Offset(0, 0), spreadRadius: 0)],
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(review.uname ?? '', style: TextStyle(fontSize: 16, letterSpacing: 1, fontWeight: FontWeight.w600, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
Text(
review.createdAt != null ? DateFormat('dd MMM').format(review.createdAt!.toDate()) : '',
style: TextStyle(fontSize: 12, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
),
],
),
const SizedBox(height: 4),
RatingBar.builder(
initialRating: double.tryParse(review.rating.toString()) ?? 0,
direction: Axis.horizontal,
itemSize: 20,
ignoreGestures: true,
itemPadding: const EdgeInsets.symmetric(horizontal: 4.0),
itemBuilder: (context, _) => Icon(Icons.star, color: AppThemeData.primary300),
onRatingUpdate: (rate) {},
),
const Divider(),
const SizedBox(height: 5),
Text(review.comment ?? '', style: TextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
],
),
),
),
);
},
);
}
Widget showTiming(BuildContext context, OnDemandDetailsController controller, bool isDark) {
final provider = controller.provider;
return Container(
decoration: BoxDecoration(color: isDark ? AppThemeData.grey300 : Colors.white, borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20))),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
child: Text("Service Timing".tr, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, fontFamily: AppThemeData.regular, color: AppThemeData.primary300)),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
child: Row(
children: [
Expanded(child: _timeCard(context, "Start Time : ".tr, provider.startTime.toString(), isDark)),
const SizedBox(width: 10),
Expanded(child: _timeCard(context, "End Time : ".tr, provider.endTime.toString(), isDark)),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
child: Text("Service Days".tr, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, fontFamily: AppThemeData.regular, color: AppThemeData.primary300)),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Wrap(
spacing: 6.0,
runSpacing: 6.0,
children:
provider.days
.map(
(day) => Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6), side: BorderSide(color: isDark ? const Color(0XFF3c3a2e) : const Color(0XFFC3C5D1), width: 1)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 20),
child: Text(day, style: TextStyle(color: isDark ? const Color(0XFFa5a292) : const Color(0XFF5A5D6D))),
),
),
)
.toList(),
),
),
const SizedBox(height: 10),
],
),
);
}
Widget _timeCard(BuildContext context, String title, String value, bool isDark) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6), side: BorderSide(color: isDark ? const Color(0XFF3c3a2e) : const Color(0XFFC3C5D1), width: 1)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 20),
child: Row(
children: [
Text(title, style: TextStyle(color: isDark ? const Color(0XFFa5a292) : const Color(0XFF5A5D6D))),
Text(value, style: TextStyle(color: isDark ? const Color(0XFFa5a292) : const Color(0XFF5A5D6D))),
],
),
),
);
}
}

View File

@@ -0,0 +1,532 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:customer/constant/constant.dart';
import 'package:customer/controllers/theme_controller.dart';
import 'package:customer/models/banner_model.dart';
import 'package:customer/models/user_model.dart';
import 'package:customer/screen_ui/auth_screens/login_screen.dart';
import 'package:customer/screen_ui/location_enable_screens/address_list_screen.dart';
import 'package:customer/screen_ui/location_enable_screens/location_permission_screen.dart';
import 'package:customer/screen_ui/on_demand_service/view_all_popular_service_screen.dart';
import 'package:customer/screen_ui/on_demand_service/view_category_service_screen.dart';
import 'package:customer/themes/app_them_data.dart';
import 'package:customer/themes/round_button_fill.dart';
import 'package:customer/themes/show_toast_dialog.dart';
import 'package:customer/utils/network_image_widget.dart';
import 'package:customer/widget/osm_map/map_picker_page.dart';
import 'package:customer/widget/place_picker/location_picker_screen.dart';
import 'package:customer/widget/place_picker/selected_location_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import '../../controllers/on_demand_home_controller.dart';
import '../../models/category_model.dart';
import '../../models/provider_serivce_model.dart';
import 'on_demand_category_screen.dart';
import 'on_demand_details_screen.dart';
class OnDemandHomeScreen extends StatelessWidget {
const OnDemandHomeScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDark.value;
return GetX<OnDemandHomeController>(
init: OnDemandHomeController(),
builder: (controller) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: AppThemeData.primary300,
title: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
height: 42,
width: 42,
decoration: const BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50),
child: const Center(child: Padding(padding: EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Constant.userModel == null
? InkWell(onTap: () => Get.offAll(const LoginScreen()), child: Text("Login".tr, style: AppThemeData.boldTextStyle(color: AppThemeData.grey900, fontSize: 12)))
: Text(Constant.userModel!.fullName(), style: AppThemeData.boldTextStyle(color: AppThemeData.grey900, fontSize: 12)),
InkWell(
onTap: () async {
if (Constant.userModel != null) {
Get.to(AddressListScreen())!.then((value) {
if (value != null) {
ShippingAddress shippingAddress = value;
Constant.selectedLocation = shippingAddress;
controller.getData();
}
});
} else {
Constant.checkPermission(
onTap: () async {
ShowToastDialog.showLoader("Please wait...".tr);
// ✅ declare it once here!
ShippingAddress shippingAddress = ShippingAddress();
try {
await Geolocator.requestPermission();
await Geolocator.getCurrentPosition();
ShowToastDialog.closeLoader();
if (Constant.selectedMapType == 'osm') {
final result = await Get.to(() => MapPickerPage());
if (result != null) {
final firstPlace = result;
final lat = firstPlace.coordinates.latitude;
final lng = firstPlace.coordinates.longitude;
final address = firstPlace.address;
shippingAddress.addressAs = "Home";
shippingAddress.locality = address.toString();
shippingAddress.location = UserLocation(latitude: lat, longitude: lng);
Constant.selectedLocation = shippingAddress;
controller.getData();
Get.back();
}
} else {
Get.to(LocationPickerScreen())!.then((value) async {
if (value != null) {
SelectedLocationModel selectedLocationModel = value;
shippingAddress.addressAs = "Home";
shippingAddress.location = UserLocation(latitude: selectedLocationModel.latLng!.latitude, longitude: selectedLocationModel.latLng!.longitude);
shippingAddress.locality = "Picked from Map"; // You can reverse-geocode
Constant.selectedLocation = shippingAddress;
controller.getData();
}
});
}
} catch (e) {
await placemarkFromCoordinates(19.228825, 72.854118).then((valuePlaceMaker) {
Placemark placeMark = valuePlaceMaker[0];
shippingAddress.location = UserLocation(latitude: 19.228825, longitude: 72.854118);
String currentLocation =
"${placeMark.name}, ${placeMark.subLocality}, ${placeMark.locality}, ${placeMark.administrativeArea}, ${placeMark.postalCode}, ${placeMark.country}";
shippingAddress.locality = currentLocation;
});
Constant.selectedLocation = shippingAddress;
ShowToastDialog.closeLoader();
controller.getData();
}
},
context: context,
);
}
},
child: Text.rich(
maxLines: 1,
overflow: TextOverflow.ellipsis,
TextSpan(
children: [
TextSpan(
text: Constant.selectedLocation.getFullAddress(),
style: TextStyle(fontFamily: AppThemeData.medium, overflow: TextOverflow.ellipsis, color: AppThemeData.grey900, fontSize: 14),
),
WidgetSpan(child: SvgPicture.asset("assets/icons/ic_down.svg")),
],
),
),
),
],
),
),
],
),
),
),
body:
controller.isLoading.value
? Constant.loader()
: Constant.isZoneAvailable == false || controller.providerList.isEmpty
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset("assets/images/location.gif", height: 120),
const SizedBox(height: 12),
Text("No Store Found in Your Area".tr, style: TextStyle(color: isDark ? AppThemeData.grey100 : AppThemeData.grey800, fontSize: 22, fontFamily: AppThemeData.semiBold)),
const SizedBox(height: 5),
Text(
"Currently, there are no available store in your zone. Try changing your location to find nearby options.".tr,
textAlign: TextAlign.center,
style: TextStyle(color: isDark ? AppThemeData.grey50 : AppThemeData.grey500, fontSize: 16, fontFamily: AppThemeData.bold),
),
const SizedBox(height: 20),
RoundedButtonFill(
title: "Change Zone".tr,
width: 55,
height: 5.5,
color: AppThemeData.primary300,
textColor: AppThemeData.grey50,
onPress: () async {
Get.offAll(const LocationPermissionScreen());
},
),
],
),
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BannerView(bannerList: controller.bannerTopHome),
const SizedBox(height: 20),
Container(
height: MediaQuery.of(context).size.height * 0.12,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isDark ? AppThemeData.greyDark600 : AppThemeData.greyDark600, width: 1),
color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50,
),
child:
controller.categories.isEmpty
? Constant.showEmptyView(message: "No Categories".tr)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row(
children: [
Expanded(
child: ListView.builder(
itemCount: controller.categories.length > 3 ? 3 : controller.categories.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
final category = controller.categories[index];
return InkWell(
onTap: () {
Get.to(() => ViewCategoryServiceListScreen(), arguments: {'categoryId': category.id, 'categoryTitle': category.title});
},
child: CategoryView(category: category, index: index, isDark: isDark),
);
},
),
),
if (controller.categories.length > 3)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () {
Get.to(() => const OnDemandCategoryScreen());
},
child: ClipOval(child: Container(width: 50, height: 50, color: AppThemeData.grey200, child: const Center(child: Icon(Icons.chevron_right)))),
),
const SizedBox(height: 5),
SizedBox(
width: 70,
child: Center(
child: Text(
"View All".tr,
textAlign: TextAlign.center,
maxLines: 1,
style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
),
),
),
],
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 10),
child: Row(
children: [
Expanded(
child: Text(
"Most Popular services".tr,
style: TextStyle(color: isDark ? Colors.white : Colors.black, fontSize: 18, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w600),
),
),
InkWell(
onTap: () {
Get.to(() => ViewAllPopularServiceScreen());
},
child: Text("View all".tr, style: TextStyle(color: AppThemeData.primary300, fontSize: 14, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w600)),
),
],
),
),
controller.providerList.isEmpty
? Center(child: Text("No Services Found".tr))
: ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.providerList.length >= 6 ? 6 : controller.providerList.length,
itemBuilder: (_, index) {
return ServiceView(provider: controller.providerList[index], controller: controller, isDark: isDark);
},
),
],
),
),
),
);
},
);
}
}
class BannerView extends StatelessWidget {
final List<BannerModel> bannerList;
final RxInt currentPage = 0.obs;
final ScrollController scrollController = ScrollController();
BannerView({super.key, required this.bannerList});
void onScroll(BuildContext context) {
if (scrollController.hasClients && bannerList.isNotEmpty) {
final screenWidth = MediaQuery.of(context).size.width;
final itemWidth = screenWidth * 0.8 + 10; // banner width + spacing
final offset = scrollController.offset;
final index = (offset / itemWidth).round();
if (index != currentPage.value && index < bannerList.length) {
currentPage.value = index;
}
}
}
@override
Widget build(BuildContext context) {
scrollController.addListener(() {
onScroll(context);
});
return Column(
children: [
SizedBox(
height: 150,
child: ListView.separated(
controller: scrollController,
scrollDirection: Axis.horizontal,
itemCount: bannerList.length,
separatorBuilder: (context, index) => const SizedBox(width: 15),
itemBuilder: (context, index) {
final banner = bannerList[index];
return ClipRRect(
borderRadius: BorderRadius.circular(15),
child: SizedBox(width: MediaQuery.of(context).size.width * 0.8, child: NetworkImageWidget(imageUrl: banner.photo ?? '', fit: BoxFit.cover)),
);
},
),
),
const SizedBox(height: 8),
Obx(() {
return Row(
children: List.generate(bannerList.length, (index) {
bool isSelected = currentPage.value == index;
return Expanded(child: Container(height: 4, decoration: BoxDecoration(color: isSelected ? AppThemeData.grey300 : AppThemeData.grey100, borderRadius: BorderRadius.circular(5))));
}),
);
}),
],
);
}
}
class CategoryView extends StatelessWidget {
final CategoryModel category;
final int index;
final bool isDark;
const CategoryView({super.key, required this.category, required this.index, required this.isDark});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 55,
width: 55,
decoration: BoxDecoration(color: Constant.colorList[index % Constant.colorList.length], borderRadius: BorderRadius.circular(50)),
child: ClipOval(
child: Padding(
padding: const EdgeInsets.all(14.0),
child: CachedNetworkImage(imageUrl: category.image.toString(), errorWidget: (_, __, ___) => Image.network(Constant.placeHolderImage, fit: BoxFit.cover)),
),
),
),
const SizedBox(height: 5),
SizedBox(
width: 70,
child: Center(
child: Text(category.title ?? "", textAlign: TextAlign.center, maxLines: 1, style: AppThemeData.semiBoldTextStyle(color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
),
),
],
),
);
}
}
class ServiceView extends StatelessWidget {
final ProviderServiceModel provider;
final bool isDark;
final OnDemandHomeController? controller;
const ServiceView({super.key, required this.provider, this.isDark = false, this.controller});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Get.to(() => OnDemandDetailsScreen(), arguments: {'providerModel': provider});
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isDark ? AppThemeData.grey500 : Colors.grey.shade200),
color: isDark ? AppThemeData.grey900 : Colors.white,
),
child: Row(
children: [
// --- Left Image ---
ClipRRect(
borderRadius: const BorderRadius.only(topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)),
child: CachedNetworkImage(
imageUrl: provider.photos.isNotEmpty ? provider.photos[0] : Constant.placeHolderImage,
width: 110,
height: MediaQuery.of(context).size.height * 0.16,
fit: BoxFit.cover,
placeholder: (context, url) => Center(child: CircularProgressIndicator.adaptive(valueColor: AlwaysStoppedAnimation(AppThemeData.primary300))),
errorWidget: (context, url, error) => Image.network(Constant.placeHolderImage, fit: BoxFit.cover),
),
),
// --- Right Content ---
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title + Favourite icon
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
provider.title ?? "",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black),
),
),
if (controller != null)
Obx(
() => GestureDetector(
onTap: () => controller!.toggleFavourite(provider),
child: Icon(
controller!.lstFav.where((element) => element.service_id == provider.id).isNotEmpty ? Icons.favorite : Icons.favorite_border,
size: 24,
color: controller!.lstFav.where((element) => element.service_id == provider.id).isNotEmpty ? AppThemeData.primary300 : (isDark ? Colors.white38 : Colors.black38),
),
),
),
],
),
const SizedBox(height: 4),
// Category
if (controller != null)
FutureBuilder<CategoryModel?>(
future: controller!.getCategory(provider.categoryId ?? ""),
builder: (ctx, snap) {
if (!snap.hasData) return const SizedBox.shrink();
return Text(snap.data?.title ?? "", style: TextStyle(fontSize: 13, color: isDark ? Colors.white70 : Colors.black54));
},
),
const SizedBox(height: 4),
// Price
_buildPrice(),
const SizedBox(height: 6),
// Rating
_buildRating(),
],
),
),
),
],
),
),
);
}
Widget _buildPrice() {
if (provider.disPrice == "" || provider.disPrice == "0") {
return Text(
provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.price) : '${Constant.amountShow(amount: provider.price ?? "0")}/${'hr'.tr}',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: isDark ? Colors.white : AppThemeData.primary300),
);
} else {
return Row(
children: [
Text(
provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.disPrice ?? '0') : '${Constant.amountShow(amount: provider.disPrice)}/${'hr'.tr}',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: isDark ? Colors.white : AppThemeData.primary300),
),
const SizedBox(width: 6),
Flexible(
child: Text(
provider.priceUnit == 'Fixed' ? Constant.amountShow(amount: provider.price) : '${Constant.amountShow(amount: provider.price ?? "0")}/hr',
style: const TextStyle(fontSize: 12, color: Colors.grey, decoration: TextDecoration.lineThrough),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
}
Widget _buildRating() {
double rating = 0;
if (provider.reviewsCount != null && provider.reviewsCount != 0) {
rating = (provider.reviewsSum ?? 0) / (provider.reviewsCount ?? 1);
}
return Container(
decoration: BoxDecoration(color: AppThemeData.warning400, borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [const Icon(Icons.star, size: 14, color: Colors.white), const SizedBox(width: 3), Text(rating.toStringAsFixed(1), style: const TextStyle(fontSize: 12, color: Colors.white))],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,263 @@
import 'package:customer/constant/constant.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../controllers/0n_demand_payment_controller.dart';
import '../../controllers/theme_controller.dart';
import '../../payment/createRazorPayOrderModel.dart';
import '../../payment/rozorpayConroller.dart';
import '../../themes/app_them_data.dart';
import '../../themes/round_button_fill.dart';
import '../../themes/show_toast_dialog.dart';
import '../multi_vendor_service/wallet_screen/wallet_screen.dart';
class OnDemandPaymentScreen extends StatelessWidget {
const OnDemandPaymentScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDark.value;
return GetX<OnDemandPaymentController>(
init: OnDemandPaymentController(),
builder: (controller) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: AppThemeData.primary300,
title: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50),
child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))),
),
),
const SizedBox(width: 10),
Text("Select Payment Method".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)),
],
),
),
),
body:
controller.isLoading.value
? Constant.loader()
: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
decoration: BoxDecoration(color: isDark ? AppThemeData.greyDark200 : Colors.white),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Preferred Payment".tr, textAlign: TextAlign.start, style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500)),
const SizedBox(height: 10),
if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true)
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50,
border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Visibility(
visible: controller.walletSettingModel.value.isEnabled == true,
child: cardDecoration(controller, PaymentGateway.wallet, isDark, "assets/images/ic_wallet.png"),
),
Visibility(
visible: controller.cashOnDeliverySettingModel.value.isEnabled == true,
child: cardDecoration(controller, PaymentGateway.cod, isDark, "assets/images/ic_cash.png"),
),
],
),
),
),
if (controller.walletSettingModel.value.isEnabled == true || controller.cashOnDeliverySettingModel.value.isEnabled == true)
Column(
children: [
const SizedBox(height: 10),
Text(
"Other Payment Options".tr,
textAlign: TextAlign.start,
style: AppThemeData.boldTextStyle(fontSize: 15, color: isDark ? AppThemeData.greyDark500 : AppThemeData.grey500),
),
const SizedBox(height: 10),
],
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50,
border: Border.all(color: isDark ? AppThemeData.greyDark200 : AppThemeData.grey200),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Visibility(visible: controller.stripeModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.stripe, isDark, "assets/images/stripe.png")),
Visibility(visible: controller.payPalModel.value.isEnabled == true, child: cardDecoration(controller, PaymentGateway.paypal, isDark, "assets/images/paypal.png")),
Visibility(
visible: controller.payStackModel.value.isEnable == true,
child: cardDecoration(controller, PaymentGateway.payStack, isDark, "assets/images/paystack.png"),
),
Visibility(
visible: controller.mercadoPagoModel.value.isEnabled == true,
child: cardDecoration(controller, PaymentGateway.mercadoPago, isDark, "assets/images/mercado-pago.png"),
),
Visibility(
visible: controller.flutterWaveModel.value.isEnable == true,
child: cardDecoration(controller, PaymentGateway.flutterWave, isDark, "assets/images/flutterwave_logo.png"),
),
Visibility(visible: controller.payFastModel.value.isEnable == true, child: cardDecoration(controller, PaymentGateway.payFast, isDark, "assets/images/payfast.png")),
Visibility(
visible: controller.razorPayModel.value.isEnabled == true,
child: cardDecoration(controller, PaymentGateway.razorpay, isDark, "assets/images/razorpay.png"),
),
Visibility(visible: controller.midTransModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.midTrans, isDark, "assets/images/midtrans.png")),
Visibility(
visible: controller.orangeMoneyModel.value.enable == true,
child: cardDecoration(controller, PaymentGateway.orangeMoney, isDark, "assets/images/orange_money.png"),
),
Visibility(visible: controller.xenditModel.value.enable == true, child: cardDecoration(controller, PaymentGateway.xendit, isDark, "assets/images/xendit.png")),
],
),
),
),
SizedBox(height: 20),
RoundedButtonFill(
title: "Continue".tr,
color: AppThemeData.primary300,
textColor: AppThemeData.grey900,
onPress: () async {
print("getTotalAmount :::::::: ${"${controller.totalAmount.value}"}");
if (controller.isOrderPlaced.value == false) {
controller.isOrderPlaced.value = true;
if (controller.selectedPaymentMethod.value == PaymentGateway.stripe.name) {
controller.stripeMakePayment(amount: "${controller.totalAmount.value}");
} else if (controller.selectedPaymentMethod.value == PaymentGateway.paypal.name) {
controller.paypalPaymentSheet("${controller.totalAmount.value}", context);
} else if (controller.selectedPaymentMethod.value == PaymentGateway.payStack.name) {
controller.payStackPayment("${controller.totalAmount.value}");
} else if (controller.selectedPaymentMethod.value == PaymentGateway.mercadoPago.name) {
controller.mercadoPagoMakePayment(context: context, amount: "${controller.totalAmount.value}");
} else if (controller.selectedPaymentMethod.value == PaymentGateway.flutterWave.name) {
controller.flutterWaveInitiatePayment(context: context, amount: "${controller.totalAmount.value}");
} else if (controller.selectedPaymentMethod.value == PaymentGateway.payFast.name) {
controller.payFastPayment(context: context, amount: "${controller.totalAmount.value}");
} else if (controller.selectedPaymentMethod.value == PaymentGateway.wallet.name) {
double totalAmount = double.parse("${controller.totalAmount.value}");
double walletAmount = double.tryParse(Constant.userModel?.walletAmount?.toString() ?? "0") ?? 0;
if (walletAmount == 0) {
ShowToastDialog.showToast("Wallet balance is 0. Please recharge wallet.".tr);
} else if (walletAmount < totalAmount) {
ShowToastDialog.showToast("Insufficient wallet balance. Please add funds.".tr);
} else {
controller.placeOrder();
}
} else if (controller.selectedPaymentMethod.value == PaymentGateway.cod.name) {
controller.placeOrder();
} else if (controller.selectedPaymentMethod.value == PaymentGateway.wallet.name) {
controller.placeOrder();
} else if (controller.selectedPaymentMethod.value == PaymentGateway.midTrans.name) {
controller.midtransMakePayment(context: context, amount: "${controller.totalAmount.value}");
} else if (controller.selectedPaymentMethod.value == PaymentGateway.orangeMoney.name) {
controller.orangeMakePayment(context: context, amount: "${controller.totalAmount.value}");
} else if (controller.selectedPaymentMethod.value == PaymentGateway.xendit.name) {
controller.xenditPayment(context, "${controller.totalAmount.value}");
} else if (controller.selectedPaymentMethod.value == PaymentGateway.razorpay.name) {
RazorPayController().createOrderRazorPay(amount: double.parse("${controller.totalAmount.value}"), razorpayModel: controller.razorPayModel.value).then((value) {
if (value == null) {
Get.back();
ShowToastDialog.showToast("Something went wrong, please contact admin.".tr);
} else {
CreateRazorPayOrderModel result = value;
controller.openCheckout(amount: "${controller.totalAmount.value}", orderId: result.id);
}
});
} else {
controller.isOrderPlaced.value = false;
ShowToastDialog.showToast("Please select payment method".tr);
}
controller.isOrderPlaced.value = false;
}
},
),
],
),
),
),
);
},
);
}
Obx cardDecoration(controller, PaymentGateway value, isDark, String image) {
return Obx(
() => Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Column(
children: [
InkWell(
onTap: () {
controller.selectedPaymentMethod.value = value.name;
},
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: ShapeDecoration(shape: RoundedRectangleBorder(side: const BorderSide(width: 1, color: Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(8))),
child: Padding(padding: EdgeInsets.all(value.name == "payFast" ? 0 : 8.0), child: Image.asset(image)),
),
const SizedBox(width: 10),
value.name == "wallet"
? Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value.name.capitalizeString(),
textAlign: TextAlign.start,
style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900),
),
Text(
Constant.amountShow(amount: Constant.userModel?.walletAmount == null ? '0.0' : Constant.userModel?.walletAmount.toString()),
textAlign: TextAlign.start,
style: AppThemeData.semiBoldTextStyle(fontSize: 14, color: isDark ? AppThemeData.primary300 : AppThemeData.primary300),
),
],
),
)
: Expanded(
child: Text(
value.name.capitalizeString(),
textAlign: TextAlign.start,
style: AppThemeData.semiBoldTextStyle(fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900),
),
),
const Expanded(child: SizedBox()),
Radio(
value: value.name,
groupValue: controller.selectedPaymentMethod.value,
activeColor: isDark ? AppThemeData.primary300 : AppThemeData.primary300,
onChanged: (value) {
controller.selectedPaymentMethod.value = value.toString();
},
),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:get/get.dart';
import '../../constant/constant.dart';
import '../../controllers/on_demand_review_controller.dart';
import '../../controllers/theme_controller.dart';
import '../../themes/app_them_data.dart';
import '../../themes/round_button_fill.dart';
import '../../themes/text_field_widget.dart';
import '../../utils/network_image_widget.dart';
class OnDemandReviewScreen extends StatelessWidget {
const OnDemandReviewScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDark.value;
return GetBuilder<OnDemandReviewController>(
init: OnDemandReviewController(),
builder: (controller) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: AppThemeData.primary300,
title: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50),
child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))),
),
),
const SizedBox(width: 10),
Text(controller.ratingModel.value != null ? "Update Review".tr : "Add Review".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)),
],
),
),
),
body: Obx(
() =>
(controller.reviewFor.value == "Worker" && controller.workerModel.value == null) || (controller.reviewFor.value == "Provider" && controller.provider.value == null)
? Constant.loader()
: Padding(
padding: const EdgeInsets.only(top: 50.0),
child: Stack(
children: [
Padding(
padding: const EdgeInsets.only(left: 20, right: 20, top: 42, bottom: 20),
child: Card(
elevation: 2,
color: isDark ? AppThemeData.greyDark50 : AppThemeData.grey50,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(top: 65),
child: Column(
children: [
Text('Rate for'.tr, style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900)),
Text(
controller.reviewFor.value == "Provider" ? controller.order.value!.provider.authorName ?? "" : controller.workerModel.value!.fullName(),
style: AppThemeData.mediumTextStyle(fontSize: 18, color: isDark ? AppThemeData.greyDark900 : AppThemeData.grey900),
),
Padding(
padding: const EdgeInsets.only(top: 10),
child: RatingBar.builder(
initialRating: controller.ratings.value,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemPadding: const EdgeInsets.symmetric(horizontal: 4.0),
itemBuilder: (context, _) => const Icon(Icons.star, color: Colors.amber),
unratedColor: isDark ? AppThemeData.greyDark400 : AppThemeData.grey400,
onRatingUpdate: (rating) {
controller.ratings.value = rating;
},
),
),
Padding(padding: EdgeInsets.all(20.0), child: TextFieldWidget(hintText: "Type comment....".tr, controller: controller.comment, maxLine: 5)),
Padding(
padding: const EdgeInsets.all(20.0),
child: RoundedButtonFill(
title: controller.ratingModel.value != null ? "Update Review".tr : "Add Review".tr,
color: AppThemeData.primary300,
textColor: AppThemeData.grey50,
onPress: controller.submitReview,
),
),
],
),
),
),
),
),
Align(
alignment: Alignment.topCenter,
child: ClipRRect(
borderRadius: BorderRadius.circular(50),
child: NetworkImageWidget(
imageUrl: controller.reviewFor.value == "Provider" ? controller.order.value?.provider.authorProfilePic ?? '' : controller.workerModel.value?.profilePictureURL ?? '',
fit: BoxFit.cover,
height: 100,
width: 100,
),
),
),
],
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,127 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:customer/constant/constant.dart';
import 'package:customer/models/inbox_model.dart';
import 'package:customer/models/user_model.dart';
import 'package:customer/screen_ui/multi_vendor_service/chat_screens/chat_screen.dart';
import 'package:customer/themes/app_them_data.dart';
import 'package:customer/themes/responsive.dart';
import 'package:customer/utils/network_image_widget.dart';
import 'package:customer/widget/firebase_pagination/src/fireStore_pagination.dart';
import 'package:customer/widget/firebase_pagination/src/models/view_type.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controllers/theme_controller.dart';
import '../../../service/fire_store_utils.dart';
import '../../../themes/show_toast_dialog.dart';
class ProviderInboxScreen extends StatelessWidget {
const ProviderInboxScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDark.value;
return Scaffold(
appBar: AppBar(
backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface,
centerTitle: false,
titleSpacing: 0,
title: Text("Provider Inbox".tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)),
),
body: FirestorePagination(
//item builder type is compulsory.
physics: const BouncingScrollPhysics(),
itemBuilder: (context, documentSnapshots, index) {
final data = documentSnapshots[index].data() as Map<String, dynamic>?;
InboxModel inboxModel = InboxModel.fromJson(data!);
return InkWell(
onTap: () async {
ShowToastDialog.showLoader("Please wait...".tr);
UserModel? customer = await FireStoreUtils.getUserProfile(inboxModel.customerId.toString());
UserModel? restaurantUser = await FireStoreUtils.getUserProfile(inboxModel.restaurantId.toString());
ShowToastDialog.closeLoader();
Get.to(
const ChatScreen(),
arguments: {
"customerName": customer!.fullName(),
"restaurantName": restaurantUser!.fullName(),
"orderId": inboxModel.orderId,
"restaurantId": restaurantUser.id,
"customerId": customer.id,
"customerProfileImage": customer.profilePictureURL,
"restaurantProfileImage": restaurantUser.profilePictureURL,
"token": restaurantUser.fcmToken,
"chatType": inboxModel.chatType,
},
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5),
child: Container(
decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(10)),
child: NetworkImageWidget(
imageUrl: inboxModel.restaurantProfileImage.toString(),
fit: BoxFit.cover,
height: Responsive.height(6, context),
width: Responsive.width(12, context),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
"${inboxModel.restaurantName}",
textAlign: TextAlign.start,
style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey100 : AppThemeData.grey800),
),
),
Text(
Constant.timestampToDate(inboxModel.createdAt!),
textAlign: TextAlign.start,
style: TextStyle(fontFamily: AppThemeData.regular, fontSize: 16, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500),
),
],
),
const SizedBox(height: 5),
Text(
"${inboxModel.lastMessage}",
textAlign: TextAlign.start,
style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey200 : AppThemeData.grey700),
),
],
),
),
],
),
),
),
),
);
},
shrinkWrap: true,
onEmpty: Constant.showEmptyView(message: "No Conversion found".tr),
// orderBy is compulsory to enable pagination
query: FirebaseFirestore.instance.collection('chat_provider').where("customerId", isEqualTo: FireStoreUtils.getCurrentUid()).orderBy('createdAt', descending: true),
//Change types customerId
viewType: ViewType.list,
initialLoader: Constant.loader(),
// to fetch real-time data
isLive: true,
),
);
}
}

View File

@@ -0,0 +1,116 @@
import 'package:customer/constant/constant.dart';
import 'package:customer/screen_ui/on_demand_service/on_demand_home_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import '../../controllers/provider_controller.dart';
import '../../controllers/theme_controller.dart';
import '../../models/provider_serivce_model.dart';
import '../../themes/app_them_data.dart';
class ProviderScreen extends StatelessWidget {
const ProviderScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDark.value;
return GetX<ProviderController>(
init: ProviderController(),
builder: (controller) {
return Scaffold(
appBar: AppBar(automaticallyImplyLeading: true),
body:
controller.isLoading.value
? Center(child: Constant.loader())
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 50),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child:
(controller.userModel.value?.profilePictureURL ?? "").isNotEmpty
? CircleAvatar(backgroundImage: NetworkImage(controller.userModel.value?.profilePictureURL ?? ''), radius: 50.0)
: CircleAvatar(backgroundImage: NetworkImage(Constant.placeHolderImage), radius: 50.0),
),
const SizedBox(height: 10),
Text(
controller.userModel.value?.fullName() ?? '',
style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 20, fontWeight: FontWeight.w900),
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset("assets/icons/ic_mail.svg", color: isDark ? Colors.white : Colors.black),
const SizedBox(width: 6),
Text(
controller.userModel.value?.email ?? '',
style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 14, fontWeight: FontWeight.w500),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset("assets/icons/ic_mobile.svg", color: isDark ? Colors.white : Colors.black),
const SizedBox(width: 6),
Text(
controller.userModel.value?.phoneNumber ?? '',
style: TextStyle(color: isDark ? Colors.white : Colors.black, fontFamily: AppThemeData.regular, fontSize: 14, fontWeight: FontWeight.w500),
),
],
),
const SizedBox(height: 10),
Container(
decoration: const BoxDecoration(color: AppThemeData.warning400, borderRadius: BorderRadius.all(Radius.circular(16))),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star, size: 16, color: Colors.white),
const SizedBox(width: 3),
Text(
_getRating(controller),
style: const TextStyle(letterSpacing: 0.5, fontSize: 12, fontFamily: AppThemeData.regular, fontWeight: FontWeight.w500, color: Colors.white),
),
],
),
),
),
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 10),
controller.providerList.isEmpty
? Center(child: Text("No Services Found".tr))
: Expanded(
child: ListView.builder(
itemCount: controller.providerList.length,
padding: EdgeInsets.zero,
itemBuilder: (context, index) {
ProviderServiceModel data = controller.providerList[index];
return ServiceView(provider: data, isDark: isDark, controller: controller.onDemandHomeController.value);
},
),
),
],
),
),
);
},
);
}
String _getRating(ProviderController controller) {
final reviewsCount = double.tryParse(controller.userModel.value?.reviewsCount?.toString() ?? "0") ?? 0;
final reviewsSum = double.tryParse(controller.userModel.value?.reviewsSum?.toString() ?? "0") ?? 0;
if (reviewsCount == 0) return "0";
final avg = reviewsSum / reviewsCount;
return avg.toStringAsFixed(1);
}
}

View File

@@ -0,0 +1,74 @@
import 'package:customer/screen_ui/on_demand_service/on_demand_home_screen.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constant/constant.dart';
import '../../controllers/theme_controller.dart';
import '../../controllers/view_all_popular_service_controller.dart';
import '../../models/provider_serivce_model.dart';
import '../../themes/app_them_data.dart';
import '../../themes/text_field_widget.dart';
class ViewAllPopularServiceScreen extends StatelessWidget {
const ViewAllPopularServiceScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDark.value;
return GetX<ViewAllPopularServiceController>(
init: ViewAllPopularServiceController(),
builder: (controller) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: AppThemeData.primary300,
title: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50),
child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))),
),
),
const SizedBox(width: 10),
Text("All Services".tr, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)),
],
),
),
),
body:
controller.isLoading.value
? Constant.loader()
: Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
child: Column(
children: [
TextFieldWidget(hintText: "Search Service".tr, controller: controller.searchTextFiledController.value, onchange: (value) => controller.getFilterData(value.toString())),
const SizedBox(height: 15),
controller.providerList.isEmpty
? Expanded(child: Center(child: Constant.showEmptyView(message: "No service Found".tr)))
: Expanded(
child: ListView.builder(
itemCount: controller.providerList.length,
shrinkWrap: true,
padding: EdgeInsets.zero,
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
ProviderServiceModel data = controller.providerList[index];
return ServiceView(provider: data, isDark: isDark, controller: controller.onDemandHomeController.value);
},
),
),
],
),
),
);
},
);
}
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constant/constant.dart';
import '../../controllers/theme_controller.dart';
import '../../controllers/view_category_service_controller.dart';
import '../../models/provider_serivce_model.dart';
import '../../screen_ui/on_demand_service/on_demand_home_screen.dart';
import '../../themes/app_them_data.dart';
class ViewCategoryServiceListScreen extends StatelessWidget {
const ViewCategoryServiceListScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDark.value;
return GetX<ViewCategoryServiceController>(
init: ViewCategoryServiceController(),
builder: (controller) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: AppThemeData.primary300,
title: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(shape: BoxShape.circle, color: AppThemeData.grey50),
child: Center(child: Padding(padding: const EdgeInsets.only(left: 5), child: Icon(Icons.arrow_back_ios, color: AppThemeData.grey900, size: 20))),
),
),
const SizedBox(width: 10),
Text(controller.categoryTitle.value, style: AppThemeData.boldTextStyle(fontSize: 18, color: AppThemeData.grey900)),
],
),
),
),
body:
controller.isLoading.value
? Constant.loader()
: controller.providerList.isEmpty
? Constant.showEmptyView(message: "No Service Found".tr)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: ListView.builder(
itemCount: controller.providerList.length,
shrinkWrap: true,
padding: EdgeInsets.zero,
itemBuilder: (context, index) {
ProviderServiceModel providerModel = controller.providerList[index];
return ServiceView(isDark: isDark, provider: providerModel, controller: controller.onDemandHomeController.value);
},
),
),
);
},
);
}
}

View File

@@ -0,0 +1,128 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:customer/constant/constant.dart';
import 'package:customer/models/inbox_model.dart';
import 'package:customer/models/user_model.dart';
import 'package:customer/screen_ui/multi_vendor_service/chat_screens/chat_screen.dart';
import 'package:customer/themes/app_them_data.dart';
import 'package:customer/themes/responsive.dart';
import 'package:customer/utils/network_image_widget.dart';
import 'package:customer/widget/firebase_pagination/src/fireStore_pagination.dart';
import 'package:customer/widget/firebase_pagination/src/models/view_type.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controllers/theme_controller.dart';
import '../../../service/fire_store_utils.dart';
import '../../../themes/show_toast_dialog.dart';
class WorkerInboxScreen extends StatelessWidget {
const WorkerInboxScreen({super.key});
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDark.value;
return Scaffold(
appBar: AppBar(
backgroundColor: isDark ? AppThemeData.surfaceDark : AppThemeData.surface,
centerTitle: false,
titleSpacing: 0,
title: Text("Worker Inbox".tr, textAlign: TextAlign.start, style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 16, color: isDark ? AppThemeData.grey50 : AppThemeData.grey900)),
),
body: FirestorePagination(
//item builder type is compulsory.
physics: const BouncingScrollPhysics(),
itemBuilder: (context, documentSnapshots, index) {
final data = documentSnapshots[index].data() as Map<String, dynamic>?;
InboxModel inboxModel = InboxModel.fromJson(data!);
return InkWell(
onTap: () async {
ShowToastDialog.showLoader("Please wait...".tr);
UserModel? customer = await FireStoreUtils.getUserProfile(inboxModel.customerId.toString());
UserModel? restaurantUser = await FireStoreUtils.getUserProfile(inboxModel.restaurantId.toString());
ShowToastDialog.closeLoader();
print("customerId: ${inboxModel.customerId}, restaurantId: ${inboxModel.restaurantId}");
Get.to(
const ChatScreen(),
arguments: {
"customerName": customer!.fullName(),
"restaurantName": restaurantUser?.fullName() ?? '',
"orderId": inboxModel.orderId,
"restaurantId": restaurantUser?.id ?? '',
"customerId": customer.id,
"customerProfileImage": customer.profilePictureURL,
"restaurantProfileImage": restaurantUser?.profilePictureURL,
"token": restaurantUser?.fcmToken,
"chatType": inboxModel.chatType,
},
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5),
child: Container(
decoration: ShapeDecoration(color: isDark ? AppThemeData.grey900 : AppThemeData.grey50, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(10)),
child: NetworkImageWidget(
imageUrl: inboxModel.restaurantProfileImage.toString(),
fit: BoxFit.cover,
height: Responsive.height(6, context),
width: Responsive.width(12, context),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
"${inboxModel.restaurantName}",
textAlign: TextAlign.start,
style: TextStyle(fontFamily: AppThemeData.semiBold, fontSize: 16, color: isDark ? AppThemeData.grey100 : AppThemeData.grey800),
),
),
Text(
Constant.timestampToDate(inboxModel.createdAt!),
textAlign: TextAlign.start,
style: TextStyle(fontFamily: AppThemeData.regular, fontSize: 16, color: isDark ? AppThemeData.grey400 : AppThemeData.grey500),
),
],
),
const SizedBox(height: 5),
Text(
"${inboxModel.lastMessage}",
textAlign: TextAlign.start,
style: TextStyle(fontFamily: AppThemeData.medium, fontSize: 14, color: isDark ? AppThemeData.grey200 : AppThemeData.grey700),
),
],
),
),
],
),
),
),
),
);
},
shrinkWrap: true,
onEmpty: Constant.showEmptyView(message: "No Conversion found".tr),
// orderBy is compulsory to enable pagination
query: FirebaseFirestore.instance.collection('chat_worker').where("customerId", isEqualTo: FireStoreUtils.getCurrentUid()).orderBy('createdAt', descending: true),
//Change types customerId
viewType: ViewType.list,
initialLoader: Constant.loader(),
// to fetch real-time data
isLive: true,
),
);
}
}